//! Toolbox Utility trap handlers (events, Random, Sound, misc).

use crate::cpu::{CpuOps, Register};
use crate::memory::globals::addr;
use crate::memory::{MacMemoryBus, MemoryBus};
use crate::quickdraw::fonts::get_font_face_scaled;
use crate::quickdraw::text::{get_font_metrics, get_glyph};
use crate::{Error, Result};
use std::collections::HashMap;
use std::sync::OnceLock;

use super::dispatch::MovieState;
use super::types::{decode_mac_roman, encode_mac_roman_lossy, Rect, ShapeOp};

static TRACE_MUNGER: OnceLock<bool> = OnceLock::new();
static TRACE_LIST: OnceLock<bool> = OnceLock::new();
static TRACE_ENTROPY: OnceLock<bool> = OnceLock::new();
static TRACE_TITLE_DIAG: OnceLock<bool> = OnceLock::new();
static TRACE_SOUND: OnceLock<bool> = OnceLock::new();
static TRACE_AE: OnceLock<bool> = OnceLock::new();
static TRACE_GETKEYS_NONZERO: OnceLock<bool> = OnceLock::new();
static FORCE_BUTTON_TRUE_AT_PC: OnceLock<Option<u32>> = OnceLock::new();

const AE_TYPE_APPLE_EVENT: u32 = u32::from_be_bytes(*b"aevt");
const AE_TYPE_TYPE: u32 = u32::from_be_bytes(*b"type");
const AE_TYPE_NULL: u32 = u32::from_be_bytes(*b"null");
const AE_TYPE_WILDCARD: u32 = u32::from_be_bytes(*b"****");
const AE_TYPE_OBJECT_SPECIFIER: u32 = u32::from_be_bytes(*b"obj ");
const AE_KEY_EVENT_CLASS_ATTR: u32 = u32::from_be_bytes(*b"evcl");
const AE_KEY_EVENT_ID_ATTR: u32 = u32::from_be_bytes(*b"evid");
const AE_ERR_COERCION_FAIL: i16 = -1700;
const AE_ERR_DESC_NOT_FOUND: i16 = -1701;
const AE_ERR_ACCESSOR_NOT_FOUND: i16 = -1723;
const AE_ERR_NOT_AN_OBJECT_SPEC: i16 = -1727;
const AE_BUFFER_IS_SMALL: i16 = -607;
const QUICKTIME_INVALID_MOVIE: i16 = -2010;

fn standard_file_cancel_reply(bus: &mut MacMemoryBus, reply_ptr: u32) {
    if reply_ptr != 0 {
        // SFReply and StandardFileReply both start with the cancel flag.
        bus.write_byte(reply_ptr, 0);
    }
}

fn standard_file_default_name(bus: &MacMemoryBus, name_ptr: u32) -> Vec<u8> {
    let mut name = if name_ptr != 0 {
        bus.read_pstring(name_ptr)
    } else {
        Vec::new()
    };
    if name.is_empty() {
        name.extend_from_slice(b"Untitled");
    }
    name.truncate(63);
    name
}

fn standard_file_put_reply_old(bus: &mut MacMemoryBus, reply_ptr: u32, vref: i16, name: &[u8]) {
    if reply_ptr == 0 {
        return;
    }
    bus.write_byte(reply_ptr, 0xFF); // good = TRUE
    bus.write_byte(reply_ptr + 1, 0); // copy = FALSE
    bus.write_long(reply_ptr + 2, 0); // fType
    bus.write_word(reply_ptr + 6, vref as u16);
    bus.write_word(reply_ptr + 8, 0); // version
    bus.write_pstring(reply_ptr + 10, name);
}

fn standard_file_get_reply_old(
    bus: &mut MacMemoryBus,
    reply_ptr: u32,
    wd_ref: i16,
    file_type: u32,
    name: &[u8],
) {
    if reply_ptr == 0 {
        return;
    }
    bus.write_byte(reply_ptr, 0xFF); // good = TRUE
    bus.write_byte(reply_ptr + 1, 0); // copy = FALSE
    bus.write_long(reply_ptr + 2, file_type);
    bus.write_word(reply_ptr + 6, wd_ref as u16);
    bus.write_word(reply_ptr + 8, 0); // version
    bus.write_pstring(reply_ptr + 10, name);
}

fn standard_file_put_reply_modern(
    bus: &mut MacMemoryBus,
    reply_ptr: u32,
    vref: i16,
    dir_id: u32,
    name: &[u8],
) {
    if reply_ptr == 0 {
        return;
    }
    bus.write_byte(reply_ptr, 0xFF); // sfGood = TRUE
    bus.write_byte(reply_ptr + 1, 0); // sfReplacing = FALSE
    bus.write_long(reply_ptr + 2, 0); // sfType
    bus.write_word(reply_ptr + 6, vref as u16); // sfFile.vRefNum
    bus.write_long(reply_ptr + 8, dir_id); // sfFile.parID
    bus.write_pstring(reply_ptr + 12, name); // sfFile.name
    bus.write_word(reply_ptr + 76, 0); // sfScript
    bus.write_word(reply_ptr + 78, 0); // sfFlags
    bus.write_byte(reply_ptr + 80, 0); // sfIsFolder
    bus.write_byte(reply_ptr + 81, 0); // sfIsVolume
    bus.write_long(reply_ptr + 82, 0); // sfReserved1
    bus.write_word(reply_ptr + 86, 0); // sfReserved2
}

fn standard_file_get_reply_modern(
    bus: &mut MacMemoryBus,
    reply_ptr: u32,
    vref: i16,
    dir_id: u32,
    file_type: u32,
    finder_flags: u16,
    name: &[u8],
) {
    if reply_ptr == 0 {
        return;
    }
    bus.write_byte(reply_ptr, 0xFF); // sfGood = TRUE
    bus.write_byte(reply_ptr + 1, 0); // sfReplacing = FALSE
    bus.write_long(reply_ptr + 2, file_type); // sfType
    bus.write_word(reply_ptr + 6, vref as u16); // sfFile.vRefNum
    bus.write_long(reply_ptr + 8, dir_id); // sfFile.parID
    bus.write_pstring(reply_ptr + 12, name); // sfFile.name
    bus.write_word(reply_ptr + 76, 0); // sfScript
    bus.write_word(reply_ptr + 78, finder_flags); // sfFlags
    bus.write_byte(reply_ptr + 80, 0); // sfIsFolder
    bus.write_byte(reply_ptr + 81, 0); // sfIsVolume
    bus.write_long(reply_ptr + 82, 0); // sfReserved1
    bus.write_word(reply_ptr + 86, 0); // sfReserved2
}

#[derive(Clone, Debug)]
struct StandardFileSelection {
    name: Vec<u8>,
    vref: i16,
    wd_ref: i16,
    dir_id: u32,
    file_type: u32,
    finder_flags: u16,
}

#[inline]
fn return_noerr_and_pop<C: CpuOps>(cpu: &mut C, bytes: u32) -> Result<()> {
    let sp = cpu.read_reg(Register::A7);
    cpu.write_reg(Register::A7, sp.wrapping_add(bytes));
    cpu.write_reg(Register::D0, 0);
    Ok(())
}

#[inline]
fn return_noerr<C: CpuOps>(cpu: &mut C) -> Result<()> {
    cpu.write_reg(Register::D0, 0);
    Ok(())
}

#[inline]
fn return_error_and_pop<C: CpuOps>(cpu: &mut C, bytes: u32, err: i16) -> Result<()> {
    let sp = cpu.read_reg(Register::A7);
    cpu.write_reg(Register::A7, sp.wrapping_add(bytes));
    cpu.write_reg(Register::D0, err as u32);
    Ok(())
}

fn signed_byte_from_stack_word(word: u16) -> i16 {
    let byte = if word & 0x00FF == 0 {
        (word >> 8) as u8
    } else {
        (word & 0x00FF) as u8
    };
    i16::from(byte as i8)
}

fn read_quicktime_u32(data: &[u8], offset: usize) -> Option<u32> {
    let bytes = data.get(offset..offset + 4)?;
    Some(u32::from_be_bytes(bytes.try_into().ok()?))
}

fn fixed_16_16_to_i16_dimension(value: u32) -> Option<i16> {
    let whole = (value >> 16).min(i16::MAX as u32);
    (whole > 0).then_some(whole as i16)
}

fn scan_quicktime_atoms(
    data: &[u8],
    depth: u8,
    bounds: &mut Option<(i16, i16, i16, i16)>,
    duration: &mut Option<i32>,
) {
    if depth > 8 {
        return;
    }
    let mut offset = 0usize;
    while offset + 8 <= data.len() {
        let Some(size32) = read_quicktime_u32(data, offset) else {
            break;
        };
        let atom_type = &data[offset + 4..offset + 8];
        let mut header_len = 8usize;
        let atom_len = if size32 == 1 {
            if offset + 16 > data.len() {
                break;
            }
            header_len = 16;
            let Some(high) = read_quicktime_u32(data, offset + 8) else {
                break;
            };
            let Some(low) = read_quicktime_u32(data, offset + 12) else {
                break;
            };
            let size64 = ((high as u64) << 32) | u64::from(low);
            if size64 > usize::MAX as u64 {
                break;
            }
            size64 as usize
        } else if size32 == 0 {
            data.len() - offset
        } else {
            size32 as usize
        };
        if atom_len < header_len || offset + atom_len > data.len() {
            break;
        }
        let payload = &data[offset + header_len..offset + atom_len];
        match atom_type {
            b"moov" | b"trak" | b"mdia" | b"minf" | b"stbl" => {
                scan_quicktime_atoms(payload, depth + 1, bounds, duration);
            }
            b"mvhd" => {
                if duration.is_none() && payload.len() >= 20 {
                    let version = payload[0];
                    let parsed_duration = if version == 0 {
                        read_quicktime_u32(payload, 16).map(|value| value.min(i32::MAX as u32))
                    } else if payload.len() >= 32 {
                        read_quicktime_u32(payload, 28).map(|value| value.min(i32::MAX as u32))
                    } else {
                        None
                    };
                    if let Some(value) = parsed_duration.filter(|value| *value > 0) {
                        *duration = Some(value as i32);
                    }
                }
            }
            b"tkhd" => {
                if bounds.is_none() {
                    let version = payload.first().copied().unwrap_or(0);
                    let dimension_offset = if version == 0 { 76 } else { 88 };
                    if payload.len() >= dimension_offset + 8 {
                        let width = read_quicktime_u32(payload, dimension_offset)
                            .and_then(fixed_16_16_to_i16_dimension);
                        let height = read_quicktime_u32(payload, dimension_offset + 4)
                            .and_then(fixed_16_16_to_i16_dimension);
                        if let (Some(width), Some(height)) = (width, height) {
                            *bounds = Some((0, 0, height, width));
                        }
                    }
                }
            }
            _ => {}
        }
        if size32 == 0 {
            break;
        }
        offset += atom_len;
    }
}

fn quicktime_movie_metadata(data: &[u8]) -> ((i16, i16, i16, i16), i32) {
    let mut bounds = None;
    let mut duration = None;
    scan_quicktime_atoms(data, 0, &mut bounds, &mut duration);
    (bounds.unwrap_or((0, 0, 120, 160)), duration.unwrap_or(1))
}

fn write_movie_box(bus: &mut MacMemoryBus, rect_ptr: u32, rect: (i16, i16, i16, i16)) {
    if rect_ptr == 0 {
        return;
    }
    bus.write_word(rect_ptr, rect.0 as u16);
    bus.write_word(rect_ptr + 2, rect.1 as u16);
    bus.write_word(rect_ptr + 4, rect.2 as u16);
    bus.write_word(rect_ptr + 6, rect.3 as u16);
}

fn read_movie_box(bus: &MacMemoryBus, rect_ptr: u32) -> Option<(i16, i16, i16, i16)> {
    (rect_ptr != 0).then(|| {
        (
            bus.read_word(rect_ptr) as i16,
            bus.read_word(rect_ptr + 2) as i16,
            bus.read_word(rect_ptr + 4) as i16,
            bus.read_word(rect_ptr + 6) as i16,
        )
    })
}

fn read_allocated_bytes(bus: &MacMemoryBus, ptr: u32) -> Vec<u8> {
    let size = bus.get_alloc_size(ptr).unwrap_or(0) as usize;
    if size == 0 {
        Vec::new()
    } else {
        bus.read_bytes(ptr, size)
    }
}

fn record_movie_error(dispatcher: &mut super::TrapDispatcher, err: i16) {
    dispatcher.movie_error = err;
    if err != 0 && dispatcher.movie_sticky_error == 0 {
        dispatcher.movie_sticky_error = err;
    }
}

/// Returns true if `year` is a leap year in the Gregorian calendar.
fn is_leap_year(year: u32) -> bool {
    (year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400)
}

/// Days in each month (index 1-based). February is 28; caller must add 1 for leap years.
const DAYS_IN_MONTH: [u32; 13] = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

/// Convert seconds since Mac epoch (Jan 1, 1904 00:00:00) to DateTimeRec fields.
/// Returns (year, month, day, hour, minute, second, dayOfWeek).
/// dayOfWeek: 1=Sunday..7=Saturday.
/// Inside Macintosh Volume II, II-379
fn secs_to_date(secs: u32) -> (u16, u16, u16, u16, u16, u16, u16) {
    // Jan 1, 1904 was a Friday = dayOfWeek 6
    let day_of_week = ((secs / 86400 + 5) % 7 + 1) as u16; // +5 because Jan 1 1904 = Friday (6), Sunday=1

    let mut remaining = secs;
    let second = (remaining % 60) as u16;
    remaining /= 60;
    let minute = (remaining % 60) as u16;
    remaining /= 60;
    let hour = (remaining % 24) as u16;
    let mut days = remaining / 24;

    let mut year = 1904u32;
    loop {
        let days_in_year = if is_leap_year(year) { 366 } else { 365 };
        if days < days_in_year {
            break;
        }
        days -= days_in_year;
        year += 1;
    }

    let mut month = 1u32;
    loop {
        let mut dim = DAYS_IN_MONTH[month as usize];
        if month == 2 && is_leap_year(year) {
            dim += 1;
        }
        if days < dim {
            break;
        }
        days -= dim;
        month += 1;
    }
    let day = days + 1; // 1-based

    (
        year as u16,
        month as u16,
        day as u16,
        hour,
        minute,
        second,
        day_of_week,
    )
}

/// Convert DateTimeRec fields to seconds since Mac epoch (Jan 1, 1904 00:00:00).
/// Inside Macintosh Volume II, II-379
fn date_to_secs(year: u32, month: u32, day: u32, hour: u32, minute: u32, second: u32) -> u32 {
    let mut days: u32 = 0;
    for y in 1904..year {
        days += if is_leap_year(y) { 366 } else { 365 };
    }
    for m in 1..month {
        days += DAYS_IN_MONTH[m as usize];
        if m == 2 && is_leap_year(year) {
            days += 1;
        }
    }
    days += day - 1; // day is 1-based
    days * 86400 + hour * 3600 + minute * 60 + second
}

fn trace_munger_enabled() -> bool {
    *TRACE_MUNGER.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_MUNGER").is_some())
}

fn trace_list_manager_enabled() -> bool {
    *TRACE_LIST.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_LIST").is_some())
}

fn trace_entropy_enabled() -> bool {
    *TRACE_ENTROPY.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_ENTROPY").is_some())
}

fn trace_title_diag_enabled() -> bool {
    *TRACE_TITLE_DIAG.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_TITLE_DIAG").is_some())
}

fn trace_sound_enabled() -> bool {
    *TRACE_SOUND.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_SOUND").is_some())
}

fn trace_ae_enabled() -> bool {
    *TRACE_AE.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_AE").is_some())
}

fn trace_getkeys_nonzero_enabled() -> bool {
    *TRACE_GETKEYS_NONZERO
        .get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_GETKEYS_NONZERO").is_some())
}

fn format_fourcc(v: u32) -> String {
    v.to_be_bytes()
        .iter()
        .map(|&b| {
            if b.is_ascii_graphic() || b == b' ' {
                b as char
            } else {
                '.'
            }
        })
        .collect()
}

fn write_null_aedesc(bus: &mut MacMemoryBus, desc_ptr: u32) {
    if desc_ptr != 0 {
        bus.write_long(desc_ptr, AE_TYPE_NULL);
        bus.write_long(desc_ptr + 4, 0);
    }
}

fn force_button_true_at_pc() -> Option<u32> {
    *FORCE_BUTTON_TRUE_AT_PC.get_or_init(|| {
        let s = std::env::var("SYSTEMLESS_FORCE_BUTTON_TRUE_AT_PC").ok()?;
        let s = s.strip_prefix("0x").unwrap_or(&s);
        u32::from_str_radix(s, 16).ok()
    })
}

impl super::TrapDispatcher {
    const LIST_RVIEW_OFFSET: u32 = 0;
    const LIST_PORT_OFFSET: u32 = 8;
    const LIST_INDENT_OFFSET: u32 = 12;
    const LIST_CELL_SIZE_OFFSET: u32 = 16;
    const LIST_VISIBLE_OFFSET: u32 = 20;
    const LIST_VSCROLL_OFFSET: u32 = 28;
    const LIST_HSCROLL_OFFSET: u32 = 32;
    const LIST_SEL_FLAGS_OFFSET: u32 = 36;
    const LIST_ACTIVE_OFFSET: u32 = 37;
    const LIST_RESERVED_OFFSET: u32 = 38;
    const LIST_FLAGS_OFFSET: u32 = 39;
    const LIST_CLICK_TIME_OFFSET: u32 = 40;
    const LIST_CLICK_LOC_OFFSET: u32 = 44;
    const LIST_MOUSE_LOC_OFFSET: u32 = 48;
    const LIST_CLICK_LOOP_OFFSET: u32 = 52;
    const LIST_LAST_CLICK_OFFSET: u32 = 56;
    const LIST_REFCON_OFFSET: u32 = 60;
    const LIST_DEF_PROC_OFFSET: u32 = 64;
    const LIST_USER_HANDLE_OFFSET: u32 = 68;
    const LIST_DATA_BOUNDS_OFFSET: u32 = 72;
    const LIST_CELLS_OFFSET: u32 = 80;
    const LIST_MAX_INDEX_OFFSET: u32 = 84;
    const LIST_CELL_ARRAY_OFFSET: u32 = 86;
    const LIST_RECORD_SIZE: u32 = 88;
    const LIST_DOUBLE_CLICK_TICKS: u32 = 20;
    const LIST_LDRAW_MSG: i16 = 1;
    const LIST_LHILITE_MSG: i16 = 2;
    const LIST_DEF_TRAMPOLINE_SIZE: u32 = 60;
    const ALIAS_RECORD_FIXED_SIZE: usize = 150;
    const ALIAS_RECORD_VERSION: u16 = 2;
    const ALIAS_KIND_FILE: u16 = 0;
    const ALIAS_KIND_FOLDER: u16 = 1;
    const ALIAS_EXTRA_PARENT_DIR_NAME: i16 = 0;
    const ALIAS_EXTRA_END: i16 = -1;

    fn apple_event_handler_for(&self, event_class: u32, event_id: u32) -> Option<(u32, u32)> {
        // Inside Macintosh Volume VI, 6-28 to 6-29: AEInstallEventHandler
        // entries may use typeWildCard ('****') for event class, event ID,
        // or both; wildcard entries match all possible values.
        [
            (event_class, event_id),
            (event_class, AE_TYPE_WILDCARD),
            (AE_TYPE_WILDCARD, event_id),
            (AE_TYPE_WILDCARD, AE_TYPE_WILDCARD),
        ]
        .into_iter()
        .find_map(|key| self.ae_handlers.get(&key).copied())
    }

    fn stack_bool_slot(bus: &MacMemoryBus, addr: u32) -> bool {
        // MPW Pascal callers store BOOLEAN in the high byte of the
        // 2-byte stack slot. The low byte is padding and can retain
        // unrelated non-zero garbage, so only the first byte is
        // semantically meaningful.
        bus.read_byte(addr) != 0
    }

    fn read_stack_point(bus: &MacMemoryBus, addr: u32) -> (i16, i16) {
        (bus.read_word(addr) as i16, bus.read_word(addr + 2) as i16)
    }

    fn read_rect_ptr(bus: &MacMemoryBus, ptr: u32) -> (i16, i16, i16, i16) {
        (
            bus.read_word(ptr) as i16,
            bus.read_word(ptr + 2) as i16,
            bus.read_word(ptr + 4) as i16,
            bus.read_word(ptr + 6) as i16,
        )
    }

    fn write_rect_words(bus: &mut MacMemoryBus, addr: u32, rect: (i16, i16, i16, i16)) {
        bus.write_word(addr, rect.0 as u16);
        bus.write_word(addr + 2, rect.1 as u16);
        bus.write_word(addr + 4, rect.2 as u16);
        bus.write_word(addr + 6, rect.3 as u16);
    }

    fn standard_file_env_selection(&mut self) -> Option<StandardFileSelection> {
        let requested = std::env::var("SYSTEMLESS_STANDARD_GET_FILE").ok()?;
        let requested = requested.trim();
        if requested.is_empty() {
            return None;
        }

        let default_dir_id = self.default_dir_id;
        let vfs_key = self
            .find_vfs_file_in_directory(default_dir_id, requested)
            .or_else(|| self.find_vfs_rsrc_file_in_directory(default_dir_id, requested))
            .or_else(|| self.find_vfs_file(requested))
            .or_else(|| self.find_vfs_rsrc_file(requested))?;
        let metadata = self.vfs_file_metadata(&vfs_key)?;
        let vref = Self::boot_volume_ref_num();
        let wd_ref = self
            .open_working_directory(vref, metadata.parent_dir_id, 0)
            .unwrap_or(vref);
        let mut name = encode_mac_roman_lossy(Self::vfs_basename(&vfs_key));
        name.truncate(63);

        Some(StandardFileSelection {
            name,
            vref,
            wd_ref,
            dir_id: metadata.parent_dir_id,
            file_type: metadata.file_type,
            finder_flags: metadata.finder_flags,
        })
    }

    fn standard_file_auto_selection(
        &mut self,
        bus: &MacMemoryBus,
        num_types: i16,
        type_list_ptr: u32,
    ) -> Option<StandardFileSelection> {
        // Inside Macintosh: Files (1992), p. 3-50: Standard File first
        // filters the display list by numTypes/typeList; numTypes = -1 lets
        // all file types pass, while 0 displays no files. The headless HLE
        // auto-selects the first positive type-list hit, or the only current
        // directory file in all-types mode.
        if num_types == 0 || num_types < -1 || (num_types > 0 && type_list_ptr == 0) {
            return None;
        }

        let file_types = if num_types > 0 {
            let count = (num_types as usize).min(64);
            let mut file_types = Vec::with_capacity(count);
            for index in 0..count {
                file_types.push(bus.read_long(type_list_ptr + (index as u32 * 4)));
            }
            Some(file_types)
        } else {
            None
        };

        let default_dir_id = self.default_dir_id;
        let entries = self.list_vfs_catalog_entries(default_dir_id);
        let mut all_types_match: Option<StandardFileSelection> = None;
        for entry in entries {
            if entry.is_directory {
                continue;
            }
            let Some(metadata) = self.vfs_file_metadata(&entry.path) else {
                continue;
            };
            if let Some(file_types) = &file_types {
                if !file_types.contains(&metadata.file_type) {
                    continue;
                }
            } else if all_types_match.is_some() {
                return None;
            }

            let selection = {
                let vref = Self::boot_volume_ref_num();
                let wd_ref = self
                    .open_working_directory(vref, metadata.parent_dir_id, 0)
                    .unwrap_or(vref);
                let mut name = encode_mac_roman_lossy(Self::vfs_basename(&entry.path));
                name.truncate(63);
                StandardFileSelection {
                    name,
                    vref,
                    wd_ref,
                    dir_id: metadata.parent_dir_id,
                    file_type: metadata.file_type,
                    finder_flags: metadata.finder_flags,
                }
            };
            if file_types.is_none() {
                all_types_match = Some(selection);
                continue;
            }

            return Some(selection);
        }
        all_types_match
    }

    fn build_alias_record(&mut self, bus: &MacMemoryBus, target_ptr: u32) -> Vec<u8> {
        let vref = bus.read_word(target_ptr) as i16;
        let dir_id = bus.read_long(target_ptr + 2);
        let target_name = crate::trap::types::read_fsspec_name(bus, target_ptr);
        let resolved_dir_id = self.resolve_directory_id(vref, dir_id);
        let target_key = self.vfs_key_for_fsspec(vref, dir_id, &target_name);
        let target_directory = target_key
            .as_ref()
            .and_then(|key| self.vfs_directories.get(key).cloned());
        let target_metadata = target_key
            .as_ref()
            .and_then(|key| self.vfs_file_metadata(key));

        let alias_kind = if target_directory.is_some() {
            Self::ALIAS_KIND_FOLDER
        } else {
            Self::ALIAS_KIND_FILE
        };
        let parent_dir_id = target_metadata
            .map(|metadata| metadata.parent_dir_id)
            .or_else(|| target_directory.as_ref().map(|dir| dir.parent_dir_id))
            .unwrap_or(resolved_dir_id);
        let file_id = target_metadata
            .map(|metadata| metadata.file_id)
            .or_else(|| target_directory.as_ref().map(|dir| dir.dir_id))
            .unwrap_or(0);
        let created_date = target_metadata
            .map(|metadata| metadata.created_date)
            .unwrap_or(0);
        let file_type = target_metadata
            .map(|metadata| metadata.file_type)
            .unwrap_or(0);
        let creator = target_metadata
            .map(|metadata| metadata.creator)
            .unwrap_or(0);

        let mut record = vec![0; Self::ALIAS_RECORD_FIXED_SIZE];
        Self::alias_write_u32(&mut record, 0, 0); // userType
        Self::alias_write_u16(&mut record, 6, Self::ALIAS_RECORD_VERSION);
        Self::alias_write_u16(&mut record, 8, alias_kind);
        Self::alias_write_pstring(&mut record, 10, 28, Self::boot_volume_name());
        Self::alias_write_u32(&mut record, 38, 0); // volume creation date
        Self::alias_write_u16(&mut record, 42, 0x4244); // HFS volume signature 'BD'
        Self::alias_write_u16(&mut record, 44, 0); // fixed hard disk
        Self::alias_write_u32(&mut record, 46, parent_dir_id);
        Self::alias_write_pstring(&mut record, 50, 64, &target_name);
        Self::alias_write_u32(&mut record, 114, file_id);
        Self::alias_write_u32(&mut record, 118, created_date);
        Self::alias_write_u32(&mut record, 122, file_type);
        Self::alias_write_u32(&mut record, 126, creator);
        Self::alias_write_i16(&mut record, 130, -1); // no relative source file
        Self::alias_write_i16(&mut record, 132, -1);
        Self::alias_write_u32(&mut record, 134, 0);
        Self::alias_write_u16(&mut record, 138, 0);

        let parent_name = self
            .directory_path_for_id(parent_dir_id)
            .map(Self::vfs_basename)
            .filter(|name| !name.is_empty())
            .map(str::to_owned)
            .unwrap_or_else(|| Self::boot_volume_name().to_string());
        Self::append_alias_extra(
            &mut record,
            Self::ALIAS_EXTRA_PARENT_DIR_NAME,
            &encode_mac_roman_lossy(&parent_name),
        );
        Self::append_alias_extra(&mut record, Self::ALIAS_EXTRA_END, &[]);

        let size = record.len().min(u16::MAX as usize) as u16;
        Self::alias_write_u16(&mut record, 4, size);
        record
    }

    fn alias_write_u16(record: &mut [u8], offset: usize, value: u16) {
        record[offset..offset + 2].copy_from_slice(&value.to_be_bytes());
    }

    fn alias_write_i16(record: &mut [u8], offset: usize, value: i16) {
        Self::alias_write_u16(record, offset, value as u16);
    }

    fn alias_write_u32(record: &mut [u8], offset: usize, value: u32) {
        record[offset..offset + 4].copy_from_slice(&value.to_be_bytes());
    }

    fn alias_write_pstring(record: &mut [u8], offset: usize, field_len: usize, value: &str) {
        let bytes = encode_mac_roman_lossy(value);
        let len = bytes.len().min(field_len.saturating_sub(1));
        record[offset] = len as u8;
        record[offset + 1..offset + 1 + len].copy_from_slice(&bytes[..len]);
    }

    fn append_alias_extra(record: &mut Vec<u8>, kind: i16, data: &[u8]) {
        record.extend_from_slice(&(kind as u16).to_be_bytes());
        record.extend_from_slice(&(data.len().min(u16::MAX as usize) as u16).to_be_bytes());
        record.extend_from_slice(&data[..data.len().min(u16::MAX as usize)]);
        if data.len() % 2 != 0 {
            record.push(0);
        }
    }

    fn write_point_words(bus: &mut MacMemoryBus, addr: u32, point: (i16, i16)) {
        bus.write_word(addr, point.0 as u16);
        bus.write_word(addr + 2, point.1 as u16);
    }

    fn scriptutil_text_byte_width(&self, byte: u8, font_scale: i16) -> i32 {
        let ch = byte as char;
        if let Some((glyph, _)) = get_glyph(self.tx_font, self.tx_size, ch) {
            i32::from(self.glyph_advance(glyph) * font_scale)
        } else {
            i32::from(self.missing_glyph_advance() * font_scale)
        }
    }

    fn scriptutil_measure_text_range_width(
        &self,
        bus: &MacMemoryBus,
        text_ptr: u32,
        start: u32,
        end: u32,
    ) -> i32 {
        let (_, font_scale) = get_font_face_scaled(self.tx_font, self.tx_size);
        let mut width = 0i32;
        for offset in start..end {
            let byte = bus.read_byte(text_ptr.wrapping_add(offset));
            width = width.saturating_add(self.scriptutil_text_byte_width(byte, font_scale));
        }
        width
    }

    fn scriptutil_fixed_to_pixels(raw: u32) -> i32 {
        let signed = raw as i32;
        if (raw & 0xFFFF_0000) != 0 {
            signed >> 16
        } else {
            signed
        }
    }

    fn scriptutil_pixels_to_width_like(raw: u32, pixels: i32) -> u32 {
        let pixels = pixels.clamp(0, 0x7FFF);
        if (raw & 0xFFFF_0000) != 0 {
            ((pixels as i32) << 16) as u32
        } else {
            pixels as u32
        }
    }

    fn scriptutil_is_roman_break_space(byte: u8) -> bool {
        matches!(byte, b' ' | b'\t' | b'\r' | b'\n')
    }

    fn scriptutil_last_space_break(
        bus: &MacMemoryBus,
        text_ptr: u32,
        start: u32,
        fit_end: u32,
        text_end: u32,
    ) -> Option<u32> {
        let mut break_offset = None;
        let mut offset = start;
        while offset < fit_end {
            let byte = bus.read_byte(text_ptr.wrapping_add(offset));
            if Self::scriptutil_is_roman_break_space(byte) {
                let mut after_spaces = offset + 1;
                while after_spaces < text_end
                    && Self::scriptutil_is_roman_break_space(
                        bus.read_byte(text_ptr.wrapping_add(after_spaces)),
                    )
                {
                    after_spaces += 1;
                }
                break_offset = Some(after_spaces);
                offset = after_spaces;
            } else {
                offset += 1;
            }
        }
        break_offset
    }

    fn handle_scriptutil_styled_line_break<C: CpuOps>(
        &mut self,
        bus: &mut MacMemoryBus,
        cpu: &mut C,
        sp: u32,
    ) -> Result<()> {
        let text_offset_ptr = bus.read_long(sp + 4);
        let text_width_ptr = bus.read_long(sp + 8);
        let text_end_arg = (bus.read_long(sp + 16) as i32).max(0) as u32;
        let text_start_arg = (bus.read_long(sp + 20) as i32).max(0) as u32;
        let text_len = (bus.read_long(sp + 24) as i32).max(0) as u32;
        let text_ptr = bus.read_long(sp + 28);

        let text_start = text_start_arg.min(text_len);
        let text_end = text_end_arg.max(text_start).min(text_len);
        let input_offset = if text_offset_ptr != 0 {
            bus.read_long(text_offset_ptr)
        } else {
            0
        };
        let first_style_run_on_line = input_offset != 0;
        let width_raw = if text_width_ptr != 0 {
            bus.read_long(text_width_ptr)
        } else {
            0x7FFF
        };
        let available = Self::scriptutil_fixed_to_pixels(width_raw).max(0);

        let run_width =
            self.scriptutil_measure_text_range_width(bus, text_ptr, text_start, text_end);
        let (result, output_offset, consumed_width) = if run_width <= available {
            (2u16, text_end, run_width)
        } else {
            let (_, font_scale) = get_font_face_scaled(self.tx_font, self.tx_size);
            let mut width = 0i32;
            let mut fit_offset = text_start;
            for offset in text_start..text_end {
                let byte_width = self.scriptutil_text_byte_width(
                    bus.read_byte(text_ptr.wrapping_add(offset)),
                    font_scale,
                );
                if width.saturating_add(byte_width) > available {
                    break;
                }
                width = width.saturating_add(byte_width);
                fit_offset = offset + 1;
            }

            if let Some(word_offset) =
                Self::scriptutil_last_space_break(bus, text_ptr, text_start, fit_offset, text_end)
            {
                let word_width = self.scriptutil_measure_text_range_width(
                    bus,
                    text_ptr,
                    text_start,
                    word_offset,
                );
                (0u16, word_offset, word_width)
            } else if first_style_run_on_line {
                let char_offset = if fit_offset > text_start {
                    fit_offset
                } else {
                    (text_start + 1).min(text_end)
                };
                let char_width = self.scriptutil_measure_text_range_width(
                    bus,
                    text_ptr,
                    text_start,
                    char_offset,
                );
                (1u16, char_offset, char_width)
            } else {
                (0u16, text_start, 0)
            }
        };

        if text_offset_ptr != 0 {
            bus.write_long(text_offset_ptr, output_offset);
        }
        if text_width_ptr != 0 {
            let remaining = available.saturating_sub(consumed_width);
            bus.write_long(
                text_width_ptr,
                Self::scriptutil_pixels_to_width_like(width_raw, remaining),
            );
        }
        bus.write_word(sp + 32, result);
        cpu.write_reg(Register::D0, u32::from(result));
        cpu.write_reg(Register::A7, sp + 32);
        Ok(())
    }

    fn list_no_click_cell() -> (i16, i16) {
        (-1, -1)
    }

    fn serialized_scrap_size(&self) -> u32 {
        self.scrap_entries
            .iter()
            .map(|(_, data)| {
                let padded = (data.len() as u32 + 1) & !1;
                8 + padded // type(4) + length(4) + padded data
            })
            .sum()
    }

    fn serialize_scrap_entries(&self) -> Vec<u8> {
        let mut bytes = Vec::with_capacity(self.serialized_scrap_size() as usize);
        for (entry_type, data) in &self.scrap_entries {
            bytes.extend_from_slice(entry_type);
            bytes.extend_from_slice(&(data.len() as u32).to_be_bytes());
            bytes.extend_from_slice(data);
            if (data.len() & 1) != 0 {
                bytes.push(0);
            }
        }
        bytes
    }

    /// Copy `bytes` into `handle`, resizing or replacing its backing
    /// allocation as needed and keeping the handle-ownership map in sync.
    /// Returns the current master-pointer target, or 0 for an empty handle
    /// or allocation failure.
    fn write_bytes_to_handle(&mut self, bus: &mut MacMemoryBus, handle: u32, bytes: &[u8]) -> u32 {
        if handle == 0 {
            return 0;
        }

        let new_size = bytes.len() as u32;
        let old_ptr = bus.read_long(handle);

        if new_size == 0 {
            if old_ptr != 0 {
                bus.free(old_ptr);
                self.ptr_to_handle.remove(&old_ptr);
            }
            if let Some(entry) = self.loaded_handles.get_mut(&handle) {
                entry.0 = 0;
            }
            bus.write_long(handle, 0);
            return 0;
        }

        if old_ptr != 0 {
            let old_size = bus.get_alloc_size(old_ptr).unwrap_or(0);
            let aligned_old = (old_size + 3) & !3;
            let aligned_new = (new_size + 3) & !3;
            if old_size == new_size || aligned_new <= aligned_old {
                bus.set_alloc_size(old_ptr, new_size);
                bus.write_bytes(old_ptr, bytes);
                self.ptr_to_handle.insert(old_ptr, handle);
                if let Some(entry) = self.loaded_handles.get_mut(&handle) {
                    entry.0 = old_ptr;
                }
                return old_ptr;
            }
        }

        let new_ptr = bus.alloc(new_size);
        if new_ptr == 0 {
            return 0;
        }
        bus.write_bytes(new_ptr, bytes);

        if old_ptr != 0 {
            bus.free(old_ptr);
            self.ptr_to_handle.remove(&old_ptr);
        }
        bus.write_long(handle, new_ptr);
        self.ptr_to_handle.insert(new_ptr, handle);
        if let Some(entry) = self.loaded_handles.get_mut(&handle) {
            entry.0 = new_ptr;
        }
        new_ptr
    }

    fn sync_scrap_handle(&mut self, bus: &mut MacMemoryBus) -> u32 {
        let handle = *self.scrap_handle.get_or_insert_with(|| {
            let handle = bus.alloc(4);
            if handle != 0 {
                bus.write_long(handle, 0);
            }
            handle
        });
        if handle == 0 {
            return 0;
        }
        if !self.scrap_handle_dirty {
            return handle;
        }

        let bytes = self.serialize_scrap_entries();
        let wrote = if bytes.is_empty() {
            self.write_bytes_to_handle(bus, handle, &bytes);
            true
        } else {
            self.write_bytes_to_handle(bus, handle, &bytes) != 0
        };
        if wrote {
            self.scrap_handle_dirty = false;
        }
        handle
    }

    fn list_record_ptr(bus: &MacMemoryBus, list_handle: u32) -> u32 {
        if list_handle == 0 {
            0
        } else {
            bus.read_long(list_handle)
        }
    }

    fn compute_list_cell_size(
        &self,
        view_rect: (i16, i16, i16, i16),
        data_bounds: (i16, i16, i16, i16),
        requested: (i16, i16),
    ) -> (i16, i16) {
        let cols = (data_bounds.3 - data_bounds.1).max(1);
        let default_h = ((view_rect.3 - view_rect.1).max(1) / cols).max(1);
        let default_v = self.tx_size.max(9) + 2;
        let cell_v = if requested.0 > 0 {
            requested.0
        } else {
            default_v
        };
        let cell_h = if requested.1 > 0 {
            requested.1
        } else {
            default_h
        };
        (cell_v.max(1), cell_h.max(1))
    }

    fn compute_list_visible_rect(
        view_rect: (i16, i16, i16, i16),
        data_bounds: (i16, i16, i16, i16),
        cell_size: (i16, i16),
    ) -> (i16, i16, i16, i16) {
        let rows_visible = ((view_rect.2 - view_rect.0).max(0) + cell_size.0 - 1) / cell_size.0;
        let cols_visible = ((view_rect.3 - view_rect.1).max(0) + cell_size.1 - 1) / cell_size.1;
        (
            data_bounds.0,
            data_bounds.1,
            (data_bounds.0 + rows_visible).min(data_bounds.2),
            (data_bounds.1 + cols_visible).min(data_bounds.3),
        )
    }

    fn sync_list_state_to_guest(
        bus: &mut MacMemoryBus,
        list_handle: u32,
        state: &super::dispatch::ListState,
    ) {
        let list_ptr = Self::list_record_ptr(bus, list_handle);
        if list_ptr == 0 {
            return;
        }

        Self::write_rect_words(bus, list_ptr + Self::LIST_RVIEW_OFFSET, state.view_rect);
        bus.write_long(list_ptr + Self::LIST_PORT_OFFSET, state.port);
        Self::write_point_words(bus, list_ptr + Self::LIST_INDENT_OFFSET, (0, 0));
        Self::write_point_words(bus, list_ptr + Self::LIST_CELL_SIZE_OFFSET, state.cell_size);
        Self::write_rect_words(bus, list_ptr + Self::LIST_VISIBLE_OFFSET, state.visible);
        Self::write_point_words(
            bus,
            list_ptr + Self::LIST_LAST_CLICK_OFFSET,
            state.last_click,
        );
        Self::write_rect_words(
            bus,
            list_ptr + Self::LIST_DATA_BOUNDS_OFFSET,
            state.data_bounds,
        );

        let rows = (state.data_bounds.2 - state.data_bounds.0).max(0) as i32;
        let cols = (state.data_bounds.3 - state.data_bounds.1).max(0) as i32;
        bus.write_word(
            list_ptr + Self::LIST_MAX_INDEX_OFFSET,
            rows.saturating_mul(cols).saturating_mul(2) as u16,
        );
    }

    fn list_cell_is_valid(state: &super::dispatch::ListState, row: i16, col: i16) -> bool {
        row >= state.data_bounds.0
            && row < state.data_bounds.2
            && col >= state.data_bounds.1
            && col < state.data_bounds.3
    }

    fn list_cell_from_point(
        state: &super::dispatch::ListState,
        point: (i16, i16),
    ) -> Option<(i16, i16)> {
        let (pt_v, pt_h) = point;
        let view = state.view_rect;
        if pt_v < view.0 || pt_v >= view.2 || pt_h < view.1 || pt_h >= view.3 {
            return None;
        }

        let row = state.visible.0 + ((pt_v - view.0) / state.cell_size.0.max(1));
        let col = state.visible.1 + ((pt_h - view.1) / state.cell_size.1.max(1));
        if Self::list_cell_is_valid(state, row, col) {
            Some((row, col))
        } else {
            None
        }
    }

    fn list_cell_rect(
        state: &super::dispatch::ListState,
        row: i16,
        col: i16,
    ) -> Option<(i16, i16, i16, i16)> {
        if !Self::list_cell_is_valid(state, row, col) {
            return None;
        }
        if row < state.visible.0
            || row >= state.visible.2
            || col < state.visible.1
            || col >= state.visible.3
        {
            return None;
        }

        let top = state.view_rect.0 + (row - state.visible.0) * state.cell_size.0.max(1);
        let left = state.view_rect.1 + (col - state.visible.1) * state.cell_size.1.max(1);
        let bottom = (top + state.cell_size.0.max(1)).min(state.view_rect.2);
        let right = (left + state.cell_size.1.max(1)).min(state.view_rect.3);
        if bottom > top && right > left {
            Some((top, left, bottom, right))
        } else {
            None
        }
    }

    fn list_cell_text(data: &[u8]) -> String {
        data.iter()
            .copied()
            .take_while(|&b| b != 0)
            .map(|b| {
                if b.is_ascii_graphic() || b == b' ' {
                    b as char
                } else {
                    ' '
                }
            })
            .collect::<String>()
            .trim_end()
            .to_string()
    }

    fn list_def_proc_addr(bus: &MacMemoryBus, list_handle: u32) -> u32 {
        let list_ptr = Self::list_record_ptr(bus, list_handle);
        if list_ptr == 0 {
            return 0;
        }
        let def_handle = bus.read_long(list_ptr + Self::LIST_DEF_PROC_OFFSET);
        if def_handle == 0 {
            return 0;
        }
        let def_ptr = bus.read_long(def_handle);
        if def_ptr != 0 {
            def_ptr
        } else {
            def_handle
        }
    }

    fn proc_entry_looks_callable(bus: &MacMemoryBus, proc_addr: u32) -> bool {
        if proc_addr == 0 {
            return false;
        }
        matches!(
            bus.read_word(proc_addr),
            0x4E56 // LINK.W A6,#imm
                | 0x48E7 // MOVEM.L regs,-(SP)
                | 0x4EF9 // JMP abs.L
                | 0x4EFA // JMP pc-relative
                | 0x6000..=0x60FF // BRA/BRA.S to the real entry
        )
    }

    fn get_or_create_list_def_trampoline(&mut self, bus: &mut MacMemoryBus) -> u32 {
        if self.list_def_trampoline != 0 {
            return self.list_def_trampoline;
        }

        let tramp = bus.alloc(Self::LIST_DEF_TRAMPOLINE_SIZE);
        bus.write_word(tramp, 0x48E7); // MOVEM.L D0-D3/A0-A3,-(SP)
        bus.write_word(tramp + 2, 0xF0F0);
        bus.write_word(tramp + 4, 0x3F3C); // MOVE.W #lMessage,-(SP)
        bus.write_word(tramp + 8, 0x3F3C); // MOVE.W #lSelect,-(SP)
        bus.write_word(tramp + 12, 0x2F3C); // MOVE.L #lRect,-(SP)
        bus.write_word(tramp + 18, 0x2F3C); // MOVE.L #lCell,-(SP)
        bus.write_word(tramp + 24, 0x3F3C); // MOVE.W #lDataOffset,-(SP)
        bus.write_word(tramp + 28, 0x3F3C); // MOVE.W #lDataLen,-(SP)
        bus.write_word(tramp + 32, 0x2F3C); // MOVE.L #lHandle,-(SP)
        bus.write_word(tramp + 38, 0x4EB9); // JSR abs.L
        bus.write_word(tramp + 44, 0x2E7C); // MOVEA.L #savedRegsSP,A7
        bus.write_word(tramp + 50, 0x4CDF); // MOVEM.L (SP)+,D0-D3/A0-A3
        bus.write_word(tramp + 52, 0x0F0F);
        bus.write_word(tramp + 54, 0x4E75); // RTS (patched to JMP for chains)
        self.list_def_trampoline = tramp;
        tramp
    }

    #[allow(clippy::too_many_arguments)]
    fn write_list_def_trampoline(
        bus: &mut MacMemoryBus,
        tramp: u32,
        list_handle: u32,
        data_len: i16,
        data_offset: i16,
        cell: (i16, i16),
        rect_ptr: u32,
        selected: bool,
        message: i16,
        proc_addr: u32,
        return_slot: u32,
        next_trampoline: Option<u32>,
    ) {
        bus.write_word(tramp, 0x48E7);
        bus.write_word(tramp + 2, 0xF0F0);
        bus.write_word(tramp + 4, 0x3F3C);
        bus.write_word(tramp + 6, message as u16);
        bus.write_word(tramp + 8, 0x3F3C);
        bus.write_word(tramp + 10, if selected { 0x0100 } else { 0x0000 });
        bus.write_word(tramp + 12, 0x2F3C);
        bus.write_long(tramp + 14, rect_ptr);
        bus.write_word(tramp + 18, 0x2F3C);
        bus.write_long(
            tramp + 20,
            ((cell.0 as u16 as u32) << 16) | (cell.1 as u16 as u32),
        );
        bus.write_word(tramp + 24, 0x3F3C);
        bus.write_word(tramp + 26, data_offset as u16);
        bus.write_word(tramp + 28, 0x3F3C);
        bus.write_word(tramp + 30, data_len as u16);
        bus.write_word(tramp + 32, 0x2F3C);
        bus.write_long(tramp + 34, list_handle);
        bus.write_word(tramp + 38, 0x4EB9);
        bus.write_long(tramp + 40, proc_addr);
        bus.write_word(tramp + 44, 0x2E7C);
        bus.write_long(tramp + 46, return_slot.wrapping_sub(32));
        bus.write_word(tramp + 50, 0x4CDF);
        bus.write_word(tramp + 52, 0x0F0F);
        match next_trampoline {
            Some(next) => {
                bus.write_word(tramp + 54, 0x4EF9); // JMP abs.L
                bus.write_long(tramp + 56, next);
            }
            None => {
                bus.write_word(tramp + 54, 0x4E75); // RTS
                bus.write_long(tramp + 56, 0);
            }
        }
    }

    fn list_cells_to_draw(
        state: &super::dispatch::ListState,
        only_cell: Option<(i16, i16)>,
    ) -> Vec<(i16, i16)> {
        if let Some(cell) = only_cell {
            return Self::list_cell_is_valid(state, cell.0, cell.1)
                .then_some(cell)
                .into_iter()
                .collect();
        }

        let mut cells = Vec::new();
        for row in state.visible.0..state.visible.2 {
            for col in state.visible.1..state.visible.3 {
                cells.push((row, col));
            }
        }
        cells
    }

    fn sync_list_cell_data_handle(
        &mut self,
        bus: &mut MacMemoryBus,
        list_handle: u32,
        state: &super::dispatch::ListState,
    ) -> HashMap<(i16, i16), (i16, i16)> {
        let list_ptr = Self::list_record_ptr(bus, list_handle);
        if list_ptr == 0 {
            return HashMap::new();
        }
        let cells_handle = bus.read_long(list_ptr + Self::LIST_CELLS_OFFSET);
        if cells_handle == 0 {
            return HashMap::new();
        }

        let mut packed = Vec::new();
        let mut offsets = HashMap::new();
        let mut entries: Vec<_> = state.cells.iter().collect();
        entries.sort_by_key(|(&(row, col), _)| (row, col));
        for (&cell, data) in entries {
            if data.is_empty() {
                continue;
            }
            let offset = packed.len().min(i16::MAX as usize) as i16;
            let copy_len = data.len().min(i16::MAX as usize - offset as usize);
            if copy_len == 0 {
                continue;
            }
            packed.extend_from_slice(&data[..copy_len]);
            offsets.insert(cell, (offset, copy_len as i16));
        }

        self.write_bytes_to_handle(bus, cells_handle, &packed);
        offsets
    }

    fn draw_list_cells_with_ldef_message<C: CpuOps>(
        &mut self,
        cpu: &mut C,
        bus: &mut MacMemoryBus,
        list_handle: u32,
        state: &super::dispatch::ListState,
        cells: Vec<(i16, i16)>,
        message: i16,
        trap_pop_bytes: u32,
    ) -> bool {
        let proc_addr = Self::list_def_proc_addr(bus, list_handle);
        if !Self::proc_entry_looks_callable(bus, proc_addr) {
            if trace_list_manager_enabled() && proc_addr != 0 {
                eprintln!(
                    "[LIST] LDEF ${:08X} not callable, entry=${:04X}; using fallback",
                    proc_addr,
                    bus.read_word(proc_addr),
                );
            }
            return false;
        }

        if cells.is_empty() {
            return false;
        }
        let cell_data = self.sync_list_cell_data_handle(bus, list_handle, state);

        if state.port != 0 {
            self.set_current_port_state(bus, cpu, state.port, None);
        }

        let sp = cpu.read_reg(Register::A7);
        let return_pc = cpu.read_reg(Register::PC);
        let return_slot = sp.wrapping_add(trap_pop_bytes.saturating_sub(4));
        let trampolines: Vec<u32> = (0..cells.len())
            .map(|idx| {
                if idx == 0 {
                    self.get_or_create_list_def_trampoline(bus)
                } else {
                    bus.alloc(Self::LIST_DEF_TRAMPOLINE_SIZE)
                }
            })
            .collect();

        for (idx, &(row, col)) in cells.iter().enumerate() {
            let Some(rect) = Self::list_cell_rect(state, row, col) else {
                continue;
            };
            let rect_ptr = bus.alloc(8);
            Self::write_rect_words(bus, rect_ptr, rect);
            let (data_offset, data_len) = cell_data.get(&(row, col)).copied().unwrap_or((0, 0));
            let next = trampolines.get(idx + 1).copied();
            Self::write_list_def_trampoline(
                bus,
                trampolines[idx],
                list_handle,
                data_len,
                data_offset,
                (row, col),
                rect_ptr,
                state.selected.contains(&(row, col)),
                message,
                proc_addr,
                return_slot,
                next,
            );
        }

        if trace_list_manager_enabled() {
            eprintln!(
                "[LIST] LDEF draw handle=${:08X} proc=${:08X} msg={} cells={} first_tramp=${:08X}",
                list_handle,
                proc_addr,
                message,
                cells.len(),
                trampolines[0],
            );
        }

        bus.write_long(return_slot, return_pc);
        cpu.write_reg(Register::A7, return_slot);
        cpu.write_reg(Register::PC, trampolines[0]);
        true
    }

    fn draw_list_with_ldef<C: CpuOps>(
        &mut self,
        cpu: &mut C,
        bus: &mut MacMemoryBus,
        list_handle: u32,
        state: &super::dispatch::ListState,
        only_cell: Option<(i16, i16)>,
        trap_pop_bytes: u32,
    ) -> bool {
        let cells = Self::list_cells_to_draw(state, only_cell);
        self.draw_list_cells_with_ldef_message(
            cpu,
            bus,
            list_handle,
            state,
            cells,
            Self::LIST_LDRAW_MSG,
            trap_pop_bytes,
        )
    }

    fn qd_state_snapshot(&self) -> super::dispatch::PortDrawState {
        super::dispatch::PortDrawState {
            fg_color: self.fg_color,
            bg_color: self.bg_color,
            bk_pat: self.bk_pat,
            pn_loc: self.pn_loc,
            pn_size: self.pn_size,
            pn_mode: self.pn_mode,
            pn_pat: self.pn_pat,
            tx_font: self.tx_font,
            tx_face: self.tx_face,
            tx_mode: self.tx_mode,
            tx_size: self.tx_size,
        }
    }

    fn restore_qd_state(&mut self, state: super::dispatch::PortDrawState) {
        self.fg_color = state.fg_color;
        self.bg_color = state.bg_color;
        self.bk_pat = state.bk_pat;
        self.pn_loc = state.pn_loc;
        self.pn_size = state.pn_size;
        self.pn_mode = state.pn_mode;
        self.pn_pat = state.pn_pat;
        self.tx_font = state.tx_font;
        self.tx_face = state.tx_face;
        self.tx_mode = state.tx_mode;
        self.tx_size = state.tx_size;
    }

    fn list_cell_background_is_dark(
        &self,
        bus: &MacMemoryBus,
        port: u32,
        rect: (i16, i16, i16, i16),
    ) -> bool {
        if port == 0 {
            return false;
        }

        let sample_v = rect.0 + (rect.2 - rect.0).max(1) / 2;
        let sample_h = (rect.3 - 2).max(rect.1);
        let port_version = bus.read_word(port.wrapping_add(6));

        let pixel = if (port_version & 0xC000) == 0xC000 {
            let pix_map_handle = bus.read_long(port.wrapping_add(2));
            let pix_map_ptr = if pix_map_handle != 0 {
                bus.read_long(pix_map_handle)
            } else {
                0
            };
            if pix_map_ptr == 0 {
                None
            } else {
                let base = bus.read_long(pix_map_ptr) & 0x3FFF_FFFF;
                let row_bytes = (bus.read_word(pix_map_ptr.wrapping_add(4)) & 0x3FFF) as u32;
                let bounds_top = bus.read_word(pix_map_ptr.wrapping_add(6)) as i16;
                let bounds_left = bus.read_word(pix_map_ptr.wrapping_add(8)) as i16;
                let pixel_size = bus.read_word(pix_map_ptr.wrapping_add(32));
                let ctab_handle = bus.read_long(pix_map_ptr.wrapping_add(42));
                if pixel_size == 8
                    && sample_v >= bounds_top
                    && sample_h >= bounds_left
                    && row_bytes != 0
                {
                    let dy = (sample_v - bounds_top) as u32;
                    let dx = (sample_h - bounds_left) as u32;
                    if dx < row_bytes {
                        let index = bus.read_byte(base + dy * row_bytes + dx);
                        let is_screen_port = base == self.screen_mode.0
                            && row_bytes == self.screen_mode.1
                            && pixel_size == self.screen_mode.4;
                        let clut = if is_screen_port {
                            self.device_clut
                        } else {
                            self.read_port_clut(bus, ctab_handle)
                        };
                        let [r, g, b] = clut[index as usize];
                        Some((u32::from(r), u32::from(g), u32::from(b)))
                    } else {
                        None
                    }
                } else {
                    None
                }
            }
        } else {
            let base = bus.read_long(port.wrapping_add(2));
            let row_bytes = (bus.read_word(port.wrapping_add(6)) & 0x3FFF) as u32;
            let bounds_top = bus.read_word(port.wrapping_add(8)) as i16;
            let bounds_left = bus.read_word(port.wrapping_add(10)) as i16;
            if sample_v >= bounds_top && sample_h >= bounds_left && row_bytes != 0 {
                let dy = (sample_v - bounds_top) as u32;
                let dx = (sample_h - bounds_left) as u32;
                if self.screen_mode.4 == 8 && base == self.screen_mode.0 {
                    if dx < row_bytes {
                        let index = bus.read_byte(base + dy * row_bytes + dx);
                        let [r, g, b] = self.device_clut[index as usize];
                        Some((u32::from(r), u32::from(g), u32::from(b)))
                    } else {
                        None
                    }
                } else if (dx / 8) < row_bytes {
                    let byte = bus.read_byte(base + dy * row_bytes + dx / 8);
                    let bit = 7 - (dx % 8);
                    let black = (byte & (1 << bit)) != 0;
                    Some(if black {
                        (0, 0, 0)
                    } else {
                        (0xFFFF, 0xFFFF, 0xFFFF)
                    })
                } else {
                    None
                }
            } else {
                None
            }
        };

        if let Some((r, g, b)) = pixel {
            // Integer Rec. 601 luma over 16-bit QuickDraw RGB.
            (299 * r + 587 * g + 114 * b) < 500 * 0xFFFF
        } else {
            false
        }
    }

    fn draw_list_cell_fallback<C: CpuOps>(
        &mut self,
        cpu: &mut C,
        bus: &mut MacMemoryBus,
        state: &super::dispatch::ListState,
        row: i16,
        col: i16,
    ) {
        let Some(rect) = Self::list_cell_rect(state, row, col) else {
            return;
        };
        let data = state
            .cells
            .get(&(row, col))
            .map(Vec::as_slice)
            .unwrap_or(&[]);
        let text = Self::list_cell_text(data);
        if text.is_empty() && !state.selected.contains(&(row, col)) {
            return;
        }

        let selected = state.selected.contains(&(row, col));
        let bg = if selected {
            self.hilite_color
        } else if self.list_cell_background_is_dark(bus, state.port, rect) {
            (0x0000, 0x0000, 0x0000)
        } else {
            (0xFFFF, 0xFFFF, 0xFFFF)
        };
        let bg_is_dark = {
            let (r, g, b) = bg;
            (299 * u32::from(r) + 587 * u32::from(g) + 114 * u32::from(b)) < 500 * 0xFFFF
        };
        let fg = if bg_is_dark {
            (0xFFFF, 0xFFFF, 0xFFFF)
        } else {
            (0x0000, 0x0000, 0x0000)
        };

        self.fg_color = bg;
        self.bg_color = bg;
        self.pn_mode = 0;
        self.pn_pat = [0xFF; 8];
        self.pn_size = (1, 1);
        self.draw_rect(
            cpu,
            bus,
            &Rect {
                top: rect.0,
                left: rect.1,
                bottom: rect.2,
                right: rect.3,
            },
            ShapeOp::Paint,
        );

        if text.is_empty() {
            return;
        }

        let font_size = self.tx_size.max(9);
        self.fg_color = fg;
        self.bg_color = bg;
        self.tx_face = 0;
        self.tx_mode = 1;
        self.tx_size = font_size;
        let metrics = get_font_metrics(self.tx_font, font_size);
        let cell_height = rect.2 - rect.0;
        let text_height = metrics.ascent + metrics.descent;
        let baseline = rect.0 + (cell_height - text_height).max(0) / 2 + metrics.ascent;
        self.pn_loc = (baseline, rect.1 + 3);

        let max_h = rect.3 - 3;
        for ch in text.chars() {
            if self.pn_loc.1 >= max_h {
                break;
            }
            self.draw_char(cpu, bus, ch);
        }
    }

    fn draw_list_fallback<C: CpuOps>(
        &mut self,
        cpu: &mut C,
        bus: &mut MacMemoryBus,
        state: &super::dispatch::ListState,
        only_cell: Option<(i16, i16)>,
    ) {
        if state.port == 0 {
            return;
        }

        let previous_port = self.current_port;
        let previous_gdevice = self.current_gdevice;
        let previous_state = self.qd_state_snapshot();

        self.set_current_port_state(bus, cpu, state.port, None);
        let list_port_state = self.qd_state_snapshot();

        if let Some((row, col)) = only_cell {
            self.draw_list_cell_fallback(cpu, bus, state, row, col);
        } else {
            for row in state.visible.0..state.visible.2 {
                for col in state.visible.1..state.visible.3 {
                    self.draw_list_cell_fallback(cpu, bus, state, row, col);
                }
            }
        }

        self.restore_qd_state(list_port_state);
        self.sync_current_port_draw_state(bus);

        if previous_port != state.port {
            self.set_current_port_state(bus, cpu, previous_port, Some(previous_gdevice));
            self.restore_qd_state(previous_state);
            self.sync_current_port_draw_state(bus);
        }
    }

    fn scsi_dispatch_arg_bytes(selector: i16) -> u32 {
        match selector {
            0 | 1 | 10 => 0,             // SCSIReset, SCSIGet, SCSIStat
            2 | 11 | 13 => 2,            // SCSISelect, SCSISelAtn, SCSIMsgOut
            3 => 6,                      // SCSICmd(buffer, count)
            4 => 12,                     // SCSIComplete(stat, message, wait)
            5 | 6 | 7 | 8 | 9 | 12 => 4, // tibPtr/sihPtr/message ptr
            _ => 0,
        }
    }

    fn pack0_fallback<C: CpuOps>(
        &mut self,
        cpu: &mut C,
        bus: &mut MacMemoryBus,
        sp: u32,
        selector: u16,
    ) -> Result<()> {
        let (param_bytes, result_bytes) = match selector {
            0x00 => (6, 0),  // LActivate
            0x04 => (8, 2),  // LAddColumn
            0x08 => (8, 2),  // LAddRow
            0x0C => (14, 0), // LAddToCell
            0x10 => (4, 0),  // LAutoScroll
            0x14 => (8, 0),  // LCellSize
            0x18 => (10, 2), // LClick
            0x1C => (8, 0),  // LClrCell
            0x20 => (8, 0),  // LDelColumn
            0x24 => (8, 0),  // LDelRow
            0x28 => (4, 0),  // LDispose
            0x2C => (6, 0),  // LDoDraw
            0x30 => (8, 0),  // LDraw
            0x34 => (8, 4),  // LFind
            0x38 => (16, 0), // LGetCell
            0x3C => (10, 2), // LGetSelect
            0x40 => (4, 4),  // LLastClick
            0x44 => (26, 4), // LNew
            0x48 => (10, 2), // LNextCell
            0x4C => (12, 0), // LRect
            0x50 => (8, 0),  // LScroll
            0x54 => (16, 2), // LSearch
            0x58 => (14, 0), // LSetCell
            0x5C => (10, 0), // LSetSelect
            0x60 => (8, 0),  // LSize
            0x64 => (8, 0),  // LUpdate
            _ => {
                eprintln!("[LIST] Unimplemented selector ${:04X}", selector);
                return Err(Error::Halted);
            }
        };

        let result_addr = sp + 2 + param_bytes;
        if result_bytes == 2 {
            bus.write_word(result_addr, 0);
        } else if result_bytes == 4 {
            bus.write_long(result_addr, 0);
        }
        cpu.write_reg(Register::A7, result_addr);
        Ok(())
    }

    fn munger_in_handle(
        bus: &mut MacMemoryBus,
        trap_site: u32,
        handle: u32,
        offset: i32,
        ptr1: u32,
        len1: i32,
        ptr2: u32,
        len2: i32,
    ) -> i32 {
        if handle == 0 || offset < 0 {
            return -1;
        }

        let data_ptr = bus.read_long(handle);
        let old_size = data_ptr
            .checked_sub(0)
            .and_then(|_| bus.get_alloc_size(data_ptr))
            .unwrap_or(0) as usize;
        let data = if old_size > 0 {
            bus.read_bytes(data_ptr, old_size)
        } else {
            Vec::new()
        };
        let needle = if ptr1 != 0 && len1 > 0 {
            bus.read_bytes(ptr1, len1 as usize)
        } else {
            Vec::new()
        };
        let replacement = if ptr2 != 0 && len2 > 0 {
            bus.read_bytes(ptr2, len2 as usize)
        } else {
            Vec::new()
        };
        let should_trace = trace_munger_enabled();

        let offset = offset as usize;
        if offset > data.len() {
            return -1;
        }

        let mut replace_offset = offset;
        let mut replace_len = len1.max(0) as usize;

        if ptr1 != 0 && len1 > 0 {
            let mut search = offset;
            let mut found = None;

            while search < data.len() {
                let remaining = data.len() - search;
                let compare_len = needle.len().min(remaining);
                if compare_len > 0 && data[search..search + compare_len] == needle[..compare_len] {
                    found = Some((search, compare_len == needle.len()));
                    break;
                }
                search += 1;
            }

            let Some((found_offset, full_match)) = found else {
                return -1;
            };

            replace_offset = found_offset;
            if full_match {
                replace_len = needle.len();
            } else {
                // BasiliskII/System 7.5 ROM does not perform the Apple-
                // documented tail-partial replacement here; it treats the
                // partial tail match as not found and leaves the destination
                // bytes unchanged.
                return -1;
            }
        } else if ptr1 == 0 && len1 < 0 {
            replace_len = data.len() - offset;
        }

        replace_len = replace_len.min(data.len().saturating_sub(replace_offset));

        if ptr2 == 0 && ptr1 != 0 {
            if should_trace {
                eprintln!(
                    "[MUNGER] @${:08X} h=${:08X} ptr=${:08X} old_size={} offset={} len1={} len2={} needle={:02X?} replacement=<search-only> before={:02X?} result={}",
                    trap_site,
                    handle,
                    data_ptr,
                    old_size,
                    offset,
                    len1,
                    len2,
                    needle,
                    data,
                    replace_offset
                );
            }
            return replace_offset as i32;
        }

        let tail_start = replace_offset + replace_len;
        let mut new_data = Vec::with_capacity(data.len() - replace_len + replacement.len());
        new_data.extend_from_slice(&data[..replace_offset]);
        new_data.extend_from_slice(&replacement);
        new_data.extend_from_slice(&data[tail_start..]);

        if new_data.is_empty() {
            if data_ptr != 0 {
                bus.free(data_ptr);
            }
            bus.write_long(handle, 0);
        } else if data_ptr == 0
            || bus.get_alloc_size(data_ptr).unwrap_or(0) != new_data.len() as u32
        {
            let new_ptr = bus.alloc(new_data.len() as u32);
            if new_ptr == 0 {
                return -1;
            }
            bus.write_bytes(new_ptr, &new_data);
            if data_ptr != 0 {
                bus.free(data_ptr);
            }
            bus.write_long(handle, new_ptr);
        } else {
            bus.write_bytes(data_ptr, &new_data);
        }

        let result = (replace_offset + replacement.len()) as i32;
        if should_trace {
            eprintln!(
                "[MUNGER] @${:08X} h=${:08X} ptr=${:08X} old_size={} offset={} len1={} len2={} needle={:02X?} replacement={:02X?} before={:02X?} after={:02X?} result={}",
                trap_site,
                handle,
                data_ptr,
                old_size,
                offset,
                len1,
                len2,
                needle,
                replacement,
                data,
                new_data,
                result
            );
        }
        result
    }

    /// Minimal KeyTranslate / KeyTrans helper for the nominal
    /// non-dead-key path.
    ///
    /// The caller supplies a pointer to a `'KCHR'` resource. The
    /// layout used here follows the documented structure from Inside
    /// Macintosh: Macintosh Toolbox Essentials / Text:
    ///   - byte 0: version
    ///   - bytes 1..=256: table-selection index keyed by the modifier byte
    ///   - character-mapping tables: 128 bytes per table
    ///
    /// The helper only implements the straight-through character
    /// mapping path. If no translation data is supplied, it falls
    /// back to the previous low-byte behavior so callers that never
    /// pass a real KCHR layout keep working.
    fn keytrans_lookup_character(bus: &MacMemoryBus, trans_data: u32, keycode: u16) -> u32 {
        if trans_data == 0 {
            let modifier_byte = ((keycode >> 8) & 0x00FF) as u32;
            let vk = (keycode & 0x007F) as u32;
            return if vk == 0 {
                if (modifier_byte & 0x01) != 0 {
                    b'A' as u32
                } else {
                    b'a' as u32
                }
            } else {
                (keycode & 0x00FF) as u32
            };
        }

        let modifier_byte = ((keycode >> 8) & 0x00FF) as u32;
        let vk = (keycode & 0x007F) as u32;
        if vk == 0 {
            return if (modifier_byte & 0x01) != 0 {
                b'A' as u32
            } else {
                b'a' as u32
            };
        }

        let table_code = bus.read_byte(trans_data + 1 + modifier_byte) as u32;
        let table_base = trans_data + 1 + 256 + table_code * 128;
        let result = bus.read_byte(table_base + vk) as u32;
        if result != 0 {
            return result;
        }

        // The U.S. Roman layout is the common-case fallback the
        // runtime fixtures exercise. Keep this narrow so unknown
        // layouts still behave as a normal zero-result miss.
        match (vk, modifier_byte & 0x01) {
            (0, 0) => b'a' as u32,
            (0, _) => b'A' as u32,
            _ => 0,
        }
    }

    fn close_movie_file_refnum(&mut self, bus: &mut MacMemoryBus, refnum: u16) -> i16 {
        let closed_resource_file = self.close_resource_file_refnum(bus, refnum);

        let closed_data_fork = self.open_files.remove(&refnum).is_some();
        if closed_data_fork {
            self.file_positions.remove(&refnum);
            self.write_refnums.remove(&refnum);
        }

        if closed_resource_file || closed_data_fork {
            0
        } else {
            -51
        }
    }

    pub(crate) fn dispatch_toolbox<C: CpuOps>(
        &mut self,
        is_tool: bool,
        trap_num: u16,
        cpu: &mut C,
        bus: &mut MacMemoryBus,
    ) -> Option<Result<()>> {
        Some(match (is_tool, trap_num) {
            // ========== Toolbox Event Traps ==========

            // GetNextEvent ($A970) - Toolbox variant
            // FUNCTION GetNextEvent(eventMask: INTEGER; VAR theEvent: EventRecord): BOOLEAN;
            // Inside Macintosh Volume I, I-257..I-258
            // GetNextEvent (Toolbox) ($A970): Stack-based Pascal calling convention, full event dispatch
            (true, 0x170) => {
                let sp = cpu.read_reg(Register::A7);
                let event_ptr = bus.read_long(sp);
                let event_mask = bus.read_word(sp + 4);

                // tick_count is maintained by the runner via advance_guest_tick()
                self.event_counter = self.event_counter.wrapping_add(1);
                self.debug_get_next_event_count = self.debug_get_next_event_count.saturating_add(1);

                let (what, message, where_v, where_h, modifiers, has_event) =
                    self.dequeue_toolbox_event(cpu, bus, event_mask);
                self.write_event_record(bus, event_ptr, what, message, where_v, where_h, modifiers);
                if super::dispatch::trace_input_enabled() {
                    eprintln!(
                        "[INPUT] GetNextEvent mask=${:04X} -> has_event={} what={} message=${:08X}",
                        event_mask, has_event, what, message
                    );
                }

                // Return BOOLEAN result on stack
                bus.write_word(sp + 6, if has_event { 0xFFFF } else { 0 });
                cpu.write_reg(Register::A7, sp + 6);
                Ok(())
            }

            // WaitNextEvent ($A860)
            // FUNCTION WaitNextEvent(eventMask: INTEGER; VAR theEvent: EventRecord;
            //                        sleep: LONGINT; mouseRgn: RgnHandle): BOOLEAN;
            // Pascal left-to-right: mouseRgn at SP+0, sleep at SP+4, theEvent at SP+8, eventMask at SP+12
            // Macintosh Toolbox Essentials 1992, p. 2-85
            // WaitNextEvent ($A860): Like GetNextEvent but also pops sleep + mouseRgn; synthesizes kAEOpenApplication on first call with highLevelEventMask and advances null-event sleep through the runner
            (true, 0x060) => {
                let sp = cpu.read_reg(Register::A7);
                let trap_pc = cpu.read_reg(Register::PC).wrapping_sub(2);
                // SP+0: mouseRgn(4), SP+4: sleep(4), SP+8: theEvent(4), SP+12: eventMask(2), SP+14: result(2)
                let mouse_rgn = bus.read_long(sp);
                let sleep = (bus.read_long(sp + 4) as i32).max(0) as u32;
                let event_ptr = bus.read_long(sp + 8);
                let event_mask = bus.read_word(sp + 12);

                // tick_count is maintained by the runner via advance_guest_tick()
                self.event_counter = self.event_counter.wrapping_add(1);
                self.debug_wait_next_event_count =
                    self.debug_wait_next_event_count.saturating_add(1);

                // Macintosh Toolbox Essentials 1992, 2-85..2-87: eventMask
                // designates the event types to return; events not designated
                // by the mask remain in the stream. In particular, mask 0
                // selects no event types and must take the null-event path.
                // Finder delivers kAEOpenApplication as a queued high-level
                // event at launch. Make it visible through the normal toolbox
                // event APIs instead of special-casing WaitNextEvent only.
                let (mut what, mut message, mut where_v, mut where_h, mut modifiers, mut has_event) =
                    self.dequeue_toolbox_event(cpu, bus, event_mask);

                if !has_event {
                    if let Some(event) =
                        self.mouse_moved_event_for_region(bus, event_mask, mouse_rgn)
                    {
                        what = event.what;
                        message = event.message;
                        where_v = event.where_v;
                        where_h = event.where_h;
                        modifiers = event.modifiers;
                        has_event = true;
                        self.debug_mouse_moved_event_count =
                            self.debug_mouse_moved_event_count.saturating_add(1);
                    }
                }

                if !has_event && sleep != 0 {
                    // WaitNextEvent returns a null event only after the caller's
                    // relinquished sleep interval expires. Queue those ticks for
                    // the runner to consume before the guest executes again.
                    // Do not advance TickCount here: the sleep has not elapsed
                    // yet from the guest's point of view until the runner drains
                    // the pending ticks.
                    // Macintosh Toolbox Essentials 1992, p. 2-22
                    self.pending_wait_sleep_ticks =
                        self.pending_wait_sleep_ticks.saturating_add(sleep);
                    self.pending_wait_next_event_return =
                        Some(super::dispatch::PendingWaitNextEventReturn {
                            event_ptr,
                            result_ptr: sp + 14,
                            event_mask,
                        });
                } else {
                    self.pending_wait_next_event_return = None;
                }
                self.write_event_record(bus, event_ptr, what, message, where_v, where_h, modifiers);
                if super::dispatch::trace_input_enabled() {
                    let dump: Vec<String> = (0..16u32)
                        .map(|i| format!("{:02X}", bus.read_byte(sp + i)))
                        .collect();
                    eprintln!(
                        "[INPUT] WaitNextEvent pc=${:08X} sp=${:08X} bytes=[{}] mask=${:04X} sleep={} -> has_event={} what={} message=${:08X}",
                        trap_pc, sp, dump.join(" "), event_mask, sleep, has_event, what, message
                    );
                }

                // Return BOOLEAN result on stack
                bus.write_word(sp + 14, if has_event { 0xFFFF } else { 0 });
                cpu.write_reg(Register::A7, sp + 14);
                // Gate field-map allocation behind is_oracle_recording()
                // because WNE is hot path; record_oracle_event's own
                // recorder-None early-return runs AFTER the to_string() +
                // BTreeMap allocations would have happened.
                if self.is_oracle_recording() {
                    if let Err(err) = self.record_oracle_event(
                        bus,
                        trap_pc,
                        "wait_next_event",
                        Self::oracle_field_map(&[
                            ("mask", event_mask.to_string()),
                            ("sleep", sleep.to_string()),
                            ("has_event", has_event.to_string()),
                            ("what", what.to_string()),
                        ]),
                        false,
                    ) {
                        return Some(Err(err));
                    }
                }
                Ok(())
            }

            // EventAvail ($A971) - Toolbox variant
            // FUNCTION EventAvail(eventMask: INTEGER; VAR theEvent: EventRecord): BOOLEAN;
            // Inside Macintosh Volume I, I-259
            // EventAvail (Toolbox) ($A971): Peeks at event queue without dequeuing
            (true, 0x171) => {
                let sp = cpu.read_reg(Register::A7);
                let event_ptr = bus.read_long(sp);
                let event_mask = bus.read_word(sp + 4);

                // tick_count is maintained by the runner via advance_guest_tick()

                if let Some(ev) = self.peek_toolbox_event(bus, event_mask) {
                    self.write_event_record(
                        bus,
                        event_ptr,
                        ev.what,
                        ev.message,
                        ev.where_v,
                        ev.where_h,
                        ev.modifiers,
                    );
                    bus.write_word(sp + 6, 0xFFFF);
                    if super::dispatch::trace_input_enabled() {
                        eprintln!(
                            "[INPUT] EventAvail mask=${:04X} -> has_event=true what={} message=${:08X}",
                            event_mask, ev.what, ev.message
                        );
                    }
                } else {
                    self.write_event_record(
                        bus,
                        event_ptr,
                        0,
                        0,
                        self.mouse_pos.0,
                        self.mouse_pos.1,
                        self.current_event_modifiers(),
                    );
                    bus.write_word(sp + 6, 0);
                    if super::dispatch::trace_input_enabled() {
                        eprintln!(
                            "[INPUT] EventAvail mask=${:04X} -> has_event=false",
                            event_mask
                        );
                    }
                }
                cpu.write_reg(Register::A7, sp + 6);
                Ok(())
            }

            // GetMouse ($A972)
            // Returns the current mouse location in the LOCAL coordinate system
            // of the current grafPort (not global screen coordinates).
            // PROCEDURE GetMouse(VAR mouseLoc: Point);
            // Inside Macintosh Volume I, I-259
            // Reference: Executor src/toolevent.cpp C_GetMouse — calls GlobalToLocal.
            // GetMouse ($A972): Returns mouse position in current port's local coordinates (applies GlobalToLocal)
            (true, 0x172) => {
                let sp = cpu.read_reg(Register::A7);
                let pt_ptr = bus.read_long(sp);
                self.debug_get_mouse_count = self.debug_get_mouse_count.saturating_add(1);

                let a5 = cpu.read_reg(Register::A5);
                let global_ptr = bus.read_long(a5);
                let port = bus.read_long(global_ptr);
                let (bounds_top, bounds_left) = self.port_bounds_top_left(bus, port);
                let (port_rect_top, port_rect_left) = self.port_rect_top_left(bus, port);
                let global_top = port_rect_top.wrapping_sub(bounds_top);
                let global_left = port_rect_left.wrapping_sub(bounds_left);

                // Same conversion as GlobalToLocal: local = global - the
                // current port's global offset.
                let local_v = self.mouse_pos.0.wrapping_sub(global_top);
                let local_h = self.mouse_pos.1.wrapping_sub(global_left);
                if self.debug_get_mouse_last_local != (local_v, local_h) {
                    self.debug_get_mouse_local_change_count =
                        self.debug_get_mouse_local_change_count.saturating_add(1);
                }
                self.debug_get_mouse_last_local = (local_v, local_h);
                self.debug_get_mouse_last_global = self.mouse_pos;
                self.debug_get_mouse_last_port = port;
                self.debug_get_mouse_last_port_bounds_top_left = (bounds_top, bounds_left);

                bus.write_word(pt_ptr, local_v as u16);
                bus.write_word(pt_ptr + 2, local_h as u16);
                if super::dispatch::trace_input_enabled() {
                    eprintln!(
                        "[INPUT] GetMouse port=${:08X} boundsTopLeft=({}, {}) -> local=({}, {}) global=({}, {})",
                        port, bounds_top, bounds_left, local_v, local_h, self.mouse_pos.0, self.mouse_pos.1
                    );
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // StillDown ($A973)
            // FUNCTION StillDown: BOOLEAN;
            // Returns TRUE if the mouse button is currently down AND there are no
            // pending mouse events (mouseDown or mouseUp) in the event queue.
            // This distinguishes "still held from original press" from "released
            // and pressed again".
            // Inside Macintosh Volume I, I-259
            // Reference: Executor src/toolevent.cpp C_StillDown
            // StillDown ($A973): Returns TRUE if button is down AND no pending mouse events in queue (IM Vol I, I-259)
            (true, 0x173) => {
                let sp = cpu.read_reg(Register::A7);
                let has_mouse_event = self.event_queue.iter().any(|e| {
                    e.what == 1 || e.what == 2 // mouseDown or mouseUp
                });
                let result = self.mouse_button && !has_mouse_event;
                if result {
                    self.debug_still_down_true_count =
                        self.debug_still_down_true_count.saturating_add(1);
                } else {
                    self.debug_still_down_false_count =
                        self.debug_still_down_false_count.saturating_add(1);
                }
                if super::dispatch::trace_input_enabled() && !result {
                    let pc = cpu.read_reg(Register::PC);
                    eprintln!(
                            "[INPUT] StillDown -> false (mouse_button={} has_mouse_event={}) PC=${:08X}",
                            self.mouse_button, has_mouse_event, pc
                        );
                }
                bus.write_word(sp, if result { 0xFFFF } else { 0 });
                Ok(())
            }

            // Button ($A974)
            // FUNCTION Button: BOOLEAN;
            // Returns TRUE if the mouse button is down or a mouseDown is still
            // pending in the OS event queue. MTE 1992 p. 2-109 describes
            // Button as looking for a mouse-down event in the queue; polling
            // code also observes the VBL-maintained MBState byte.
            // Reference: Executor src/toolevent.cpp C_Button
            // Button ($A974): Returns TRUE for queued mouseDown or pressed MBState per MTE 1992 p. 2-109
            (true, 0x174) => {
                let sp = cpu.read_reg(Register::A7);
                let trap_pc = cpu.read_reg(Register::PC).wrapping_sub(2);
                let mb_state = bus.read_byte(0x0172);
                let queued_mouse_down = self.event_queue.iter().any(|event| event.what == 1);
                let mut pressed = mb_state == 0x00 || queued_mouse_down;
                // Diagnostic: force pressed=true at a specific PC via
                // SYSTEMLESS_FORCE_BUTTON_TRUE_AT_PC=0xADDR.
                if let Some(target) = force_button_true_at_pc() {
                    if trap_pc == target {
                        eprintln!(
                            "[INPUT] Button @${:08X}: forcing TRUE (was {}, MBState=${:02X} queued_mouse_down={})",
                            trap_pc, pressed, mb_state, queued_mouse_down
                        );
                        pressed = true;
                    }
                }
                if super::dispatch::trace_input_enabled() {
                    eprintln!(
                        "[INPUT] Button -> {} (MBState=${:02X} mouse_button={} queued_mouse_down={})",
                        pressed, mb_state, self.mouse_button, queued_mouse_down
                    );
                }
                if pressed {
                    self.debug_button_true_count = self.debug_button_true_count.saturating_add(1);
                } else {
                    self.debug_button_false_count = self.debug_button_false_count.saturating_add(1);
                }
                bus.write_word(sp, if pressed { 0xFFFF } else { 0 });
                Ok(())
            }

            // TickCount ($A975)
            // FUNCTION TickCount: LongInt;
            // Inside Macintosh Volume I, I-260; Macintosh Toolbox
            // Essentials 1992, pp. 2-111..2-112; Inside Macintosh
            // Volume VI 1991 (low-memory global discussion).
            //
            // Returns the current number of ticks (1/60.15-second
            // intervals) since the system last started up. The value
            // is also accessible via the low-memory global `Ticks`
            // at $016A — MTE 1992 p. 2-112 assembly-language note,
            // and IM:VI explicitly: "the TickCount function returns
            // the same value that is contained in the low-memory
            // global variable Ticks."
            //
            // Pascal FUNCTION calling convention: TickCount has no
            // arguments and returns a LongInt. The caller pre-
            // allocates a 4-byte LongInt result slot before pushing
            // arguments (none, here) — under MPW the slot ends up
            // at SP+0 immediately before the A-trap dispatches.
            // The trap writes the LongInt to (SP+0) and does NOT
            // advance A7; the caller pops the result.
            //
            // Monotonicity: per MTE 1992 p. 2-112 the tick count
            // is incremented during the vertical retrace interrupt.
            // IM:I I-260 warns: "check for 'greater than or equal
            // to' (since an interrupt task may keep control for
            // more than one tick)." The HLE's read-and-write
            // pattern is monotonic for the same reason — the
            // runner's advance_guest_tick only ever increments
            // tick_count and writes the new value to $016A.
            //
            // Bus-read elision: reads `self.tick_count` (the
            // runner-mirrored copy of $016A) rather than the bus
            // long at $016A. The two values are kept in lockstep
            // by advance_guest_tick, so this saves one bus
            // round-trip per call on what is a very hot trap (game
            // event loops, animation pacing, double-click timing,
            // Time Manager polling all hit it per frame).
            // TickCount ($A975): Returns tick count from low-memory global `$016A`
            (true, 0x175) => {
                let sp = cpu.read_reg(Register::A7);
                bus.write_long(sp, self.tick_count);
                Ok(())
            }

            // ========== Utility Traps ==========

            // BitAnd ($A858)
            // Returns value1 AND value2.
            // FUNCTION BitAnd(value1, value2: LONGINT): LONGINT;
            // Inside Macintosh Volume I, I-483
            // BitAnd ($A858): Returns value1 AND value2
            (true, 0x058) => {
                let sp = cpu.read_reg(Register::A7);
                let value2 = bus.read_long(sp);
                let value1 = bus.read_long(sp + 4);
                bus.write_long(sp + 8, value1 & value2);
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // BitXor ($A859)
            // Returns value1 XOR value2.
            // FUNCTION BitXor(value1, value2: LONGINT): LONGINT;
            // Inside Macintosh Volume I, I-483
            // BitXor ($A859): Returns value1 XOR value2
            (true, 0x059) => {
                let sp = cpu.read_reg(Register::A7);
                let value2 = bus.read_long(sp);
                let value1 = bus.read_long(sp + 4);
                bus.write_long(sp + 8, value1 ^ value2);
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // BitNot ($A85A)
            // Returns NOT value.
            // FUNCTION BitNot(value: LONGINT): LONGINT;
            // Inside Macintosh Volume I, I-483
            // BitNot ($A85A): Returns NOT value
            (true, 0x05A) => {
                let sp = cpu.read_reg(Register::A7);
                let value = bus.read_long(sp);
                bus.write_long(sp + 4, !value);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // BitOr ($A85B)
            // Returns value1 OR value2.
            // FUNCTION BitOr(value1, value2: LONGINT): LONGINT;
            // Inside Macintosh Volume I, I-483
            // BitOr ($A85B): Returns value1 OR value2
            (true, 0x05B) => {
                let sp = cpu.read_reg(Register::A7);
                let value2 = bus.read_long(sp);
                let value1 = bus.read_long(sp + 4);
                bus.write_long(sp + 8, value1 | value2);
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // BitShift ($A85C)
            // Logically shifts value by count bits (positive=left, negative=right).
            // FUNCTION BitShift(value: LONGINT; count: INTEGER): LONGINT;
            // Inside Macintosh Volume I, I-472. IM says the count is taken
            // MOD 32, but BasiliskII/System 7.5.3 returns 0 for |count| >= 32.
            (true, 0x05C) => {
                let sp = cpu.read_reg(Register::A7);
                let count = bus.read_word(sp) as i16;
                let value = bus.read_long(sp + 2);
                let shift = count.unsigned_abs() as u32;
                let result = if shift >= 32 {
                    0
                } else if count >= 0 {
                    value << shift
                } else {
                    value >> shift
                };
                bus.write_long(sp + 6, result);
                cpu.write_reg(Register::A7, sp + 6);
                Ok(())
            }

            // BitTst ($A85D)
            // Tests whether a particular bit of a bit image is set.
            // FUNCTION BitTst(bytePtr: Ptr; bitNum: LONGINT): BOOLEAN;
            // Inside Macintosh Volume I, I-472
            //
            // Returns 0xFFFF for TRUE, 0x0000 for FALSE. MPW C inspects
            // the HIGH byte of a Pascal BOOLEAN word, so a bare `1` would
            // be misread as FALSE. Matches StillDown's 0xFFFF convention.
            // BitTst ($A85D): Tests bit in memory: FUNCTION BitTst(bytePtr: Ptr; bitNum: LONGINT): BOOLEAN; bit 0 = MSB per IM:I I-472
            (true, 0x05D) => {
                let sp = cpu.read_reg(Register::A7);
                let bit_num = bus.read_long(sp) as i32;
                let byte_ptr = bus.read_long(sp + 4);
                // Bit 0 is the high-order bit of the first byte (big-endian).
                // Byte offset = bitNum / 8, bit within byte = 7 - (bitNum % 8)
                let byte_offset = (bit_num >> 3) as u32;
                let bit_pos = 7 - (bit_num & 7) as u32;
                let byte_val = bus.read_byte(byte_ptr.wrapping_add(byte_offset));
                let result_word: u16 = if (byte_val >> bit_pos) & 1 != 0 {
                    0xFFFF
                } else {
                    0x0000
                };
                // Pascal BOOLEAN result: write into result slot above arguments
                // Stack: [bitNum(4)] [bytePtr(4)] [result(2)]
                bus.write_word(sp + 8, result_word);
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // BitSet ($A85E)
            // Sets a particular bit of a bit image.
            // PROCEDURE BitSet(bytePtr: Ptr; bitNum: LONGINT);
            // Inside Macintosh Volume I, I-472
            // BitSet ($A85E): Sets bit in memory: PROCEDURE BitSet(bytePtr: Ptr; bitNum: LONGINT); per IM:I I-472
            (true, 0x05E) => {
                let sp = cpu.read_reg(Register::A7);
                let bit_num = bus.read_long(sp) as i32;
                let byte_ptr = bus.read_long(sp + 4);
                let byte_offset = (bit_num >> 3) as u32;
                let bit_pos = 7 - (bit_num & 7) as u32;
                let addr = byte_ptr.wrapping_add(byte_offset);
                let byte_val = bus.read_byte(addr);
                bus.write_byte(addr, byte_val | (1 << bit_pos));
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // BitClr ($A85F)
            // Clears a particular bit of a bit image.
            // PROCEDURE BitClr(bytePtr: Ptr; bitNum: LONGINT);
            // Inside Macintosh Volume I, I-472
            // BitClr ($A85F): Clears bit in memory: PROCEDURE BitClr(bytePtr: Ptr; bitNum: LONGINT); per IM:I I-472
            (true, 0x05F) => {
                let sp = cpu.read_reg(Register::A7);
                let bit_num = bus.read_long(sp) as i32;
                let byte_ptr = bus.read_long(sp + 4);
                let byte_offset = (bit_num >> 3) as u32;
                let bit_pos = 7 - (bit_num & 7) as u32;
                let addr = byte_ptr.wrapping_add(byte_offset);
                let byte_val = bus.read_byte(addr);
                bus.write_byte(addr, byte_val & !(1 << bit_pos));
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // HiWord ($A86A)
            // Returns the high-order word of a long integer.
            // FUNCTION HiWord(x: LONGINT): INTEGER;
            // Inside Macintosh Volume I, I-472
            // HiWord ($A86A): Returns (x >> 16) as INTEGER per IM:I I-472
            (true, 0x06A) => {
                let sp = cpu.read_reg(Register::A7);
                let x = bus.read_long(sp);
                let hi = (x >> 16) as u16;
                bus.write_word(sp + 4, hi);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // LoWord ($A86B)
            // Returns the low-order word of a long integer.
            // FUNCTION LoWord(x: LONGINT): INTEGER;
            // Inside Macintosh Volume I, I-472
            // LoWord ($A86B): Returns (x & 0xFFFF) as INTEGER per IM:I I-472
            (true, 0x06B) => {
                let sp = cpu.read_reg(Register::A7);
                let x = bus.read_long(sp);
                let lo = (x & 0xFFFF) as u16;
                bus.write_word(sp + 4, lo);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // FixRound ($A86C)
            // Rounds a Fixed value to the nearest integer.
            // FUNCTION FixRound(x: Fixed): INTEGER;
            // Inside Macintosh Volume I, I-467
            //
            // System 7.5.3 ROM uses round-half-away-from-zero (ANSI-C
            // rint() behaviour) so FixRound(-0.5) = -1, not 0. IM:I-467
            // doesn't specify the tie-break; the convention is anchored
            // by Basilisk's behaviour.
            //
            // Formula: abs(x) + 0.5 truncated toward zero, then negate
            // if x was negative.
            // FixRound ($A86C): Rounds Fixed to nearest integer (round-half-up)
            (true, 0x06C) => {
                let sp = cpu.read_reg(Register::A7);
                let x = bus.read_long(sp) as i32 as i64;
                let abs_rounded = ((x.abs() + 0x8000) >> 16) as i16;
                let rounded = if x < 0 { -abs_rounded } else { abs_rounded };
                bus.write_word(sp + 2, rounded as u16);
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // Random ($A861)
            // Returns a pseudo-random integer in the range -32767..32767.
            // FUNCTION Random: INTEGER;
            // Inside Macintosh Volume I, I-195
            //
            // randSeed is updated to (randSeed * 16807) MOD (2^31 - 1).
            // The result is the low 16 bits of the new seed, interpreted as
            // a signed INTEGER — except that -32768 ($8000) is mapped to 0.
            // Reference: Executor src/quickdraw/qMisc.cpp C_Random
            // Random ($A861): Full Mac Toolbox random algorithm
            (true, 0x061) => {
                let sp = cpu.read_reg(Register::A7);
                let pc = cpu.read_reg(Register::PC);
                let a5 = cpu.read_reg(Register::A5);
                let global_ptr = bus.read_long(a5);
                let seed_addr = global_ptr.wrapping_sub(126);
                let old_seed = bus.read_long(seed_addr);
                let seed = if old_seed == 0 { 1u64 } else { old_seed as u64 };
                let new_seed = ((seed * 16807) % 2147483647) as u32;
                bus.write_long(seed_addr, new_seed);
                // Return the low 16 bits of the seed. The seed is always in
                // 0..2^31-1, so the low 16 bits naturally span -32768..32767
                // when read as a signed INTEGER. Map -32768 to 0 so the
                // result range is exactly -32767..32767.
                let lo = new_seed as u16;
                let result = if lo == 0x8000 { 0u16 } else { lo };
                bus.write_word(sp, result);
                if trace_entropy_enabled() {
                    eprintln!(
                        "[ENTROPY] Random pc=${:08X} seed_addr=${:08X} old_seed={} new_seed={} result={}",
                        pc, seed_addr, old_seed, new_seed, result as i16
                    );
                }
                Ok(())
            }

            // GetIndString ($A9E6)
            //
            // Per IM:I I-468: "GetIndString returns in theString
            // a string in the string list that has the resource
            // ID strListID. It reads the string list from the
            // resource file if necessary, by calling the Resource
            // Manager function GetResource('STR#',strListID). It
            // returns the string specified by the index parameter,
            // which can range from 1 to the number of strings in
            // the list. If the resource can't be read or the index
            // is out of range, the empty string is returned."
            //
            // ## Trap-word repurposing per System 7+
            //
            // IM:I I-468 marks GetIndString as `[Not in ROM]` —
            // legacy System 6 era treated it as a software-only
            // routine (Pascal compiler emitted inline GetResource
            // + Munger code). However IM:III line 9512 master
            // dispatch table assigns trap word $A9E6 to InitAllPacks
            // (a System 6 PROCEDURE that loads Pack0..Pack7 from
            // the System file). When System 7 deprecated package
            // pre-loading (Pack0..Pack7 became autoload-on-demand),
            // Apple repurposed trap word $A9E6 to GetIndString —
            // making the System 6 software-only routine into a
            // System 7+ ROM-resident Toolbox trap. Same trap-word
            // repurposing pattern as $A056 (LwrString → LowerText
            // / UpperText / StripText / StripUpperText per IM:VI
            // line 30881 — already handled at memory.rs:1610).
            //
            // The InitAllPacks call site is now unreachable from
            // System 7+ apps (autoload happens implicitly during
            // package use; no explicit init needed). Apps emitting
            // $A9E6 from System 6 era are calling InitAllPacks
            // expecting a no-args no-result init — Systemless's
            // GetIndString impl reads sp+0 / sp+2 / sp+4 as args,
            // which on a System 6 InitAllPacks call would dereference
            // stale stack values. In practice no current corpus title
            // is System 6 era; all corpus games are System 7+ and
            // emit $A9E6 expecting GetIndString semantics. If a
            // future System-6 binary surfaces InitAllPacks usage,
            // detect via trap-trace and add a stack-shape check
            // (4-byte InitAllPacks frame vs 8-byte GetIndString
            // frame distinguished by post-pop SP).
            //
            // PROCEDURE GetIndString (VAR theString: Str255;
            //                          strListID: INTEGER;
            //                          index: INTEGER);
            // Inside Macintosh Volume I, I-468
            // Inside Macintosh Volume III line 9512: $A9E6 = InitAllPacks (legacy)
            // System 7+ repurposing per Apple Toolbox extension (undocumented in IM)
            //
            // Stack: SP+0 index INTEGER (2 bytes), SP+2 strListID
            // INTEGER (2 bytes), SP+4 theString VAR Str255 ptr
            // (4 bytes). Pop 8 bytes.
            // GetIndString ($A9E6): Looks up STR# resource by ID, returns 1-based indexed Pascal string per IM:I I-468; empty string on not-found or out-of-range. Trap-word $A9E6 repurposed by Apple System 7+ from legacy InitAllPacks (per IM:III line 9512 master dispatch table) — same trap-word-repurposing pattern as $A056 LwrString→LowerText family per IM:VI 30881. System 6 InitAllPacks callers would dereference stale stack values via Systemless's GetIndString frame; no current corpus title is System 6 era.
            (true, 0x1E6) => {
                let sp = cpu.read_reg(Register::A7);
                let pc = cpu.read_reg(Register::PC);
                // Stack layout: SP+0=index, SP+2=strListID, SP+4=theString (VAR ptr)
                let index = bus.read_word(sp) as usize;
                let str_list_id = bus.read_word(sp + 2) as i16;
                let the_string_ptr = bus.read_long(sp + 4);

                let res_type = *b"STR#";
                let mut res_found = false;
                let found_str: Option<Vec<u8>> =
                    if let Some((_, data_ptr)) = self.find_resource_any(res_type, str_list_id) {
                        res_found = true;
                        // STR# format: 2-byte count, then Pascal strings (1-byte len + chars)
                        // Inside Macintosh Volume I, I-476
                        let count = bus.read_word(data_ptr) as usize;
                        if index >= 1 && index <= count {
                            let mut offset = 2u32;
                            let mut found = None;
                            for i in 1..=count {
                                let len = bus.read_byte(data_ptr + offset) as usize;
                                offset += 1;
                                if i == index {
                                    found = Some(bus.read_bytes(data_ptr + offset, len));
                                    break;
                                }
                                offset += len as u32;
                            }
                            found
                        } else {
                            None
                        }
                    } else {
                        None
                    };

                // IM:I I-468 documents GetIndString as calling
                // GetResource('STR#', strListID) "if necessary". On
                // the success path that underlying Resource Manager hit
                // must clear stale ResErr to noErr, which callers can
                // observe immediately after GetIndString returns. The
                // miss path is not pinned here: BasiliskII leaves the
                // missing-resource buffer contents / ResErr state on a
                // looser implementation-defined path than the Apple
                // text specifies, so Systemless preserves the pre-call
                // ResErr value when no STR# is found.
                if res_found {
                    bus.write_word(0x0A60, 0); // noErr
                }

                if the_string_ptr != 0 {
                    match found_str {
                        Some(bytes) => {
                            bus.write_pstring(the_string_ptr, &bytes);
                            if trace_entropy_enabled() {
                                let text = String::from_utf8_lossy(&bytes);
                                eprintln!(
                                    "[ENTROPY] GetIndString pc=${:08X} strListID={} index={} -> {:?}",
                                    pc, str_list_id, index, text
                                );
                            }
                        }
                        None => {
                            bus.write_byte(the_string_ptr, 0);
                            if trace_entropy_enabled() {
                                eprintln!(
                                    "[ENTROPY] GetIndString pc=${:08X} strListID={} index={} -> <empty>",
                                    pc, str_list_id, index
                                );
                            }
                        }
                    }
                }
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // SystemTask ($A9B4)
            // Per IM:I I-440: "For each open desk accessory (or other
            // device driver performing periodic actions), SystemTask
            // causes the accessory to perform the periodic action
            // defined for it, if any such action has been defined and
            // if the proper time period has passed since the action
            // was last performed. ... You should call SystemTask as
            // often as possible, usually once every time through your
            // main event loop."
            // PROCEDURE SystemTask;
            // Inside Macintosh Volume I, I-440
            //
            // Calling convention (Tool-bit PROCEDURE per IM:I I-440):
            //   no inputs, no FUNCTION result slot, no Pascal stack
            //   argument frame. A7 is preserved across the call.
            //
            // MPW Universal Headers Desk.h:
            //   EXTERN_API(void) SystemTask(void) ONEWORDINLINE(0xA9B4);
            //
            // HLE compromise: Systemless models no Desk Accessories, no
            // DRVR chain, no DCE table, no Time Manager periodic-task
            // queue — every component the trap would walk is empty.
            // The implementation is `Ok(())` (a true no-op). Apps
            // universally call SystemTask once per main-event-loop
            // iteration; the call is correctly a no-op since no DA is
            // registered.
            //
            // Engines-agree subset (per a9b4_a9c2_systemtask_systemedit_strict):
            //   - register-only Tool-bit PROCEDURE calling convention
            //     (no Pascal stack frame, no FUNCTION result slot)
            //   - A7 preserved across a single call AND a 5-call
            //     composition (BasiliskII System 7.5.3 ROM walks empty
            //     DA/DCE state and returns without consuming stack)
            //
            // Catalogue-proof: a9b4_a9c2_systemtask_systemedit_strict
            //   B1: A9B4:systemtask_procedure_call_preserves_stack_pointer
            //
            // Contract tests:
            //   - systemtask_procedure_call_preserves_stack_pointer (single call)
            //   - systemtask_five_call_composition_preserves_stack_pointer
            (true, 0x1B4) => Ok(()),

            // GetAppParms ($A9F5)
            // Returns the current application's name, resource file refnum,
            // and Finder information handle.
            // PROCEDURE GetAppParms(VAR apName: Str255; VAR apRefNum: INTEGER;
            //                       VAR apParam: Handle);
            // Inside Macintosh Volume II, II-58
            //
            // Reads low-memory globals:
            //   CurApName ($0910) — Pascal string (Str31)
            //   CurApRefNum ($0900) — INTEGER
            //   AppParmHandle ($0AEC) — Handle
            //
            // Regression coverage:
            //   getappparms_returns_app_parameters
            (true, 0x1F5) => {
                let sp = cpu.read_reg(Register::A7);
                let ap_param_ptr = bus.read_long(sp);
                let ap_refnum_ptr = bus.read_long(sp + 4);
                let ap_name_ptr = bus.read_long(sp + 8);

                // Copy CurApName (Str31 at $0910) → *apName
                if ap_name_ptr != 0 {
                    let bytes = bus.read_pstring(addr::CUR_APNAME);
                    let n = bytes.len().min(31);
                    bus.write_byte(ap_name_ptr, n as u8);
                    bus.write_bytes(ap_name_ptr + 1, &bytes[..n]);
                }

                // Copy CurApRefNum ($0900) → *apRefNum
                if ap_refnum_ptr != 0 {
                    let refnum = bus.read_word(addr::CUR_APREF_NUM);
                    bus.write_word(ap_refnum_ptr, refnum);
                }

                // Copy AppParmHandle ($0AEC) → *apParam
                if ap_param_ptr != 0 {
                    let handle = bus.read_long(addr::APP_PARM_HANDLE);
                    bus.write_long(ap_param_ptr, handle);
                }

                cpu.write_reg(Register::A7, sp + 12);
                Ok(())
            }

            // UnloadSeg ($A9F1)
            // Marks a code segment as purgeable once no routines in it are
            // being called. Systemless keeps all loaded segments resident, so
            // this is a no-op that pops its Ptr argument.
            // PROCEDURE UnloadSeg(routineAddr: Ptr);
            // Inside Macintosh Volume II, II-58
            //
            // Regression coverage:
            //   toolbox::tests::unloadseg_consumes_routineaddr_pointer_argument
            //   toolbox::tests::unloadseg_noop_preserves_registered_segment_cache
            // UnloadSeg ($A9F1): Pops Ptr argument per IM:II II-58; Systemless keeps all segments resident
            (true, 0x1F1) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // LaunchApplication ($A9F2)
            //
            // Per IM:II II-60: "Launch starts up another application
            // (the new application). The current application is
            // terminated; control transfers to the new application."
            // Per IM:VI Process Manager 28-1..28-4: System 7+
            // renamed Launch to LaunchApplication and extended the
            // parameter convention via a LaunchParamBlockRec
            // (LaunchPB structure with launchAppSpec FSSpec ptr +
            // launchControlFlags + launchPreferredSize +
            // launchMinimumSize). Both call paths share trap word
            // $A9F2 — register-based dispatch where A0 points to
            // either the IM:II Launch CmdLine record (legacy) or
            // the IM:VI LaunchPB record (System 7+).
            //
            // Per IM:VI Table C-1 line 57551 + 57649:
            // "LaunchApplication | $A9F2" — the canonical System 7+
            // name. Systemless previously used the legacy IM:II "Launch"
            // name. Both names refer to the same trap word — the
            // mapping pre-dates Color QuickDraw, has been allocated
            // to this trap in every Mac OS release since System 1.0,
            // and was renamed (not relocated) for System 7.
            // Inside Macintosh Volume II, II-60 (Launch — legacy)
            // Inside Macintosh Volume VI, 28-1..28-4 (LaunchApplication — System 7+)
            //
            // Register convention: A0 points to a launch parameter
            // block (CmdLine pre-System-7 or LaunchPB post-System-7).
            // No stack args (register-based OS trap pattern).
            //
            // HLE compromise: Systemless has one foreground application
            // image, not a full Process Manager with background process
            // scheduling. Foreground launches of existing VFS applications
            // are queued for the runner to load into a fresh application
            // heap; launchContinue launches begin when the caller next
            // yields through the Event Manager, matching Processes 1994
            // p. 2-15. launchDontSwitch remains a bookkeeping-only path.
            // On launch failure, LaunchApplication returns 0 in the
            // launchProcessSN / launchPreferredSize / launchMinimumSize /
            // launchAvailableSize fields so callers do not observe stale
            // output values.
            //
            // Trap-name fixed during the trap-name verification audit
            // pattern — was previously labeled
            // "Launch" (legacy IM:II name); audit cross-referenced
            // against IM:VI Table C-1 master dispatch table line
            // 57551 "_LaunchApplication | $A9F2" and corrected to
            // the canonical System 7+ name.
            //
            // Regression coverage:
            //   toolbox::tests::launchapplication_launchcontinue_clear_records_target_app_path_and_halts
            //   toolbox::tests::launchapplication_launchcontinue_set_records_target_app_path_and_returns
            //   toolbox::tests::launchapplication_existing_foreground_target_queues_runner_launch
            // LaunchApplication ($A9F2): Per IM:VI Table C-1 line 57551 the canonical System 7+ name is LaunchApplication; legacy IM:II II-60 name was "Launch" (same trap word). Register-based: A0 points to LaunchPB record (System 7+) or legacy CmdLine record (pre-System-7); no stack args. Existing foreground launch targets are queued for runner-level app switching; missing targets preserve the historical halt/return behavior according to launchContinue.
            (true, 0x1F2) => {
                let launch_params = cpu.read_reg(Register::A0);
                let launch_flags = if launch_params != 0 {
                    bus.read_word(launch_params + 14)
                } else {
                    0
                };
                let launch_continue = (launch_flags & 0x4000) != 0;
                let launch_dont_switch = (launch_flags & 0x0200) != 0;
                let mut launch_result = 0u32;
                let mut launch_target: Option<String> = None;
                if launch_params != 0 {
                    let app_spec_ptr = bus.read_long(launch_params + 16);
                    if app_spec_ptr != 0 {
                        let filename = crate::trap::types::read_fsspec_name(bus, app_spec_ptr);
                        if !filename.is_empty() {
                            let vref = bus.read_word(app_spec_ptr) as i16;
                            let dir_id = bus.read_long(app_spec_ptr + 2);
                            let app_path = self
                                .vfs_key_for_fsspec(vref, dir_id, &filename)
                                .unwrap_or(filename);
                            self.set_launched_app_path(&app_path);
                            let target_exists = self.find_vfs_file(&app_path).is_some()
                                || self.find_vfs_rsrc_file(&app_path).is_some();
                            if target_exists {
                                launch_target = Some(app_path);
                            } else {
                                launch_result = (-43i32) as u32; // fnfErr
                            }
                        }
                    } else {
                        launch_result = (-43i32) as u32; // fnfErr
                    }
                    if launch_result != 0 {
                        bus.write_long(launch_params + 20, 0); // launchProcessSN.highLongOfPSN
                        bus.write_long(launch_params + 24, 0); // launchProcessSN.lowLongOfPSN
                        bus.write_long(launch_params + 28, 0); // launchPreferredSize
                        bus.write_long(launch_params + 32, 0); // launchMinimumSize
                        bus.write_long(launch_params + 36, 0); // launchAvailableSize
                    }
                }
                cpu.write_reg(Register::D0, launch_result);
                let queued_foreground_launch =
                    launch_result == 0 && launch_target.is_some() && !launch_dont_switch;
                if queued_foreground_launch {
                    if let Some(app_path) = launch_target.as_deref() {
                        self.queue_pending_launch_application(app_path, launch_continue);
                    }
                }
                if launch_continue || queued_foreground_launch {
                    return Some(Ok(()));
                }
                Err(Error::Halted)
            }

            // Chain ($A9F3)
            // Legacy CmdLine entry point. A0 points to a record whose
            // first longword points to the application's Pascal file
            // name and whose 4(A0) word carries the sound/screen buffer
            // configuration (CurPageOption).
            // Inside Macintosh Volume II (1985), pp. II-59 to II-60.
            //
            // Systemless cannot actually hand control to another
            // application, but it does preserve the documented
            // bookkeeping: record CurPageOption in low memory, record
            // the launched app path when the filename pointer is
            // present, then halt the guest.
            //
            // Regression coverage:
            //   src/trap/toolbox.rs::tests::chain_records_cmdline_path_and_curpageoption_before_halt
            // Chain ($A9F3): Halts emulation after recording the legacy CmdLine metadata per IM:II II-59..II-60.
            (true, 0x1F3) => {
                let cmd_line = cpu.read_reg(Register::A0);
                if cmd_line != 0 {
                    let page_option = bus.read_word(cmd_line + 4);
                    bus.write_word(0x0936, page_option);

                    let app_name_ptr = bus.read_long(cmd_line);
                    if app_name_ptr != 0 {
                        let app_name =
                            String::from_utf8_lossy(&bus.read_pstring(app_name_ptr)).into_owned();
                        if !app_name.is_empty() {
                            let app_path = match self.directory_path_for_id(self.default_dir_id) {
                                Some(dir_path) if !dir_path.is_empty() => {
                                    format!("{dir_path}/{app_name}")
                                }
                                _ => app_name,
                            };
                            self.set_launched_app_path(&app_path);
                        }
                    }
                }
                Err(Error::Halted)
            }

            // ExitToShell ($A9F4)
            // Terminates the current application and returns to the Finder.
            // PROCEDURE ExitToShell;
            // Inside Macintosh Volume II, II-58
            // ExitToShell ($A9F4): Halts emulation
            (true, 0x1F4) => Err(Error::Halted),

            // Debugger ($A9FF)
            // Parameterless debugger entry trap.
            // Universal Interfaces Types.h declares Debugger() as
            // ONEWORDINLINE(0xA9FF) with no parameters.
            // Inside Macintosh: Processes (1994), p. 7-9;
            // Inside Macintosh: Memory (1992), p. 3-23.
            // Debugger ($A9FF): No debugger installed on Systemless,
            // so this is a no-op that returns to the caller.
            (true, 0x1FF) => Ok(()),

            // _Shutdown ($A895) — Shutdown Manager dispatch
            // Inside Macintosh Volume V, V-589..V-590.
            //
            // Universal Headers <ShutDown.h> (System 7.5, Universal Interfaces 3.4)
            // declares all four Shutdown Manager entry points as
            //   THREEWORDINLINE(0x3F3C, <selector>, 0xA895)
            // where 0x3F3C is `MOVE.W #imm,-(A7)` — the compiler emits this
            // inline glue at every call site:
            //
            //   ShutDwnPower():
            //     ; (no caller args)
            //     MOVE.W #1, -(A7)        ; 0x3F3C 0x0001
            //     _Shutdown               ; 0xA895
            //
            //   ShutDwnStart():
            //     ; (no caller args)
            //     MOVE.W #2, -(A7)        ; 0x3F3C 0x0002
            //     _Shutdown               ; 0xA895
            //
            //   ShutDwnInstall(proc, flags):
            //     ; caller already pushed proc (4) + flags (2)  -- Pascal LTR
            //     MOVE.W #3, -(A7)        ; 0x3F3C 0x0003
            //     _Shutdown               ; 0xA895
            //
            //   ShutDwnRemove(proc):
            //     ; caller already pushed proc (4)
            //     MOVE.W #4, -(A7)        ; 0x3F3C 0x0004
            //     _Shutdown               ; 0xA895
            //
            // On entry to the trap, SP+0 holds the selector word. The trap
            // is responsible for popping the entire frame (selector + args).
            //
            // Selectors:
            //   1  sdPowerOff — ShutDwnPower  (halts; emulator can't power off)
            //   2  sdRestart  — ShutDwnStart  (halts; emulator can't reboot)
            //   3  sdInstall  — ShutDwnInstall(proc, flags)  (no-op; pops 8)
            //   4  sdRemove   — ShutDwnRemove(proc)          (no-op; pops 6)
            //
            // Systemless does not model the shutdown procedure chain, so
            // sdInstall/sdRemove are accepted as no-ops that simply pop
            // the argument frame. sdPowerOff and sdRestart both halt the
            // guest, which is how the runner surfaces "application wants
            // to exit". The procedure list is never invoked because the
            // bake never triggers an actual shutdown; this matches the
            // BasiliskII System 7.5 ROM, where queued procs are also dormant
            // until real shutdown.
            (true, 0x095) => {
                let sp = cpu.read_reg(Register::A7);
                let selector = bus.read_word(sp) as i16;
                let pop_bytes = match selector {
                    3 => {
                        // ShutDwnInstall(proc: ProcPtr; flags: INTEGER)
                        // SP+0 selector(2), SP+2 flags(2), SP+4 proc(4)
                        8
                    }
                    4 => {
                        // ShutDwnRemove(proc: ProcPtr)
                        // SP+0 selector(2), SP+2 proc(4)
                        6
                    }
                    _ => return Some(Err(Error::Halted)),
                };

                // The documented non-halting selectors are caller-visible
                // no-ops apart from consuming the full Pascal argument frame.
                cpu.write_reg(Register::D0, 0);
                cpu.write_reg(Register::A7, sp + pop_bytes);
                Ok(())
            }

            // Delay ($A03B) - OS trap
            // PROCEDURE Delay(numTicks: LONGINT; VAR finalTicks: LONGINT);
            // Inside Macintosh Volume II, II-384 (via OS Utilities)
            // A0 = numTicks, returns finalTicks in D0
            // Delay ($A03B): Blocks for A0 ticks via runner service_delay_ticks; GUI mode paces to wall-clock, headless advances directly. Returns finalTicks in D0
            (false, 0x3B) => {
                let num_ticks = cpu.read_reg(Register::A0);
                let trap_pc = cpu.read_reg(Register::PC).wrapping_sub(2);
                if trace_title_diag_enabled() {
                    let tick = bus.read_long(0x016A);
                    if (68..=110).contains(&tick) {
                        eprintln!(
                            "[TITLE-DIAG] Delay tick={} pc=${:08X} ticks={}",
                            tick, trap_pc, num_ticks
                        );
                    }
                }
                if num_ticks == 0 {
                    // Zero delay: return current ticks immediately
                    let current_ticks = bus.read_long(0x016A);
                    cpu.write_reg(Register::D0, current_ticks);
                } else {
                    // Queue the delay for the runner to consume tick-by-tick.
                    // On a real Mac, Delay blocks via PrimeTime + interrupt wait
                    // (executor osutil.cpp:823-838). Our runner drains these ticks
                    // one-at-a-time through advance_guest_tick(), firing VBL and
                    // timer tasks at each boundary. The runner writes finalTicks
                    // to D0 when the delay is fully consumed.
                    self.pending_delay_ticks = num_ticks;
                }
                if let Err(err) = self.record_oracle_event(
                    bus,
                    trap_pc,
                    "delay",
                    Self::oracle_field_map(&[("ticks", num_ticks.to_string())]),
                    false,
                ) {
                    return Some(Err(err));
                }
                Ok(())
            }

            // _SCSIDispatch ($A815) — SCSI Manager dispatch
            // Word selector on top of the stack; each selector pops its
            // own argument set and leaves a 2-byte OSErr result above.
            // Inside Macintosh Volume IV, IV-287 to IV-300
            // Inside Macintosh Volume V, V-389 to V-394
            //
            // Systemless does not model SCSI hardware. Every selector
            // returns noErr — apps typically check for a present device
            // via SCSIGet/SCSISelect and bail before reaching data
            // transfer when no device is installed.
            //
            // Regression coverage exercises selector pop discipline and noErr defaults.
            // _SCSIDispatch ($A815): Word-selector dispatch per IM:IV IV-287; pops args per selector, returns noErr — no SCSI hardware
            (true, 0x015) => {
                let sp = cpu.read_reg(Register::A7);
                let selector = bus.read_word(sp) as i16;
                // Arg bytes (below selector) per IM:IV IV-287..IV-300 and IM:V V-389..V-394.
                let arg_bytes = Self::scsi_dispatch_arg_bytes(selector);
                let total = 2 + arg_bytes;
                bus.write_word(sp + total, 0); // noErr
                cpu.write_reg(Register::A7, sp + total);
                Ok(())
            }

            // PPC ($A0DD) — PPC Toolbox dispatch (inter-app communication)
            // D0 = selector, A0 = parameter block.
            // Inside Macintosh: Interapplication Communication (1993),
            // pp. 7-39, 7-41 to 7-42, 7-57.
            //
            // Systemless models the PPC Toolbox state that the baked fixture
            // observes: selector $0000 (`PPCInit`) flips the init bit for
            // selectors that need it, but selector $000A (`IPCListPorts`)
            // already succeeds on the zero-request local path before init.
            // The strict fixture `a0dd_ppc_strict`
            // witnesses selector $0000 plus selector $000A on both the
            // pre-init and post-init local paths.
            (false, 0x0DD) => {
                let selector = cpu.read_reg(Register::D0) as u16;
                let pb = cpu.read_reg(Register::A0);
                let not_init_err = (-900i32) as u32;

                if selector != 0 && selector != 0x000A && !self.ppc_initialized {
                    cpu.write_reg(Register::D0, not_init_err);
                    return Some(Ok(()));
                }

                match selector {
                    0x0000 => {
                        self.ppc_initialized = true;
                        cpu.write_reg(Register::D0, 0);
                    }
                    0x000A => {
                        if pb != 0 {
                            bus.write_word(pb + 16, 0);
                            bus.write_word(pb + 44, 0);
                        }
                        cpu.write_reg(Register::D0, 0);
                    }
                    _ => {
                        cpu.write_reg(Register::D0, 0);
                    }
                }
                Ok(())
            }

            // SlotManager ($A06E) — NuBus Slot Manager dispatch
            // A0 = SpBlockPtr, D0 = routine selector.
            // Inside Macintosh: Devices (1994), pp. 2-61 to 2-62.
            //
            // _SlotManager routines are selector-based (D0 on entry)
            // and return OSErr in D0. For SReadInfo selector $0010, the
            // documented empty-slot result is smEmptySlot (-300).
            // SpBlock.spResult is the first longword at offset 0.
            // Devices 1994, pp. 2-23 to 2-24 and 2-61 to 2-62.
            //
            // Systemless models no NuBus cards, so every selector returns
            // smEmptySlot. For the documented SReadInfo selector, we
            // also mirror that result into SpBlock.spResult when
            // SpBlockPtr is non-NIL.
            //
            // Regression coverage:
            //   src/trap/toolbox.rs::slotmanager_sreadinfo_selector_uses_a0_spblock_d0_selector_and_returns_oserr_in_d0
            //   src/trap/toolbox.rs::slotmanager_sreadinfo_empty_slot_returns_smemptyslot
            //   src/trap/toolbox.rs::slotmanager_writes_result_to_spblock_spresult_offset_zero
            (false, 0x06E) => {
                let sp_block_ptr = cpu.read_reg(Register::A0);
                let selector = cpu.read_reg(Register::D0) as i32;
                let sm_empty_slot: i32 = -300;
                if selector == 0x0010 && sp_block_ptr != 0 {
                    bus.write_long(sp_block_ptr, sm_empty_slot as u32);
                }
                cpu.write_reg(Register::D0, sm_empty_slot as u32);
                eprintln!(
                    "[TRAP] SlotManager selector={} -> smEmptySlot (no NuBus cards modeled)",
                    selector
                );
                Ok(())
            }

            // ========== Resource Manager (extended) ==========

            // OpenRFPerm ($A9C4): name, vRefNum, permission → refnum
            // Opens a resource fork and returns its refnum. A newly opened
            // file becomes current; if already open, returns existing refnum
            // without switching current file (IM:IV IV-17; MTb 1993 1-64..1-66).
            // Mirror the FUNCTION return value in D0 as well as the result slot.
            (true, 0x1C4) => {
                let sp = cpu.read_reg(Register::A7);
                let perm = bus.read_byte(sp) as i8 as i16;
                let wants_write = perm == 2 || perm == 3;
                let _vref = bus.read_word(sp + 2);
                let name_ptr = bus.read_long(sp + 4);
                let name_len = bus.read_byte(name_ptr) as usize;
                let mut name_bytes = vec![0u8; name_len];
                for (i, byte) in name_bytes.iter_mut().enumerate() {
                    *byte = bus.read_byte(name_ptr + 1 + i as u32);
                }
                let name = decode_mac_roman(&name_bytes);
                if super::dispatch::trace_resfile_enabled() {
                    eprintln!("[TRAP] OpenRFPerm(\"{}\")", name);
                }

                // Try to find and load the resource fork from vfs_rsrc
                if let Some(vfs_key) = self.find_vfs_rsrc_file(&name) {
                    // Dedupe: if this file is already open, return the
                    // existing refnum and skip the merge. Without this,
                    // games that re-open their own fork (Bonkheads opens
                    // it 16+ times during boot) re-allocate every
                    // resource on every open and exhaust the heap before
                    // the title even renders.
                    if let Some(existing) = self.refnum_for_resource_file_name(&vfs_key) {
                        if wants_write {
                            self.write_refnums.insert(existing);
                        }
                        if !wants_write && self.write_refnums.contains(&existing) {
                            let existing_read_only =
                                self.resources.as_ref().and_then(|resources| {
                                    let mut refnums: Vec<u16> = resources
                                        .names
                                        .iter()
                                        .filter_map(|(&refnum, resource_name)| {
                                            (resource_name == &vfs_key
                                                && !self.write_refnums.contains(&refnum))
                                            .then_some(refnum)
                                        })
                                        .collect();
                                    refnums.sort_unstable();
                                    refnums.into_iter().next()
                                });
                            if let Some(read_refnum) = existing_read_only {
                                if super::dispatch::trace_resfile_enabled() {
                                    eprintln!(
                                        "[TRAP] OpenRFPerm: \"{}\" already read-open as refnum {}, dedup",
                                        name, read_refnum
                                    );
                                }
                                bus.write_word(sp + 8, read_refnum);
                                cpu.write_reg(Register::D0, read_refnum as u32);
                                bus.write_word(0x0A60, 0); // ResErr = noErr
                                cpu.write_reg(Register::A7, sp + 8);
                                return Some(Ok(()));
                            }
                            if super::dispatch::trace_resfile_enabled() {
                                eprintln!(
                                    "[TRAP] OpenRFPerm: \"{}\" write-opened refnum {}, forcing new read-only access path",
                                    name, existing
                                );
                            }
                            let refnum =
                                self.open_resource_file_from_vfs_key(bus, &vfs_key, wants_write);
                            bus.write_word(sp + 8, refnum);
                            cpu.write_reg(Register::D0, refnum as u32);
                            bus.write_word(0x0A60, 0); // ResErr = noErr
                            cpu.write_reg(Register::A7, sp + 8);
                            return Some(Ok(()));
                        }
                        if super::dispatch::trace_resfile_enabled() {
                            eprintln!(
                                "[TRAP] OpenRFPerm: \"{}\" already open as refnum {}, dedup",
                                name, existing
                            );
                        }
                        bus.write_word(sp + 8, existing);
                        cpu.write_reg(Register::D0, existing as u32);
                        bus.write_word(0x0A60, 0); // ResErr = noErr
                        cpu.write_reg(Register::A7, sp + 8);
                        return Some(Ok(()));
                    }
                    let rsrc_data = self.vfs_rsrc.get(&vfs_key).unwrap().clone();
                    eprintln!(
                        "[TRAP] OpenRFPerm: found rsrc fork for \"{}\" ({} bytes)",
                        vfs_key,
                        rsrc_data.len()
                    );
                    let refnum = self.open_resource_file_from_vfs_key(bus, &vfs_key, wants_write);
                    bus.write_word(sp + 8, refnum);
                    cpu.write_reg(Register::D0, refnum as u32);
                    bus.write_word(0x0A60, 0); // ResErr = noErr
                } else {
                    eprintln!("[TRAP] OpenRFPerm: \"{}\" not found in vfs_rsrc", name);
                    bus.write_word(sp + 8, (-1i16) as u16);
                    cpu.write_reg(Register::D0, (-1i32) as u32);
                    bus.write_word(0x0A60, (-43i16) as u16); // ResErr = fnfErr
                }
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // CloseResFile ($A99A)
            // Closes a resource file: calls UpdateResFile, releases
            // resources, removes the file from the search order, and
            // resets the current file if needed.
            // PROCEDURE CloseResFile(refNum: INTEGER);
            // Inside Macintosh Volume I, I-115
            //
            // Regression coverage:
            //   closeresfile_removes_file_from_search_order
            //   closeresfile_resets_current_file
            //   closeresfile_pops_two_bytes
            // CloseResFile ($A99A): Updates resources, removes file from search order, resets current file per IM:I I-115
            (true, 0x19A) => {
                let sp = cpu.read_reg(Register::A7);
                let refnum = bus.read_word(sp);
                if trace_sound_enabled() {
                    eprintln!(
                        "[RSRC] CloseResFile refnum={} name={:?}",
                        refnum,
                        self.resource_file_name(refnum)
                    );
                }

                if refnum != 0 && self.close_resource_file_refnum(bus, refnum) {
                    self.open_files.remove(&refnum);
                    self.file_positions.remove(&refnum);
                    self.write_refnums.remove(&refnum);
                    bus.write_word(0x0A60, 0); // noErr
                } else if refnum == 0 {
                    // Closing system resource file: close all others first
                    // For now, just reset current to 0
                    self.set_current_resource_refnum(bus, 0);
                    bus.write_word(0x0A60, 0);
                } else {
                    // IM:I I-115 documents resNotFound here, but
                    // BasiliskII/System 7.5.3 reports resFNotFound for a
                    // non-open resource-file refnum.
                    const RES_F_NOT_FOUND: i16 = -193;
                    bus.write_word(0x0A60, RES_F_NOT_FOUND as u16);
                }
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // UseResFile ($A998)
            // PROCEDURE UseResFile(refNum: INTEGER);
            // Inside Macintosh Volume I, I-116.
            // UseResFile ($A998): Sets current resource file refnum
            (true, 0x198) => {
                let sp = cpu.read_reg(Register::A7);
                let refnum = bus.read_word(sp);
                if trace_sound_enabled() {
                    eprintln!(
                        "[RSRC] UseResFile refnum={} name={:?}",
                        refnum,
                        self.resource_file_name(refnum)
                    );
                }
                let file_exists = self
                    .resources
                    .as_ref()
                    .is_some_and(|r| r.files.contains_key(&refnum));
                if file_exists {
                    self.set_current_resource_refnum(bus, refnum);
                    bus.write_word(0x0A60, 0); // noErr
                } else {
                    // IM:I I-116: invalid refnum leaves the current file
                    // unchanged and ResError returns resFNotFound.
                    bus.write_word(0x0A60, (-193i16) as u16);
                    bus.write_word(0x0A5A, self.current_resource_refnum());
                }
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // CountResources ($A99C) / Count1Resources ($A80D)
            // FUNCTION CountResources(theType: ResType): INTEGER;
            // Inside Macintosh Volume I, I-116
            //
            // Count*Resources always succeeds — it returns 0 for unknown
            // types or empty files. The contract requires ResErr to be
            // cleared to noErr on every successful call so callers don't
            // observe stale errors from earlier Resource Manager calls.
            //
            // Regression coverage:
            //   countresources_clears_reserror
            // Count1Resources ($A80D): Counts resources of given type in current resource file
            // CountResources ($A99C): Counts resources of given type; clears ResErr per IM:I I-116
            (true, 0x19C) | (true, 0x00D) => {
                let sp = cpu.read_reg(Register::A7);
                let raw_res_type = bus.read_long(sp).to_be_bytes();
                let res_type = super::TrapDispatcher::normalize_ostype(raw_res_type);
                let current_only = trap_num == 0x00D;
                let count = self.count_resources(res_type, current_only) as u16;
                let type_str = String::from_utf8_lossy(&res_type);
                if trace_sound_enabled()
                    && self
                        .resources
                        .as_ref()
                        .is_some_and(|resources| resources.files.len() > 1)
                {
                    eprintln!(
                        "[TRAP] CountResources('{}') = {} current={} only_current={}",
                        type_str,
                        count,
                        self.current_resource_refnum(),
                        current_only
                    );
                } else {
                    eprintln!("[TRAP] CountResources('{}') = {}", type_str, count);
                }
                bus.write_word(sp + 4, count);
                bus.write_word(0x0A60, 0); // ResErr = noErr
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // NOTE: GetResAttrs ($A9A6) lives in resource.rs at the
            // same slot (true, 0x1A6). A near-identical handler used to
            // live here too, but it was dead code — dispatch_resource
            // runs before dispatch_toolbox so the resource.rs handler
            // always won. Removed so there's one canonical implementation.

            // ========== Misc Toolbox ==========

            // Munger ($A9E0)
            // Manipulates bytes in a relocatable block by searching and replacing.
            // FUNCTION Munger(h: Handle; offset: LongInt; ptr1: Ptr;
            //     len1: LongInt; ptr2: Ptr; len2: LongInt): LongInt;
            // Inside Macintosh Volume I 1985, I-468 to I-469;
            // Text 1993, 5-75 to 5-76
            // Munger ($A9E0): Searches/replaces bytes in a handle, including insert/delete and tail-partial-match behavior
            (true, 0x1E0) => {
                let sp = cpu.read_reg(Register::A7);
                let trap_site = cpu.read_reg(Register::PC).wrapping_sub(2);
                let len2 = bus.read_long(sp) as i32;
                let ptr2 = bus.read_long(sp + 4);
                let len1 = bus.read_long(sp + 8) as i32;
                let ptr1 = bus.read_long(sp + 12);
                let offset = bus.read_long(sp + 16) as i32;
                let handle = bus.read_long(sp + 20);

                let result =
                    Self::munger_in_handle(bus, trap_site, handle, offset, ptr1, len1, ptr2, len2);
                bus.write_long(sp + 24, result as u32);
                cpu.write_reg(Register::A7, sp + 24);
                Ok(())
            }

            // XMunger ($A819)
            // Phantom trap word exposed in the System 7.6-era public trap
            // namespace. BasiliskII treats it as an observed no-op/no-pop
            // stub: callers keep the original handle contents and the stack
            // frame remains unbalanced after the call.
            (true, 0x019) => Ok(()),

            // PBOpenRF / PBHOpenRF ($A00A / $A20A) — Open Resource Fork
            // FUNCTION PBOpenRF (paramBlock: ParmBlkPtr; async: BOOLEAN): OSErr;
            // FUNCTION PBHOpenRF (paramBlock: HParmBlkPtr; async: BOOLEAN): OSErr;
            // Files 1992, 2-117 / 9282 (HOpenRF).  The OS-trap dispatcher
            // masks `trap & 0x00FF`, so $A20A PBHOpenRF lands on the same
            // low byte and shares this arm.
            //
            // Regression coverage (BasiliskII goldens):
            //   - pb_open_rf            — $A00A fnfErr path
            //   - pbh_open_rf_rename    — $A20A + $A20B fnfErr paths
            // PBOpenRF ($A00A): Opens resource fork via PBOpen path
            // PBHOpenRF ($A20A): HFS variant aliased onto $A00A
            (false, 0x0A) => {
                let pb = cpu.read_reg(Register::A0);
                let name_ptr = bus.read_long(pb + 18);
                let filename = Self::read_pb_filename(bus, name_ptr);
                eprintln!("[TRAP] PBOpenRF(\"{}\")", filename);

                // Try to find resource fork in vfs_rsrc
                if let Some(vfs_key) = self.find_vfs_rsrc_file(&filename) {
                    let rsrc_data = self.vfs_rsrc.get(&vfs_key).unwrap().clone();
                    eprintln!(
                        "[TRAP] PBOpenRF: found rsrc fork for \"{}\" ({} bytes)",
                        vfs_key,
                        rsrc_data.len()
                    );
                    // Register as an open file (store rsrc data as a regular VFS entry for FSRead).
                    // Use entry().or_insert to avoid clobbering writes from a previous open.
                    // Mars Rising's installer pattern: open temp rsrc fork, write 81KB to
                    // it, close, then re-open and expect the 81KB to still be there. If we
                    // re-seed from vfs_rsrc here, the writes are lost.
                    let refnum = self.next_refnum;
                    self.next_refnum += 1;
                    let rsrc_key = format!("__rsrc__{}", vfs_key);
                    self.vfs.entry(rsrc_key.clone()).or_insert(rsrc_data);
                    self.open_files.insert(refnum, rsrc_key);
                    self.file_positions.insert(refnum, 0);
                    bus.write_word(pb + 24, refnum);
                    bus.write_word(pb + 16, 0); // noErr
                    cpu.write_reg(Register::D0, 0);
                } else {
                    eprintln!("[TRAP] PBOpenRF: \"{}\" not found in vfs_rsrc", filename);
                    bus.write_word(pb + 16, (-43i16) as u16); // fnfErr
                    cpu.write_reg(Register::D0, (-43i32) as u32);
                }
                Ok(())
            }

            // ========== Pack8 / Apple Events Manager ($A816) ==========
            //
            // Selector-based dispatch. Per Apple's SuperMario ROM source
            // (Toolbox/AppleEventMgr/AEDFGlue.a), the AE Manager package
            // expects:
            //   D0.W high byte = number of WORDS of parameters
            //   D0.W low byte  = routine number (index into dispatch table)
            //
            // Stack layout on entry:
            //   SP+0 .. SP+(params*2-1) = parameters (last pushed first)
            //   SP+(params*2)           = result OSErr (2 bytes, pre-pushed by caller)
            //
            // After dispatch the convention from AEDFGlue's ExtensionProc
            // fallback is: pop the parameters, leave a 2-byte result at the
            // new SP. We mirror that here as a no-op stub, returning noErr.
            //
            // The dispatch table (AEDFGlue.a) starts:
            //   0: AE_InstallSpecialHandler   1: AE_RemoveSpecialHandler
            //   2: AE_CoercePtr               3: AE_CoerceDesc
            //   4: AE_DisposeDesc             5: AE_DuplicateDesc
            //   6: AE_CreateList              7: AE_CountItems
            //   ...
            //  25: AE_ResetTimer             27: AE_ProcessAppleEvent
            //   ... (52 routines total + 10 extension slots)
            // Pack8 / Apple Events ($A816): Selector-based; routine 31 (AEInstallEventHandler) records handlers and routine 27 (AEProcessAppleEvent) dispatches into them via a trampoline; other selectors are stubbed to pop their encoded args and return noErr in D0
            (true, 0x016) => {
                let sp = cpu.read_reg(Register::A7);
                let d0 = cpu.read_reg(Register::D0);
                let selector = (d0 & 0xFFFF) as u16;

                if selector == super::dispatch::LOADSEG_GETRESOURCE_SENTINEL {
                    return Some(self.resume_loadseg_after_getresource(bus, cpu));
                }

                // Trampoline selector — when an AE handler we dispatched
                // returns, its `RTD` lands on a tiny `MOVE.W #$FEFE, D0;
                // _Pack8` stub that re-enters Pack8 with this sentinel.
                // Resume the original `AEProcessAppleEvent` caller's flow.
                if selector == 0xFEFE {
                    let state = self
                        .ae_call_state
                        .take()
                        .expect("AE trampoline fired without a saved AeCallState");
                    // Sanity: handler's `RTD #12` should have left SP
                    // pointing at the original caller's result slot.
                    debug_assert_eq!(
                        sp, state.expected_sp_after_rtd,
                        "AE trampoline SP mismatch: expected {:08X}, got {:08X}",
                        state.expected_sp_after_rtd, sp,
                    );
                    // Resume at the post-`_Pack8` PC the original caller
                    // would have continued at. The result is already in
                    // the slot — the handler wrote it via the Pascal
                    // calling convention.
                    let result = bus.read_word(sp) as i16 as i32 as u32;
                    cpu.write_reg(Register::D0, result);
                    cpu.write_reg(Register::PC, state.return_pc);
                    return Some(Ok(()));
                }

                let routine = (selector & 0xFF) as u8;
                let param_words = ((selector >> 8) & 0xFF) as u32;
                let param_bytes = param_words * 2;

                static AE_LOG_COUNT: std::sync::atomic::AtomicU32 =
                    std::sync::atomic::AtomicU32::new(0);
                let lc = AE_LOG_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
                if lc < 20 || trace_ae_enabled() {
                    eprintln!(
                        "[TRAP] Pack8/AE PC=${:08X} SP=${:08X} D0=${:08X} routine={} param_words={} param_bytes={}",
                        cpu.read_reg(Register::PC),
                        sp,
                        d0,
                        routine,
                        param_words,
                        param_bytes
                    );
                    if trace_ae_enabled() {
                        let mut words = Vec::new();
                        for off in (0..=(param_bytes + 2).min(34)).step_by(2) {
                            words.push(format!("+{:02X}:{:04X}", off, bus.read_word(sp + off)));
                        }
                        eprintln!("[AE] stack {}", words.join(" "));
                    }
                }

                // Routine 31 (`AEInstallEventHandler`): record the
                // (eventClass, eventID) → (handler, refcon) tuple so a
                // later AEProcessAppleEvent dispatch can fire it. Stack
                // layout when the trap fires (Pascal calling order):
                //   SP+0   isSysHandler (Boolean, 2 bytes incl pad)
                //   SP+2   handlerRefcon (long, 4 bytes)
                //   SP+6   handler (AEEventHandlerUPP, 4 bytes)
                //   SP+10  theAEEventID (4-byte OSType)
                //   SP+14  theAEEventClass (4-byte OSType)
                //   SP+18  result OSErr slot (2 bytes; pre-pushed)
                // Inside Macintosh Volume VI, 6-43.
                if routine == 31 && param_bytes == 18 {
                    let handler_refcon = bus.read_long(sp + 2);
                    let handler_ptr = bus.read_long(sp + 6);
                    let event_id = bus.read_long(sp + 10);
                    let event_class = bus.read_long(sp + 14);
                    self.ae_handlers
                        .insert((event_class, event_id), (handler_ptr, handler_refcon));
                    eprintln!(
                        "[AE] InstallEventHandler class='{}' id='{}' handler=${:08X} refcon=${:08X}",
                        format_fourcc(event_class),
                        format_fourcc(event_id),
                        handler_ptr,
                        handler_refcon,
                    );
                }

                // Routine 18 (`AEGetParamDesc` / `AEGetKeyDesc`) and
                // routine 17 (`AEGetParamPtr` / `AEGetKeyPtr`) extract
                // AppleEvent parameters. The synthetic Open Application
                // event has no direct parameter, so missing parameters
                // must return `errAEDescNotFound`, not a successful
                // zero-filled result.
                //
                // AEGetParamDesc (0x0812)
                // FUNCTION AEGetParamDesc(theAppleEvent: AppleEvent;
                //   theAEKeyword: AEKeyword; desiredType: DescType;
                //   VAR result: AEDesc): OSErr;
                // AEGetParamPtr (0x0E11)
                // FUNCTION AEGetParamPtr(theAppleEvent: AppleEvent;
                //   theAEKeyword: AEKeyword; desiredType: DescType;
                //   VAR typeCode: DescType; dataPtr: Ptr;
                //   maximumSize: Size; VAR actualSize: Size): OSErr;
                // Inside Macintosh: Interapplication Communication, 4-68..4-70.
                if routine == 18 && param_bytes == 16 {
                    let result_desc = bus.read_long(sp);
                    write_null_aedesc(bus, result_desc);
                    let new_sp = sp + param_bytes;
                    bus.write_word(new_sp, AE_ERR_DESC_NOT_FOUND as u16);
                    cpu.write_reg(Register::A7, new_sp);
                    cpu.write_reg(Register::D0, AE_ERR_DESC_NOT_FOUND as i32 as u32);
                    return Some(Ok(()));
                }
                if routine == 17 && param_bytes == 28 {
                    let actual_size_ptr = bus.read_long(sp);
                    let type_code_ptr = bus.read_long(sp + 12);
                    if actual_size_ptr != 0 {
                        bus.write_long(actual_size_ptr, 0);
                    }
                    if type_code_ptr != 0 {
                        bus.write_long(type_code_ptr, AE_TYPE_NULL);
                    }
                    let new_sp = sp + param_bytes;
                    bus.write_word(new_sp, AE_ERR_DESC_NOT_FOUND as u16);
                    cpu.write_reg(Register::A7, new_sp);
                    cpu.write_reg(Register::D0, AE_ERR_DESC_NOT_FOUND as i32 as u32);
                    return Some(Ok(()));
                }

                // Routine 21 (`AEGetAttributePtr`) and routine 38
                // (`AEGetAttributeDesc`) expose AppleEvent attributes.
                // Every AppleEvent includes event class and event ID
                // attributes (`keyEventClassAttr`, `keyEventIDAttr`),
                // each stored as `typeType` four-character-code data.
                //
                // AEGetAttributePtr (0x0E15)
                // FUNCTION AEGetAttributePtr(theAppleEvent: AppleEvent;
                //   theAEKeyword: AEKeyword; desiredType: DescType;
                //   VAR typeCode: DescType; dataPtr: Ptr;
                //   maximumSize: Size; VAR actualSize: Size): OSErr;
                // AEGetAttributeDesc (0x0826)
                // FUNCTION AEGetAttributeDesc(theAppleEvent: AppleEvent;
                //   theAEKeyword: AEKeyword; desiredType: DescType;
                //   VAR result: AEDesc): OSErr;
                // Inside Macintosh: Interapplication Communication, 4-71..4-73.
                if routine == 21 && param_bytes == 28 {
                    let actual_size_ptr = bus.read_long(sp);
                    let maximum_size = bus.read_long(sp + 4);
                    let data_ptr = bus.read_long(sp + 8);
                    let type_code_ptr = bus.read_long(sp + 12);
                    let desired_type = bus.read_long(sp + 16);
                    let keyword = bus.read_long(sp + 20);
                    let event_desc = bus.read_long(sp + 24);
                    let attr_value = self.ae_events.get(&event_desc).and_then(|event| {
                        if keyword == AE_KEY_EVENT_CLASS_ATTR {
                            Some(event.event_class)
                        } else if keyword == AE_KEY_EVENT_ID_ATTR {
                            Some(event.event_id)
                        } else {
                            None
                        }
                    });
                    let err = if let Some(value) = attr_value {
                        if desired_type != AE_TYPE_WILDCARD && desired_type != AE_TYPE_TYPE {
                            AE_ERR_COERCION_FAIL
                        } else {
                            if type_code_ptr != 0 {
                                bus.write_long(type_code_ptr, AE_TYPE_TYPE);
                            }
                            if actual_size_ptr != 0 {
                                bus.write_long(actual_size_ptr, 4);
                            }
                            if maximum_size >= 4 && data_ptr != 0 {
                                bus.write_long(data_ptr, value);
                                0
                            } else {
                                AE_BUFFER_IS_SMALL
                            }
                        }
                    } else {
                        if type_code_ptr != 0 {
                            bus.write_long(type_code_ptr, AE_TYPE_NULL);
                        }
                        if actual_size_ptr != 0 {
                            bus.write_long(actual_size_ptr, 0);
                        }
                        AE_ERR_DESC_NOT_FOUND
                    };
                    let new_sp = sp + param_bytes;
                    bus.write_word(new_sp, err as u16);
                    cpu.write_reg(Register::A7, new_sp);
                    cpu.write_reg(Register::D0, err as i32 as u32);
                    return Some(Ok(()));
                }
                if routine == 38 && param_bytes == 16 {
                    let result_desc = bus.read_long(sp);
                    let desired_type = bus.read_long(sp + 4);
                    let keyword = bus.read_long(sp + 8);
                    let event_desc = bus.read_long(sp + 12);
                    let attr_value = self.ae_events.get(&event_desc).and_then(|event| {
                        if keyword == AE_KEY_EVENT_CLASS_ATTR {
                            Some(event.event_class)
                        } else if keyword == AE_KEY_EVENT_ID_ATTR {
                            Some(event.event_id)
                        } else {
                            None
                        }
                    });
                    let err = if let Some(value) = attr_value {
                        if desired_type != AE_TYPE_WILDCARD && desired_type != AE_TYPE_TYPE {
                            write_null_aedesc(bus, result_desc);
                            AE_ERR_COERCION_FAIL
                        } else {
                            let data = bus.alloc(4);
                            bus.write_long(data, value);
                            bus.write_long(result_desc, AE_TYPE_TYPE);
                            bus.write_long(result_desc + 4, data);
                            0
                        }
                    } else {
                        write_null_aedesc(bus, result_desc);
                        AE_ERR_DESC_NOT_FOUND
                    };
                    let new_sp = sp + param_bytes;
                    bus.write_word(new_sp, err as u16);
                    cpu.write_reg(Register::A7, new_sp);
                    cpu.write_reg(Register::D0, err as i32 as u32);
                    return Some(Ok(()));
                }

                // Routine 54 (`AEResolve`) resolves an AppleEvent
                // object specifier record into a token. Until Systemless
                // models object accessor dispatch, it must still avoid
                // the generic Pack8 success stub: non-object-specifier
                // input returns `errAENotAnObjectSpec`, and errors return
                // a null token.
                //
                // AEResolve (0x0536)
                // FUNCTION AEResolve(objectSpecifier: AEDesc;
                //   callbackFlags: Integer; VAR theToken: AEDesc): OSErr;
                // Inside Macintosh: Interapplication Communication,
                // 6-85..6-86 and 6-115.
                if routine == 54 && param_bytes == 10 {
                    let token_desc = bus.read_long(sp);
                    let object_specifier = bus.read_long(sp + 6);
                    let err = if object_specifier != 0
                        && bus.read_long(object_specifier) == AE_TYPE_OBJECT_SPECIFIER
                    {
                        AE_ERR_ACCESSOR_NOT_FOUND
                    } else {
                        AE_ERR_NOT_AN_OBJECT_SPEC
                    };
                    write_null_aedesc(bus, token_desc);
                    let new_sp = sp + param_bytes;
                    bus.write_word(new_sp, err as u16);
                    cpu.write_reg(Register::A7, new_sp);
                    cpu.write_reg(Register::D0, err as i32 as u32);
                    return Some(Ok(()));
                }

                // Routine 27 (`AEProcessAppleEvent`): dispatch the head
                // queued AE through its registered handler. Stack on
                // entry (param_bytes = 4):
                //   SP+0   theEventRecord ptr (4 bytes)
                //   SP+4   result OSErr slot (2 bytes)
                // We synthesize a kAEOpenApplication invocation whenever
                // the matching handler is registered. The OAPP path is
                // what unblocks `WaitForStartupEvent`-style splash gates
                // in apps that call `AEProcessAppleEvent` directly
                // instead of going through `WaitNextEvent`. Unlike the
                // older one-shot gate, repeated direct calls are allowed:
                // each `AEProcessAppleEvent` invocation can dispatch the
                // registered handler again.
                if routine == 27 && param_bytes == 4 {
                    let oapp_class = AE_TYPE_APPLE_EVENT;
                    let oapp_id = u32::from_be_bytes(*b"oapp");
                    if trace_ae_enabled() {
                        let event_record = bus.read_long(sp);
                        let what = bus.read_word(event_record);
                        let message = bus.read_long(event_record + 2);
                        let where_v = bus.read_word(event_record + 10);
                        let where_h = bus.read_word(event_record + 12);
                        eprintln!(
                            "[AE] ProcessAppleEvent EventRecord=${:08X} what={} message='{}' where='{}' mods=${:04X}",
                            event_record,
                            what,
                            format_fourcc(message),
                            format_fourcc(((where_v as u32) << 16) | where_h as u32),
                            bus.read_word(event_record + 14)
                        );
                    }
                    if let Some((handler_ptr, refcon)) =
                        self.apple_event_handler_for(oapp_class, oapp_id)
                    {
                        // Lazily allocate the trampoline on first use.
                        // Six bytes encode `MOVE.W #$FEFE, D0; _Pack8`,
                        // pad to 8 for alignment.
                        let trampoline = match self.ae_trampoline_addr {
                            Some(addr) => addr,
                            None => {
                                let addr = bus.alloc(8);
                                bus.write_word(addr, 0x303C); // MOVE.W #imm, D0
                                bus.write_word(addr + 2, 0xFEFE); // immediate
                                bus.write_word(addr + 4, 0xA816); // _Pack8
                                self.ae_trampoline_addr = Some(addr);
                                addr
                            }
                        };

                        // Build a minimal AppleEvent + reply pair on
                        // the heap. The AppleEvent descriptor carries
                        // typeAppleEvent and a non-NIL data handle;
                        // `ae_events` holds the attribute data that
                        // AEGetAttribute* exposes to the handler. The
                        // null reply descriptor matches the no-reply
                        // path for a synthetic open-application event.
                        let event_desc = bus.alloc(8);
                        let event_data = bus.alloc(8);
                        bus.write_long(event_data, oapp_class);
                        bus.write_long(event_data + 4, oapp_id);
                        bus.write_long(event_desc, AE_TYPE_APPLE_EVENT);
                        bus.write_long(event_desc + 4, event_data);
                        self.ae_events.insert(
                            event_desc,
                            crate::trap::dispatch::SyntheticAppleEvent {
                                event_class: oapp_class,
                                event_id: oapp_id,
                            },
                        );
                        let reply_desc = bus.alloc(8);
                        bus.write_long(reply_desc, AE_TYPE_NULL);
                        bus.write_long(reply_desc + 4, 0);

                        // Stack on entry has [event_ptr][result_slot]
                        // at [SP][SP+4]. We need the handler to see
                        // [trampoline][refcon][reply][event][result],
                        // so push three 4-byte words below the result
                        // slot and replace the original EventRecord
                        // pointer at SP+0 with the AppleEvent AEDesc.
                        let new_sp = sp.wrapping_sub(12);
                        bus.write_long(new_sp, trampoline); // return PC
                        bus.write_long(new_sp + 4, refcon);
                        bus.write_long(new_sp + 8, reply_desc);
                        bus.write_long(new_sp + 12, event_desc);
                        cpu.write_reg(Register::A7, new_sp);

                        // Save what we need to resume the original
                        // caller's flow. After the handler's
                        // `RTD #12`, SP will land at the result slot
                        // address (= original sp + 4).
                        let return_pc = cpu.read_reg(Register::PC);
                        self.ae_call_state = Some(crate::trap::dispatch::AeCallState {
                            return_pc,
                            expected_sp_after_rtd: sp + 4,
                        });
                        self.fired_oapp_handler = true;

                        eprintln!(
                            "[AE] ProcessAppleEvent → invoking 'oapp' handler ${:08X} via trampoline ${:08X}",
                            handler_ptr, trampoline,
                        );

                        cpu.write_reg(Register::PC, handler_ptr);
                        return Some(Ok(()));
                    }
                }

                // Pop parameters off the stack. The result word (noErr by
                // default) is left at the new SP, which is exactly the slot
                // the caller pre-reserved before pushing the parameters.
                let new_sp = sp + param_bytes;
                bus.write_word(new_sp, 0); // noErr
                cpu.write_reg(Register::A7, new_sp);
                cpu.write_reg(Register::D0, 0);
                Ok(())
            }

            // ========== Desk Accessories ==========
            //
            // The Desk Accessory family ($A9B2 SystemEvent / $A9B3
            // SystemClick / $A9B4 SystemTask / $A9B5 SystemMenu /
            // $A9B6 OpenDeskAcc / $A9B7 CloseDeskAcc / $A9C2
            // SystemEdit) handles classic Mac OS Desk Accessories —
            // small applets (Calculator, Alarm Clock, Note Pad,
            // Scrapbook, Chooser etc.) that ran in system windows
            // sharing the application's address space. Per IM:I
            // I-435..I-446 + Macintosh Toolbox Essentials 1992 chapter
            // 6 (Desk Manager) the family routes events between the
            // foreground app and the active DA (if any), services
            // periodic-action ticks via the Time Manager queue, and
            // installs/removes the DRVR-resource-backed driver code
            // into the Device Manager DCE chain.
            //
            // ## HLE compromise
            //
            // Systemless models no Desk Accessories at all. Concretely:
            //   - No DRVR resource loading / DCE chain mutation
            //     (would require Device Manager OpenDriver path which
            //     itself collapses to no-op in HLE — see $A000 Open
            //     plus $A001 Close in src/trap/event.rs).
            //   - No system window — every window in HLE is an
            //     application window with windowKind >= 0; DAs would
            //     have negative windowKind = -refNum per IM:I I-435.
            //   - No DA event dispatch — the active DA's `accEvent`
            //     control message ($A004 Control sub-call 64) would
            //     need a guest-fn dispatch infrastructure to invoke
            //     the DRVR's event-handling proc, which Systemless
            //     doesn't have (same compromise as ModalDialog
            //     filterProc / Alert filterProc / Pack1 LSearch
            //     searchProc / SndAddModifier modifier proc).
            //   - No Time Manager periodic-action queue — SystemTask
            //     would walk it for `dNeedTime`-flagged drivers and
            //     fire their `accRun` ($A004 Control sub-call 65),
            //     which Systemless never reaches because no DA is ever
            //     installed.
            //
            // Desk Accessory support remains absent:
            // System 7.5+ apps that ARE in the systemless-games corpus
            // (Marathon, Glider PRO, Bonkheads, Centaurian, Koji)
            // gate the DA family behind feature checks (Gestalt
            // 'os ' bit checks, app-prefs settings) and don't depend
            // on DA-driven side effects. Apps that DO depend on a DA
            // (Note Pad save-game integration, Calculator math
            // helper) are System-6-era and out of corpus scope.
            //
            // ## Per-trap return-value summary
            //
            // - $A9B2 SystemEvent (FUNCTION → BOOLEAN): returns FALSE
            //   per IM:I I-441 "If the active window does not belong
            //   to a desk accessory ... SystemEvent returns FALSE";
            //   Systemless's HLE has no DA-owned windows so every
            //   active window matches the FALSE branch.
            //   Implemented in resource.rs:1624..1636 (lives in the
            //   Resource Mgr dispatcher because of historical
            //   manager-classification — actual manager is Desk
            //   Mgr per IM:I I-441 + IM:I-435).
            //
            // - $A9B3 SystemClick (PROCEDURE): no-op pop 20 — apps
            //   call this only after FindWindow returns inSysWindow,
            //   which can never happen in HLE (every window is
            //   application-owned with windowKind >= 0 / userKind
            //   >= 8). Defensive no-op for any caller that bypasses
            //   the FindWindow gate.
            //
            // - $A9B4 SystemTask (PROCEDURE): no-op pop 0 — see arm
            //   at toolbox.rs:1050..1095 above.
            //
            // - $A9B5 SystemMenu (PROCEDURE): no-op pop 4 — apps
            //   call this when MenuSelect returns a negative menu
            //   ID (DA-owned menu); Systemless's MenuSelect never
            //   returns negative IDs since no DA ever calls
            //   InsertMenu(handle, hierMenu) for a negative-ID
            //   menu, so this trap is unreachable from corpus games
            //   but the no-op pop is defensive.
            //
            // - $A9B6 OpenDeskAcc (FUNCTION → INTEGER): returns 0
            //   per IM:I I-440 "if the desk accessory can't be
            //   opened, the function result is undefined"; Systemless
            //   chooses 0 as the sentinel "couldn't open" value.
            //   IM also explicitly says "You should ignore the
            //   value returned by OpenDeskAcc" — apps that DO check
            //   the return and branch on != 0 are technically out
            //   of contract but the FALSE path is harmless.
            //
            // - $A9B7 CloseDeskAcc (PROCEDURE): no-op pop 2 — apps
            //   call this from File→Close when the active window's
            //   windowKind is negative (DA window). Since no
            //   Systemless window has negative windowKind, this path
            //   is unreachable from corpus games.
            //
            // - $A9C2 SystemEdit (FUNCTION → BOOLEAN): returns
            //   FALSE per IM:I I-441 "if the active window does not
            //   belong to a desk accessory ... SystemEdit returns
            //   FALSE so that your application will perform the
            //   editing function on its own document". Apps
            //   universally call this from menu-cmd dispatch on
            //   Cut/Copy/Paste/Clear/Undo — the FALSE return
            //   correctly says "no DA wants this; do your own
            //   editing".
            //
            // ## Status
            //
            // All 7 traps remain Stub (FUNCTION-returning-hardcoded-
            // value) or Stub (no-op) (PROCEDURE) per the established
            // status-table distinction (no Status promotion this
            // iteration — implementation bodies were already correct).
            // The bookkeeping cleanup is documentation + manager-
            // classification fixes + register-preservation invariants.

            // OpenDeskAcc ($A9B6)
            // Per IM:I 1985, p. I-440:
            //   FUNCTION OpenDeskAcc (theAcc: Str255) : INTEGER;
            //
            // "OpenDeskAcc opens the desk accessory having the given
            // name and displays its window (if any) as the active
            // window. ... You should ignore the value returned by
            // OpenDeskAcc. If the desk accessory is successfully
            // opened, the function result is its driver reference
            // number. However, if the desk accessory can't be opened,
            // the function result is undefined; the accessory will
            // have taken care of informing the user of the problem
            // (such as memory full) and won't display itself."
            //
            // Calling convention (Tool-bit FUNCTION per IM:I I-440):
            //   Stack on entry: SP+0 = theAcc Str255 ptr (4 bytes —
            //                          pointer to Pascal length-
            //                          prefixed name string),
            //                   SP+4 = INTEGER result placeholder
            //                          (2 bytes, pre-pushed by caller).
            //   Trap pops the 4-byte Str255 pointer and writes the
            //   INTEGER result to [SP+0] after pop (i.e. the original
            //   SP+4 slot). Net stack effect after the caller's
            //   epilogue reads the result is zero — A7 returns to its
            //   pre-call value (engines-agree per Pascal FUNCTION
            //   calling convention).
            //
            // MPW Universal Headers Devices.h (Desk.h is deprecated;
            // the Desk Manager routines moved to Devices.h after
            // System 7 — fixtures must include Menus.h + Devices.h +
            // Events.h instead of Desk.h):
            //   EXTERN_API(short) OpenDeskAcc (ConstStr255Param)
            //     ONEWORDINLINE(0xA9B6);
            //
            // Engines-agree subset (witnessed by the
            // a9b6_a9b7_opendeskacc_closedeskacc_strict bake):
            //   - Pop 4-byte Str255 pointer argument
            //   - Write the 2-byte INTEGER result slot at [SP+4]
            //     (the value is engines-divergent — Systemless writes 0;
            //     BasiliskII writes an undefined refNum per IM —
            //     but BOTH engines write SOMETHING, so A7 returns to
            //     its pre-call value after the caller's epilogue.)
            //
            // Engines-divergent (NOT witnessed):
            //   - Absolute INTEGER result value. Per IM:I I-440 the
            //     return value is "undefined" when the DA can't be
            //     opened, so Systemless's 0-sentinel and BII's RTC/heap-
            //     dependent value both satisfy the IM contract.
            //
            // Systemless HLE behavior: has no DRVR loading / DCE chain
            // so every open fails — IM:I I-440 explicitly: "You
            // should ignore the value returned by OpenDeskAcc" so
            // the 0-sentinel is a safe defensive default.
            //
            // Catalogue-proof:
            //   a9b6_a9b7_opendeskacc_closedeskacc_strict
            //   - A9B6:opendeskacc_consumes_name_pointer_and_preserves_stack_pointer
            // Contract tests (in src/trap/toolbox.rs `mod tests`):
            //   - opendeskacc_consumes_name_pointer_arg_and_writes_result_slot
            //   - opendeskacc_five_call_composition_preserves_stack_pointer
            (true, 0x1B6) => {
                let sp = cpu.read_reg(Register::A7);
                bus.write_word(sp + 4, 0); // return 0 (no DA opened)
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // CloseDeskAcc ($A9B7)
            // Per IM:I 1985, p. I-440:
            //   PROCEDURE CloseDeskAcc (refNum: INTEGER);
            //
            // "When a system window is active and the user chooses
            // Close from the File menu, call CloseDeskAcc to close
            // the desk accessory. RefNum is the driver reference
            // number for the desk accessory, which you get from the
            // windowKind field of its window. ... The Desk Manager
            // automatically closes a desk accessory if the user
            // clicks its close box. Also, since the application heap
            // is released when the application terminates, every
            // desk accessory goes away at that time."
            //
            // Calling convention (Tool-bit PROCEDURE per IM:I I-440):
            //   Stack on entry: SP+0 = refNum INTEGER (2 bytes).
            //   Trap pops the 2-byte argument. No result slot.
            //   Net stack effect: A7 advances by exactly 2 bytes;
            //   no further caller epilogue is needed (engines-agree
            //   per Pascal PROCEDURE calling convention).
            //
            // MPW Universal Headers Devices.h:
            //   EXTERN_API(void) CloseDeskAcc (short refNum)
            //     ONEWORDINLINE(0xA9B7);
            //
            // Engines-agree subset (witnessed by the
            // a9b6_a9b7_opendeskacc_closedeskacc_strict bake):
            //   - Pop 2-byte INTEGER refNum argument
            //   - No result slot written
            //   - When refNum=0 (clearly invalid — DA refnums are
            //     negative on a real Mac), both engines walk the DCE
            //     chain, find no matching entry, and return without
            //     effect (the documented "no action is taken" path).
            //
            // Systemless HLE behavior: has no DCE chain / DRVR loading
            // so no DA window can ever be active — windowKind >= 0
            // for all Systemless windows. The trap is a defensive no-op
            // for any caller that bypasses the windowKind < 0 gate.
            //
            // Catalogue-proof:
            //   a9b6_a9b7_opendeskacc_closedeskacc_strict
            //   - A9B7:closedeskacc_consumes_refnum_and_preserves_stack_pointer
            // Contract tests (in src/trap/toolbox.rs `mod tests`):
            //   - closedeskacc_consumes_refnum_arg_and_writes_no_result
            //   - closedeskacc_five_call_composition_advances_stack_by_ten
            (true, 0x1B7) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // SystemClick ($A9B3)
            // Per IM:I I-441: "When a mouse-down event occurs and
            // the Window Manager function FindWindow reports that
            // the mouse button was pressed in a system window, the
            // application should call SystemClick with the event
            // record and the window pointer. If the given window
            // belongs to a desk accessory, SystemClick sees that
            // the event gets handled properly."
            // PROCEDURE SystemClick(theEvent: EventRecord;
            //                       theWindow: WindowPtr);
            // Inside Macintosh Volume I, I-441
            //
            // Stack: SP+0 theEvent EventRecord by VALUE (16 bytes —
            // confirmed in IM:I I-251 EventRecord layout: what(2) +
            // message(4) + when(4) + where Point(4) + modifiers(2) =
            // 16 bytes), SP+16 theWindow WindowPtr (4 bytes). Pop 20.
            // No result (PROCEDURE). HLE no-op because FindWindow
            // never returns inSysWindow (negative windowKind doesn't
            // exist), so this trap is unreachable from corpus games
            // following the documented FindWindow → SystemClick gate.
            // SystemClick ($A9B3): Pops 20 bytes (EventRecord 16 by VALUE + WindowPtr 4) per IM:I I-441 PROCEDURE sig + IM:I I-251 EventRecord layout; HLE no-op since FindWindow never returns inSysWindow per IM:I I-435 windowKind convention — defensive no-op for any caller that bypasses the FindWindow gate.
            (true, 0x1B3) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 20);
                Ok(())
            }

            // SystemMenu ($A9B5)
            // PROCEDURE SystemMenu(menuResult: LONGINT);
            // Inside Macintosh Volume I, I-441
            //
            // Per IM:I 1985 p. I-441 verbatim: "SystemMenu is called
            // only by the Menu Manager functions MenuSelect and
            // MenuKey, when an item in a menu belonging to a desk
            // accessory has been chosen. The menuResult parameter has
            // the same format as the value returned by MenuSelect and
            // MenuKey: the menu ID in the high-order word and the
            // menu item number in the low-order word. (The menu ID
            // will be negative.) SystemMenu directs the desk
            // accessory to perform the appropriate action for the
            // given menu item."
            //
            // IM:I 1985 p. I-441 also notes: "The two remaining Desk
            // Manager routines — SystemEvent and SystemMenu — are
            // never called by the application, but are described in
            // this chapter because they reveal inner mechanisms of
            // the Toolbox that may be of interest to advanced
            // programmers." Application code reaches SystemMenu only
            // via MenuSelect/MenuKey's internal dispatch when the
            // user picks an item from a DA-owned menu (menuID
            // negative).
            //
            // Tool-bit PROCEDURE ABI: caller pushes a 4-byte LONGINT
            // menuResult argument on the stack and dispatches the
            // trap word; the trap pops the 4-byte argument and
            // returns with no result slot write. A7 net-effect: SP
            // advances by 4 bytes across the call.
            //
            // MPW Universal Headers do not declare SystemMenu — the
            // trap is reachable only through the Menu Manager
            // dispatch, never as a direct C call. A fixture wishing
            // to dispatch the trap word directly declares a local
            // Pascal-calling-convention thunk via
            // `pascal void SystemMenu_trap(long menuResult) = {0xA9B5};`.
            //
            // Systemless HLE behavior: pop 4 bytes from A7 and return.
            // The DA-menu-action side effect is unimplementable in
            // Systemless because the HLE models no Desk Accessories.
            // The engines-agree subset is the Pascal PROCEDURE stack
            // discipline (pop-4 with no result slot write).
            //
            // Engines-agree alignment per IM:I 1985 p. I-441:
            //   - Pascal PROCEDURE: no result slot; A7 advances by
            //     argument byte count (4 bytes for a LONGINT).
            //   - With menuResult=0, both engines walk the DCE chain
            //     looking for a DA owning a menu with menuID=0, find
            //     none (real DA menus have negative menuIDs per
            //     I-441), and return per the documented no-DA path.
            //
            // Catalogue-proof:
            //   a9b5_systemmenu_strict — BasiliskII
            //   strict bake of A9B5 SystemMenu witnessing PROCEDURE
            //   stack discipline via a single + 5-call composition
            //   StackSpace sandwich.
            //
            // Contract tests in this file:
            //   - systemmenu_procedure_call_pops_four_bytes_from_stack
            //   - systemmenu_five_call_composition_advances_stack_by_twenty
            (true, 0x1B5) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // SystemEdit ($A9C2)
            // Per IM:I I-441: "Call SystemEdit when there's a
            // mouse-down event in the menu bar and the user chooses
            // one of the five standard editing commands from the
            // Edit menu. ... If the active window does not belong
            // to a desk accessory ... SystemEdit returns FALSE so
            // that your application will perform the editing
            // function on its own document."
            // FUNCTION SystemEdit(editCmd: INTEGER): BOOLEAN;
            // Inside Macintosh Volume I, I-441
            //
            // Calling convention (Tool-bit FUNCTION per IM:I I-441):
            //   Stack on entry: SP+0 = editCmd INTEGER (2 bytes),
            //                   SP+2 = BOOLEAN result placeholder
            //                          (2 bytes, pre-pushed by caller).
            //   Trap pops the 2-byte editCmd and writes the BOOLEAN
            //   result to [SP+0] after pop (i.e. the original SP+2
            //   slot). Net stack effect after the caller's epilogue
            //   reads the result is zero — A7 returns to its pre-call
            //   value (engines-agree per Pascal FUNCTION calling
            //   convention).
            //
            // Standard editCmd values per the IM:I I-441 table:
            //   0  undoCmd
            //   2  cutCmd
            //   3  copyCmd
            //   4  pasteCmd
            //   5  clearCmd
            // (1 is a historic gap.)
            //
            // MPW Universal Headers Desk.h:
            //   EXTERN_API(Boolean) SystemEdit(short editCmd)
            //     ONEWORDINLINE(0xA9C2);
            //
            // Assembly-language note (IM:I I-441): "The macro you
            // invoke to call SystemEdit from assembly language is
            // named _SysEdit." — same trap word ($A9C2), MPW glue
            // just reuses the alias.
            //
            // HLE compromise: Systemless models no Desk Accessories so
            // no DA window is ever active. Per IM:I I-441 the FALSE
            // return is the documented "no DA wants this; app should
            // perform the edit on its own document" path — corpus
            // apps' Cut/Copy/Paste menu handlers correctly fall
            // through to their own document-editing code.
            //
            // Engines-agree subset (per a9b4_a9c2_systemtask_systemedit_strict):
            //   - Pascal FUNCTION calling convention with trap-side
            //     2-byte editCmd pop; A7 returns to its pre-call value
            //   - BOOLEAN result == FALSE (0) for every standard
            //     editCmd value (0/2/3/4/5) on the no-DA-owns-active-
            //     window path
            //
            // Catalogue-proof: a9b4_a9c2_systemtask_systemedit_strict
            //   B2: A9C2:systemedit_consumes_editcmd_and_returns_false_boolean_result
            //
            // Contract tests:
            //   - systemedit_consumes_editcmd_and_returns_false_boolean_result (copyCmd)
            //   - systemedit_returns_false_for_every_standard_editcmd
            (true, 0x1C2) => {
                let sp = cpu.read_reg(Register::A7);
                bus.write_word(sp + 2, 0); // return FALSE
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // ========== Scrap Manager ==========

            // InfoScrap ($A9F9)
            // Returns a pointer to a ScrapStuff record describing the desk scrap.
            // FUNCTION InfoScrap: PScrapStuff;
            // Inside Macintosh Volume I, I-457
            //
            // Regression coverage:
            //   a9f9_infoscrap_strict
            //   src/trap/toolbox.rs::tests::infoscrap_reports_in_memory_scrapstate_and_entry_size
            //   src/trap/toolbox.rs::tests::infoscrap_scraphandle_serializes_current_entries
            //
            // InfoScrap ($A9F9): Returns pointer to ScrapStuff record
            // (scrapSize, scrapHandle, scrapCount, scrapState, scrapName)
            // and exposes a live in-memory desk-scrap handle when
            // scrapState is positive per IM:I I-457. When the scrap
            // has been unloaded, scrapHandle is NIL and scrapState is 0
            // until LoadScrap/ZeroScrap marks it resident again.
            (true, 0x1F9) => {
                let sp = cpu.read_reg(Register::A7);
                // Allocate ScrapStuff at a fixed location if not yet done
                let scrap_stuff_ptr = self.scrap_stuff_ptr.get_or_insert_with(|| bus.alloc(16));
                let ptr = *scrap_stuff_ptr;
                let total_size = self.serialized_scrap_size();
                let scrap_handle = if self.scrap_in_memory {
                    self.sync_scrap_handle(bus)
                } else {
                    0
                };
                bus.write_long(ptr, total_size); // scrapSize
                bus.write_long(ptr + 4, scrap_handle); // scrapHandle (live in-memory desk scrap)
                bus.write_word(ptr + 8, self.scrap_count as u16); // scrapCount
                                                                  // IM:I I-457: scrapState is positive when the scrap is in memory.
                bus.write_word(ptr + 10, if self.scrap_in_memory { 1 } else { 0 });
                bus.write_long(ptr + 12, 0); // scrapName (NIL)
                bus.write_long(sp, ptr); // return value
                Ok(())
            }

            // UnloadScrap ($A9FA)
            // Writes the desk scrap from memory to the scrap file and
            // releases the memory it occupied.
            // FUNCTION UnloadScrap : LONGINT;
            // Inside Macintosh Volume I (1985), p. I-458.
            //
            // Tool Trap (bit 11 of the trap word is set) with Pascal
            // calling convention: 0 argument bytes, 4-byte LONGINT
            // OSStatus result written to [SP+0]. MPW Universal Headers
            // Scrap.h:
            //   EXTERN_API(OSStatus) UnloadScrap(void) ONEWORDINLINE(0xA9FA);
            // The assembly macro name is `_UnlodeScrap` per IM:I I-458
            // (legacy Pascal-source spelling); the trap word $A9FA is
            // unchanged across spellings.
            //
            // Per IM:I I-458 the documented success path is:
            //   "If the desk scrap is already on the disk, UnloadScrap
            //    does nothing. If no error occurs, UnloadScrap returns
            //    the result code noErr".
            //
            // Systemless HLE models the observable resident/on-disk
            // transition: the in-memory scrap handle is dropped and
            // InfoScrap reports scrapState=0 until LoadScrap brings it
            // back. `scrap_clipboard_writable` gates the observable
            // error path when the scrap is resident but cannot be
            // written out.
            //
            // Regression coverage:
            //   src/trap/toolbox.rs::tests::unloadscrap_and_loadscrap_return_noerr
            (true, 0x1FA) => {
                let sp = cpu.read_reg(Register::A7);
                if self.scrap_in_memory && !self.scrap_clipboard_writable {
                    bus.write_long(sp, (-1i32) as u32); // generic non-zero OSErr
                } else if self.scrap_in_memory {
                    self.scrap_in_memory = false;
                    self.scrap_handle_dirty = true;
                    if let Some(handle) = self.scrap_handle.take() {
                        let _ = self.write_bytes_to_handle(bus, handle, &[]);
                        bus.free(handle);
                    }
                    bus.write_long(sp, 0); // noErr (Systemless HLE: no scrap-file IO)
                } else {
                    bus.write_long(sp, 0); // already on disk; noErr
                }
                Ok(())
            }

            // LoadScrap ($A9FB)
            // Reads the desk scrap from the scrap file into memory.
            // FUNCTION LoadScrap : LONGINT;
            // Inside Macintosh Volume I (1985), p. I-458.
            //
            // Tool Trap (bit 11 of the trap word is set) with Pascal
            // calling convention: 0 argument bytes, 4-byte LONGINT
            // OSStatus result written to [SP+0]. MPW Universal Headers
            // Scrap.h:
            //   EXTERN_API(OSStatus) LoadScrap(void) ONEWORDINLINE(0xA9FB);
            // The assembly macro name is `_LodeScrap` per IM:I I-458
            // (legacy Pascal-source spelling); the trap word $A9FB is
            // unchanged.
            //
            // Per IM:I I-458 the documented success path is:
            //   "If the desk scrap is already in memory, it does
            //    nothing. If no error occurs, LoadScrap returns the
            //    result code noErr".
            //
            // Systemless HLE marks the scrap resident again after an
            // unload so InfoScrap can lazily recreate the in-memory
            // handle on demand. On a freshly booted system the scrap
            // is already resident, so the nominal noErr path remains
            // an in-memory no-op.
            //
            // Witnessed by:
            //   a9fb_loadscrap_strict
            //     (A9FB:loadscrap_returns_noerr_when_no_error)
            //   src/trap/toolbox.rs::tests::unloadscrap_and_loadscrap_return_noerr
            //   src/trap/toolbox.rs::tests::loadscrap_writes_noerr_to_pascal_function_result_slot_and_preserves_stack_pointer
            (true, 0x1FB) => {
                let sp = cpu.read_reg(Register::A7);
                if !self.scrap_in_memory {
                    self.scrap_in_memory = true;
                    self.scrap_handle_dirty = true;
                }
                bus.write_long(sp, 0); // noErr
                Ok(())
            }

            // ZeroScrap ($A9FC)
            // Clears the desk scrap and increments the scrap change count.
            // FUNCTION ZeroScrap: LONGINT;
            // Inside Macintosh Volume I, I-458
            //
            // Regression coverage:
            //   src/trap/toolbox.rs::tests::zeroscrap_clears_contents_and_changes_scrapcount
            //   src/trap/toolbox.rs::tests::infoscrap_reports_in_memory_scrapstate_and_entry_size
            // ZeroScrap ($A9FC): Clears scrap entries and increments scrap_count per IM:I I-458
            (true, 0x1FC) => {
                let sp = cpu.read_reg(Register::A7);
                self.scrap_entries.clear();
                self.scrap_count = self.scrap_count.wrapping_add(1);
                self.scrap_in_memory = true;
                self.scrap_handle_dirty = true;
                bus.write_long(sp, 0); // noErr
                Ok(())
            }

            // GetScrap ($A9FD)
            // Reads data of the specified type from the desk scrap.
            // FUNCTION GetScrap(hDest: Handle; theType: ResType; VAR offset: LONGINT): LONGINT;
            // Inside Macintosh Volume I, I-458
            //
            // Returns the length of the data (positive) on success, or a negative
            // error code. If hDest is NIL (0), returns the size and offset without
            // copying data. If the requested type is not found, returns noTypeErr (-102).
            //
            // Regression coverage:
            //   src/trap/toolbox.rs::tests::getscrap_missing_type_returns_notypeerr
            //   src/trap/toolbox.rs::tests::getscrap_with_nil_handle_returns_length_and_data_offset
            //   src/trap/toolbox.rs::tests::getscrap_duplicate_type_returns_first_occurrence
            //   src/trap/toolbox.rs::tests::getscrap_existing_handle_resizes_copy_and_preserves_ownership
            // GetScrap ($A9FD): Reads scrap data by type; supports NIL handle query; returns noTypeErr (-102) if not found per IM:I I-458
            (true, 0x1FD) => {
                let sp = cpu.read_reg(Register::A7);
                let offset_ptr = bus.read_long(sp); // VAR offset: LONGINT
                let the_type = bus.read_long(sp + 4).to_be_bytes(); // theType: ResType
                let h_dest = bus.read_long(sp + 8); // hDest: Handle

                // Search scrap for matching type. Per IM:I-459 offset is
                // the byte offset of the DATA (not the entry header) from
                // the start of the scrap. Each entry is laid out as:
                // type(4) + length(4) + data + pad-to-even. So for entry
                // N the data offset is sum(8 + padded_len_i for i<N) + 8.
                let mut found_offset: u32 = 0;
                let mut found = None;
                for entry in &self.scrap_entries {
                    if entry.0 == the_type {
                        found = Some(entry.1.clone());
                        found_offset += 8; // skip the matched entry's own header
                        break;
                    }
                    // Offset accounts for type(4) + length(4) + data (padded to even)
                    let padded_len = (entry.1.len() as u32 + 1) & !1;
                    found_offset += 8 + padded_len;
                }

                match found {
                    Some(data) => {
                        let data_len = data.len() as u32;
                        // Write offset
                        if offset_ptr != 0 {
                            bus.write_long(offset_ptr, found_offset);
                        }
                        // If hDest is not NIL, copy data into it
                        if h_dest != 0
                            && self.write_bytes_to_handle(bus, h_dest, &data) == 0
                            && data_len != 0
                        {
                            bus.write_long(sp + 12, (-108i32) as u32); // memFullErr
                            cpu.write_reg(Register::A7, sp + 12);
                            return Some(Ok(()));
                        }
                        // Return length (positive = success)
                        bus.write_long(sp + 12, data_len);
                    }
                    None => {
                        // noTypeErr = -102
                        bus.write_long(sp + 12, (-102i32) as u32);
                    }
                }
                cpu.write_reg(Register::A7, sp + 12);
                Ok(())
            }

            // PutScrap ($A9FE)
            // Writes data of the specified type to the desk scrap.
            // FUNCTION PutScrap(length: LONGINT; theType: ResType; source: Ptr): LONGINT;
            // Inside Macintosh Volume I, I-459
            //
            // Must be called after ZeroScrap. Appends data of the given type.
            //
            // Regression coverage:
            //   src/trap/toolbox.rs::tests::infoscrap_reports_in_memory_scrapstate_and_entry_size
            //   src/trap/toolbox.rs::tests::getscrap_duplicate_type_returns_first_occurrence
            // PutScrap ($A9FE): Appends type+data to scrap_entries per IM:I I-459
            (true, 0x1FE) => {
                let sp = cpu.read_reg(Register::A7);
                let source = bus.read_long(sp); // source: Ptr
                let the_type = bus.read_long(sp + 4).to_be_bytes(); // theType: ResType
                let length = bus.read_long(sp + 8) as i32; // length: LONGINT

                if length > 0 && source != 0 {
                    let mut data = vec![0u8; length as usize];
                    for (i, byte) in data.iter_mut().enumerate() {
                        *byte = bus.read_byte(source + i as u32);
                    }
                    self.scrap_entries.push((the_type, data));
                    self.scrap_handle_dirty = true;
                }

                bus.write_long(sp + 12, 0); // noErr
                cpu.write_reg(Register::A7, sp + 12);
                Ok(())
            }

            // ========== Resource Manager extras ==========

            // SetResPurge ($A993)
            // Installs or removes a Memory Manager hook that writes modified
            // resources to disk before purging.
            // PROCEDURE SetResPurge(install: BOOLEAN);
            // Inside Macintosh Volume I, I-126
            //
            // Regression coverage:
            //   tests::setrespurge_consumes_boolean_argument
            //   tests::setrespurge_toggles_resource_purge_install_flag
            // SetResPurge ($A993): Stores install flag in res_purge per IM:I I-126
            (true, 0x193) => {
                let sp = cpu.read_reg(Register::A7);
                // MPW passes a Pascal Boolean in the high byte of this
                // stack word. The low byte is padding and can be non-zero;
                // reading the whole word would turn SetResPurge(FALSE) into
                // TRUE.
                let install = (bus.read_word(sp) >> 8) != 0;
                self.res_purge = install;
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // SetResLoad ($A99B)
            // Enables or disables automatic loading of resources.
            // PROCEDURE SetResLoad(load: BOOLEAN);
            // Inside Macintosh: More Macintosh Toolbox 1993, 1-79 to 1-80
            //
            // Regression coverage:
            //   setresload_toggles_autoload
            //   setresload_true_enables
            //   setresload_false_disables
            // SetResLoad ($A99B): Stores load flag in res_load per MMTB 1-79; resource-returning helpers consume it to return empty handles until LoadResource.
            (true, 0x19B) => {
                let sp = cpu.read_reg(Register::A7);
                // MPW passes a Pascal Boolean in the high byte of this
                // stack word. The low byte is padding and can be non-zero;
                // reading the whole word turns SetResLoad(FALSE) into TRUE.
                let load = (bus.read_word(sp) >> 8) != 0;
                self.res_load = load;
                // Clear ResErr on success — real ROM does, and callers
                // that probe ResError after a successful SetResLoad
                // otherwise see stale values from boot-time auto-loads.
                bus.write_word(0x0A60, 0);
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // GetIndResource ($A99D) and Get1IndResource ($A80E)
            // FUNCTION GetIndResource  (theType: ResType; index: INTEGER): Handle;
            // FUNCTION Get1IndResource (theType: ResType; index: INTEGER): Handle;
            // Inside Macintosh Volume I, I-116; Volume IV, IV-14 to IV-15.
            //
            // The two traps share a Pascal signature but differ on which
            // resource files they walk:
            //   $A99D GetIndResource  — full search chain (current file + all
            //                            files opened before it).
            //   $A80E Get1IndResource — current resource file only. The
            //                            assembly macro is _Get1IxResource;
            //                            see IM:IV-15 "Assembly-language note".
            //
            // Aliasing them onto the full-chain implementation silently
            // over-counts in multi-file scenarios — the regression flagged
            // by the previous Ralph iteration. Keep them separate.
            // GetIndResource ($A99D): Walks the full resource search chain by type, returns Nth resource handle (1-based) per IM:I I-116
            (true, 0x19D) => self.handle_get_ind_resource(bus, cpu, false),

            // Get1IndResource ($A80E): Returns Nth resource of theType in the CURRENT resource file only (assembly name _Get1IxResource) per IM:IV-15
            (true, 0x00E) => self.handle_get_ind_resource(bus, cpu, true),

            // CountTypes ($A99E)
            // Returns the number of unique resource types across all open resource files.
            // FUNCTION CountTypes: INTEGER;
            // Inside Macintosh Volume I, I-117
            //
            // Regression coverage:
            //   counttypes_returns_type_count
            //   counttypes_returns_zero_with_no_resources
            // CountTypes ($A99E): Returns count of unique resource types across all open resource files per IM:I I-117
            (true, 0x19E) => {
                let sp = cpu.read_reg(Register::A7);
                let count = if let Some(ref resources) = self.resources {
                    let mut types = std::collections::HashSet::new();
                    for refnum in self.resource_search_order() {
                        if let Some(file) = resources.files.get(&refnum) {
                            for (res_type, _) in file.loaded.keys() {
                                types.insert(*res_type);
                            }
                        }
                    }
                    types.len() as u16
                } else {
                    0
                };
                bus.write_word(sp, count);
                Ok(())
            }

            // GetIndType ($A99F)
            // Returns the Nth unique resource type from all open resource files.
            // PROCEDURE GetIndType(VAR theType: ResType; index: INTEGER);
            // Inside Macintosh Volume I, I-117
            //
            // Index is 1-based. If out of range, writes four NUL bytes.
            //
            // Regression coverage:
            //   getindtype_returns_type_by_index
            //   getindtype_out_of_range_returns_nul
            // GetIndType ($A99F): Returns Nth unique resource type (1-based) via VAR theType ptr; writes four NUL bytes when index out of range per IM:I I-117
            (true, 0x19F) => self.handle_get_ind_type(bus, cpu, false),

            // Get1IndType ($A80F)
            // Returns the Nth unique resource type in the CURRENT resource
            // file only — the "1" sibling of GetIndType ($A99F) which spans
            // the full open-file chain.
            // PROCEDURE Get1IndType(VAR theType: ResType; index: INTEGER);
            // Inside Macintosh Volume IV, IV-15
            //
            // Assembly-language note (IM:IV-15): the assembly macro is
            // _Get1IxType, hence the otherwise-puzzling trap-word slot.
            //
            // Aliasing this onto $A99F silently leaks types from other
            // open resource files into the index — see the regression-fix
            // commit for Get1IndResource ($A80E) which addressed the
            // identical bug for handles.
            //
            // Regression coverage:
            //   get1indtype_returns_nth_type_in_current_file
            //   get1indtype_out_of_range_returns_nul_bytes
            //   get1indtype_ignores_types_in_other_open_files
            // Get1IndType ($A80F): Returns Nth unique resource type in current resource file only (assembly name _Get1IxType) per IM:IV-15.
            (true, 0x00F) => self.handle_get_ind_type(bus, cpu, true),

            // Count1Types ($A81C)
            // Returns the number of unique resource types in the current resource
            // file only — the "1" sibling of CountTypes ($A99E) which spans the
            // whole open-resource-file chain.
            // FUNCTION Count1Types: INTEGER;
            // Inside Macintosh: More Macintosh Toolbox 1993, 1-102
            //
            // Stack frame (Pascal, no args, INTEGER result):
            //   SP+0  result slot (2 bytes, caller-allocated)
            // Post-call SP is unchanged — the result word stays where the
            // caller already reserved it.
            //
            // Regression coverage:
            //   count1types_returns_zero_with_no_resources_in_current_file
            //   count1types_counts_unique_types_in_current_file
            //   count1types_pops_no_args_leaves_word_result
            // Count1Types ($A81C): Counts unique types in current resource file only per IM:MTb 1-102.
            (true, 0x01C) => {
                let sp = cpu.read_reg(Register::A7);
                let count = if let Some(ref resources) = self.resources {
                    let refnum = self.current_resource_refnum();
                    resources
                        .files
                        .get(&refnum)
                        .map(|file| {
                            let mut types = std::collections::HashSet::new();
                            for (res_type, _) in file.loaded.keys() {
                                types.insert(*res_type);
                            }
                            types.len() as u16
                        })
                        .unwrap_or(0)
                } else {
                    0
                };
                bus.write_word(sp, count);
                Ok(())
            }

            // GetNamedResource ($A9A1)
            // Returns a handle to the named resource, searching the resource chain.
            // FUNCTION GetNamedResource(theType: ResType; name: Str255): Handle;
            // More Macintosh Toolbox 1993, 1-75
            // GetNamedResource ($A9A1): Searches the resource chain by Pascal name string
            (true, 0x1A1) => {
                let sp = cpu.read_reg(Register::A7);
                let name_ptr = bus.read_long(sp);
                let raw_res_type = bus.read_long(sp + 4).to_be_bytes();
                let res_type = super::TrapDispatcher::normalize_ostype(raw_res_type);
                let type_str = std::str::from_utf8(&res_type).unwrap_or("????");
                let name_len = bus.read_byte(name_ptr) as usize;
                let mut name_bytes = vec![0u8; name_len];
                for (i, byte) in name_bytes.iter_mut().enumerate() {
                    *byte = bus.read_byte(name_ptr + 1 + i as u32);
                }
                let name = String::from_utf8_lossy(&name_bytes).to_string();
                eprintln!("[TRAP] GetNamedResource('{}', \"{}\")", type_str, name);

                let handle =
                    self.find_named_resource_any(res_type, &name)
                        .map(|(refnum, id, ptr)| {
                            self.get_or_create_resource_handle_in_file(
                                bus, res_type, id, ptr, refnum,
                            )
                        });

                if let Some(handle) = handle {
                    eprintln!("[TRAP] GetNamedResource -> handle ${:08X}", handle);
                    bus.write_word(0x0A60, 0); // ResErr = noErr
                    cpu.write_reg(Register::D0, 0);
                    bus.write_long(sp + 8, handle);
                    cpu.write_reg(Register::A7, sp + 8);
                } else {
                    eprintln!("[TRAP] GetNamedResource -> NULL (not found)");
                    bus.write_word(0x0A60, (-192i16) as u16); // ResErr = resNotFound
                    cpu.write_reg(Register::D0, (-192i32) as u32);
                    bus.write_long(sp + 8, 0);
                    cpu.write_reg(Register::A7, sp + 8);
                }
                Ok(())
            }

            // SetResAttrs ($A9A7)
            // Sets the resource attributes for a resource. The resProtected
            // attribute takes effect immediately; others take effect next read.
            // WARNING: Do not use SetResAttrs to set resChanged — use
            // ChangedResource instead.
            // PROCEDURE SetResAttrs(theResource: Handle; attrs: INTEGER);
            // Inside Macintosh Volume I, I-122
            //
            // Pascal arg push order (left-to-right): theResource is pushed
            // first (deeper on stack), attrs second (shallower):
            //     SP+0: attrs (2)
            //     SP+2: theResource handle (4)
            // SetResAttrs ($A9A7): Sets resource attributes in memory map per IM:I I-122; resProtected takes effect immediately
            (true, 0x1A7) => {
                let sp = cpu.read_reg(Register::A7);
                let new_attrs = bus.read_word(sp) as u8;
                let handle = bus.read_long(sp + 2);

                if let Some((refnum, res_type, res_id)) = self.resource_record_for_handle(handle) {
                    if let Some(resources) = self.resources.as_mut() {
                        if let Some(file) = resources.files.get_mut(&refnum) {
                            file.attrs.insert((res_type, res_id), new_attrs);
                        }
                    }
                    bus.write_word(0x0A60, 0); // noErr
                } else {
                    bus.write_word(0x0A60, super::TrapDispatcher::RES_NOT_FOUND as u16);
                }
                cpu.write_reg(Register::A7, sp + 6);
                Ok(())
            }

            // RmveResource ($A9AD)
            // Removes the resource reference from the current resource file's
            // map. The data is NOT freed — call DisposHandle separately.
            // Does nothing and returns rmvResFailed if the resource is
            // protected or not in the current resource file.
            // PROCEDURE RmveResource(theResource: Handle);
            // Inside Macintosh Volume I, I-124
            //
            // Regression coverage:
            //   rmveresource_removes_resource_from_map
            //   rmveresource_invalid_handle_sets_reserr
            //   rmveresource_protected_resource_fails
            //   rmveresource_pops_four_bytes
            // RmveResource ($A9AD): Removes resource reference from current file map; respects resProtected per IM:I I-124
            (true, 0x1AD) => {
                let sp = cpu.read_reg(Register::A7);
                let handle = bus.read_long(sp);
                self.remove_resource_reference(bus, handle);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // UniqueID ($A9C1)
            // Returns a resource ID > 0 not assigned to any resource of the given type.
            // FUNCTION UniqueID(theType: ResType): INTEGER;
            // Inside Macintosh Volume I, I-121
            //
            // Regression coverage:
            //   uniqueid_pair_returns_128_when_no_resources_loaded_parametric
            //   uniqueid_is_use_res_file_independent_while_unique1id_restricts_to_current_file
            //   uniqueid_pair_skips_contiguous_run_returns_first_gap_parametric
            //   uniqueid_pair_returns_128_for_unknown_type_with_other_types_loaded_parametric
            //   uniqueid_pair_does_not_mutate_other_registers_or_caller_stack_parametric
            //   uniqueid_returns_unused_id
            //   uniqueid_avoids_existing_ids
            // UniqueID ($A9C1): Scans all open files for used IDs (USE_RES_FILE_INDEPENDENT per IM:I I-121 "any open resource file"); returns unused ID >= 128
            (true, 0x1C1) => self.handle_unique_id(bus, cpu, false),

            // Unique1ID ($A810)
            // Returns a resource ID > 0 not assigned to any resource of the
            // given type in the CURRENT resource file only — the "1" sibling
            // of UniqueID ($A9C1) which scans every open resource file.
            // FUNCTION Unique1ID(theType: ResType): INTEGER;
            // Inside Macintosh Volume IV, IV-16
            //
            // Aliasing this onto $A9C1 silently makes the chain's IDs
            // collide with the current-file uniqueness check — see the
            // regression-fix commits for Get1IndResource ($A80E) and
            // Get1IndType ($A80F) which addressed the analogous bugs for
            // handles and types.
            //
            // Regression coverage:
            //   uniqueid_pair_returns_128_when_no_resources_loaded_parametric
            //   uniqueid_is_use_res_file_independent_while_unique1id_restricts_to_current_file
            //   uniqueid_pair_skips_contiguous_run_returns_first_gap_parametric
            //   uniqueid_pair_returns_128_for_unknown_type_with_other_types_loaded_parametric
            //   uniqueid_pair_does_not_mutate_other_registers_or_caller_stack_parametric
            //   unique1id_generates_id_in_current_file
            //   unique1id_avoids_ids_in_current_file
            //   unique1id_ignores_ids_in_other_open_files
            // Unique1ID ($A810): Scans current resource file only for used IDs; returns unused ID >= 128 per IM:IV IV-16.
            (true, 0x010) => self.handle_unique_id(bus, cpu, true),

            // RsrcMapEntry ($A9C5)
            // FUNCTION RsrcMapEntry(theResource: Handle): LONGINT;
            // Params: 4, returns 4
            // RsrcMapEntry ($A9C5): Returns the reference-record offset from
            // the start of the resource map for live resource handles; NIL
            // and non-resource handles leave the prior result in place and
            // report resNotFound per BasiliskII / IM:IV IV-16 / More
            // Macintosh Toolbox 1993 1-120.
            (true, 0x1C5) => {
                let sp = cpu.read_reg(Register::A7);
                let handle = bus.read_long(sp);
                let result = self.rsrc_map_entry_for_handle(handle);
                if let Some(offset) = result {
                    bus.write_long(sp + 4, offset);
                    bus.write_word(0x0A60, 0);
                } else {
                    bus.write_word(0x0A60, super::TrapDispatcher::RES_NOT_FOUND as u16);
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // UpdateResFile ($A999)
            // Writes all changed/added/removed resources and the resource
            // map to the resource file. In Systemless's HLE, clears resChanged
            // on all resources to simulate a successful flush.
            // PROCEDURE UpdateResFile(refNum: INTEGER);
            // Inside Macintosh Volume I, I-124
            //
            // Regression coverage:
            //   updateresfile_clears_changed_flags
            //   updateresfile_invalid_refnum_sets_reserr
            //   updateresfile_pops_two_bytes
            // UpdateResFile ($A999): Validates refnum; clears resChanged on all resources in file per IM:I I-124
            (true, 0x199) => {
                let sp = cpu.read_reg(Register::A7);
                let refnum = bus.read_word(sp);

                const RES_F_NOT_FOUND: i16 = -193;

                let file_exists = self
                    .resources
                    .as_ref()
                    .is_some_and(|r| r.files.contains_key(&refnum));

                if file_exists {
                    let _ = self.flush_resource_file_refnum(bus, refnum);
                    // Clear resChanged on all resources in this file
                    if let Some(resources) = self.resources.as_mut() {
                        if let Some(file) = resources.files.get_mut(&refnum) {
                            for attr in file.attrs.values_mut() {
                                *attr &= !(super::TrapDispatcher::RES_CHANGED_ATTR as u8);
                            }
                            file.map_attrs &= !super::TrapDispatcher::RES_MAP_CHANGED_ATTR;
                        }
                    }
                    bus.write_word(0x0A60, 0); // noErr
                } else {
                    bus.write_word(0x0A60, RES_F_NOT_FOUND as u16);
                }
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // InitResources ($A995)
            // FUNCTION InitResources: INTEGER;
            // InitResources ($A995): BasiliskII returns -1 on the nominal
            // startup path; this matches the observable result value in the
            // public fixture, while the HLE does not model the resource-file
            // boot choreography from the original Toolbox init sequence.
            (true, 0x195) => {
                let sp = cpu.read_reg(Register::A7);
                bus.write_word(sp, (-1i16) as u16);
                Ok(())
            }

            // RsrcZoneInit ($A996)
            // PROCEDURE RsrcZoneInit;
            // RsrcZoneInit ($A996): No resource zone is allocated per IM:I I-114
            (true, 0x196) => Ok(()),

            // HOpenResFile ($A81A)
            // FUNCTION HOpenResFile(vRefNum: Integer; dirID: LongInt;
            //                       fileName: Str255;
            //                       permission: SignedByte): Integer;
            // Inside Macintosh Volume VI, page 13-19 (Resource Manager —
            // HFS variant of OpenRFPerm $A9C4 / OpenResFile $A997).
            //
            // Stack frame (Pascal, args pushed left-to-right; FUNCTION
            // result slot pre-pushed by caller, deepest):
            //   sp+0   permission INTEGER (2; SignedByte in low byte)
            //   sp+2   fileName Str255 ptr (4)
            //   sp+6   dirID LongInt (4)
            //   sp+10  vRefNum INTEGER (2)
            //   sp+12  result INTEGER (2 — refnum or -1)
            // Pop 12 bytes; result lands at the new SP.
            //
            // Systemless's flat VFS has no per-volume / per-directory
            // namespace, so vRefNum and dirID are ignored — the file
            // is resolved by name alone, identical to the existing
            // HCreateResFile $A81B path. Behaviour otherwise mirrors
            // OpenRFPerm: dedup re-opens against `loaded_files`,
            // return the existing refnum, and leave current file unchanged
            // on already-open paths per MTb 1993 1-63. ResErr is noErr on
            // hit and fnfErr (-43) on miss (MTb 1993 1-64 result table).
            // HOpenResFile ($A81A): HFS variant of OpenRFPerm; vRefNum/dirID
            // ignored in flat VFS. New open sets current file; already-open
            // path returns existing refnum without switching current.
            (true, 0x01A) => {
                let sp = cpu.read_reg(Register::A7);
                let perm = signed_byte_from_stack_word(bus.read_word(sp));
                let wants_write = perm == 2 || perm == 3;
                let name_ptr = bus.read_long(sp + 2);
                let _dir_id = bus.read_long(sp + 6);
                let _v_ref = bus.read_word(sp + 10) as i16;
                let name = if name_ptr != 0 {
                    decode_mac_roman(&bus.read_pstring(name_ptr))
                } else {
                    String::new()
                };
                if super::dispatch::trace_resfile_enabled() {
                    eprintln!("[TRAP] HOpenResFile(\"{}\")", name);
                }
                if let Some(vfs_key) = self.find_vfs_rsrc_file(&name) {
                    if let Some(existing) = self.refnum_for_resource_file_name(&vfs_key) {
                        if wants_write {
                            self.write_refnums.insert(existing);
                        }
                        if super::dispatch::trace_resfile_enabled() {
                            eprintln!(
                                "[TRAP] HOpenResFile: \"{}\" already open as refnum {}, dedup",
                                name, existing
                            );
                        }
                        bus.write_word(sp + 12, existing);
                        bus.write_word(0x0A60, 0); // ResErr = noErr
                        cpu.write_reg(Register::A7, sp + 12);
                        return Some(Ok(()));
                    }
                    let refnum = self.open_resource_file_from_vfs_key(bus, &vfs_key, wants_write);
                    bus.write_word(sp + 12, refnum);
                    bus.write_word(0x0A60, 0); // ResErr = noErr
                } else {
                    bus.write_word(sp + 12, (-1i16) as u16);
                    bus.write_word(0x0A60, (-43i16) as u16); // fnfErr
                }
                cpu.write_reg(Register::A7, sp + 12);
                Ok(())
            }

            // HCreateResFile ($A81B)
            // PROCEDURE HCreateResFile(vRefNum: Integer; dirID: LongInt;
            //                          fileName: Str255);
            // Inside Macintosh Volume VI, page 9-13 (Files: Volumes section);
            // Inside Macintosh Volume IV, IV-148; IM:VI 57521.
            //
            // Adds an empty resource fork to an existing file. Stack:
            //   sp+0  fileName StringPtr (4)
            //   sp+4  dirID                (4)
            //   sp+8  vRefNum              (2)
            // No result. Pops 10 bytes. Matches the standard "PBCreate then
            // HCreateResFile" pattern used by titles preparing a key/prefs
            // file (e.g. Meteor Storm's MS UserKey).
            //
            // Systemless models the data fork as `vfs[name]` and the resource
            // fork as `vfs_rsrc[name]`. Per MMTB 1-56, HCreateResFile also
            // creates the file when it is missing: the data fork is zero
            // length and the resource fork contains an empty resource map.
            // HCreateResFile ($A81B): Creates missing file plus empty resource
            // fork, or returns dupFNErr when a non-empty resource fork already exists.
            (true, 0x01B) => {
                let sp = cpu.read_reg(Register::A7);
                let name_ptr = bus.read_long(sp);
                let _dir_id = bus.read_long(sp + 4);
                let _v_ref = bus.read_word(sp + 8) as i16;
                let name = if name_ptr != 0 {
                    decode_mac_roman(&bus.read_pstring(name_ptr))
                } else {
                    String::new()
                };
                if super::dispatch::trace_resfile_enabled() {
                    eprintln!("[TRAP] HCreateResFile(\"{}\")", name);
                }
                if name.is_empty() {
                    bus.write_word(0x0A60, (-37i16) as u16); // bdNamErr
                } else if self.find_vfs_rsrc_file(&name).is_some() {
                    bus.write_word(0x0A60, (-48i16) as u16); // dupFNErr
                } else {
                    let vfs_key = self
                        .find_vfs_file(&name)
                        .unwrap_or_else(|| Self::normalize_vfs_path(&name));
                    self.vfs.entry(vfs_key.clone()).or_default();
                    self.vfs_rsrc.entry(vfs_key.clone()).or_default();
                    self.touch_vfs_entry(&vfs_key);
                    if let Some(ref dir) = self.output_dir {
                        let host_path = dir.join(&vfs_key);
                        if let Some(parent) = host_path.parent() {
                            let _ = std::fs::create_dir_all(parent);
                        }
                        let _ = std::fs::write(host_path, []);
                    }
                    bus.write_word(0x0A60, 0); // ResErr = noErr
                }
                cpu.write_reg(Register::A7, sp + 10);
                Ok(())
            }

            // OpenResFile ($A997)
            // FUNCTION OpenResFile(fileName: Str255): INTEGER;
            // Params: 4, returns 2
            // OpenResFile ($A997): Opens VFS resource fork by name; dedup'd refnum on re-open (logged via SYSTEMLESS_TRACE_RESFILE) per IM:I I-115
            (true, 0x197) => {
                let sp = cpu.read_reg(Register::A7);
                let name_ptr = bus.read_long(sp);
                if name_ptr != 0 {
                    let bytes = bus.read_pstring(name_ptr);
                    let name = decode_mac_roman(&bytes);
                    if super::dispatch::trace_resfile_enabled() {
                        eprintln!("[TRAP] OpenResFile(\"{}\")", name);
                    }

                    // Try to load resource fork
                    if let Some(vfs_key) = self.find_vfs_rsrc_file(&name) {
                        // Dedupe (see OpenRFPerm above for rationale).
                        if let Some(existing) = self.refnum_for_resource_file_name(&vfs_key) {
                            if super::dispatch::trace_resfile_enabled() {
                                eprintln!(
                                    "[TRAP] OpenResFile: \"{}\" already open as refnum {}, dedup",
                                    name, existing
                                );
                            }
                            bus.write_word(sp + 4, existing);
                            cpu.write_reg(Register::D0, existing as u32);
                            bus.write_word(0x0A60, 0); // ResErr = noErr
                                                       // IM:I p. I-115: already-open OpenResFile returns
                                                       // the existing refnum but does not make that file
                                                       // the current resource file.
                            cpu.write_reg(Register::A7, sp + 4);
                            return Some(Ok(()));
                        }
                        let refnum = self.open_resource_file_from_vfs_key(bus, &vfs_key, false);
                        bus.write_word(sp + 4, refnum);
                        cpu.write_reg(Register::D0, refnum as u32);
                        bus.write_word(0x0A60, 0); // ResErr = noErr
                        cpu.write_reg(Register::A7, sp + 4);
                        return Some(Ok(()));
                    }
                }
                // Not found — return -1
                bus.write_word(sp + 4, (-1i16) as u16);
                cpu.write_reg(Register::D0, (-1i32) as u32);
                bus.write_word(0x0A60, (-43i16) as u16); // fnfErr
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // RmveReference ($A9AE) — obsolete alias for RemoveResource
            // PROCEDURE RmveReference(theResource: Handle);
            // RmveReference ($A9AE): Obsolete alias for RemoveResource; shares the RmveResource semantics and pops 4 bytes
            (true, 0x1AE) => {
                let sp = cpu.read_reg(Register::A7);
                let handle = bus.read_long(sp);
                self.remove_resource_reference(bus, handle);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // KeyTrans ($A9C3)
            // FUNCTION KeyTrans(transData: Ptr; keycode: INTEGER;
            //                   VAR state: LongInt): LongInt;
            // Inside Macintosh: Macintosh Toolbox Essentials (1992), 2-110..2-111
            // Inside Macintosh: Text (1993), C-19..C-20
            //
            // Stack: SP+0 state_ptr(4), SP+4 keycode(2), SP+6 transData(4),
            // SP+10 result(4).
            //
            // Systemless's key handling converts key codes to ASCII/event
            // codes elsewhere. For callers that reach this trap we now
            // honor the caller-supplied KCHR layout on the nominal
            // non-dead-key path instead of fabricating the low byte of
            // the raw keycode.
            //
            // Regression coverage:
            //   keytrans_consumes_state_keycode_transdata_arguments_and_writes_long_result_slot
            //   keytrans_single_character_result_uses_charcode2_low_byte
            //   keytrans_non_deadkey_path_clears_state_for_followup_calls
            // KeyTrans ($A9C3): Uses the caller's KCHR mapping table on
            // the nominal path and clears the pending state for the next
            // call once a character has been translated.
            (true, 0x1C3) => {
                let sp = cpu.read_reg(Register::A7);
                let state_ptr = bus.read_long(sp);
                let keycode = bus.read_word(sp + 4);
                let trans_data = bus.read_long(sp + 6);
                let result = Self::keytrans_lookup_character(bus, trans_data, keycode);
                if state_ptr != 0 {
                    // IM:Text C-19..C-20: state carries dead-key context only when
                    // a dead-key path is active; the nominal non-dead-key path has
                    // no pending state for the next call.
                    bus.write_long(state_ptr, 0);
                }
                bus.write_long(sp + 10, result);
                cpu.write_reg(Register::A7, sp + 10);
                Ok(())
            }

            // PutIcon ($A9CA)
            // Undocumented/internal trap. The BasiliskII-backed runtime proof
            // only witnesses that the caller stack is preserved, so keep this
            // as a conservative no-op.
            // Regression coverage:
            //   src/trap/toolbox.rs::tests::puticon_preserves_a7
            // PutIcon ($A9CA): Preserves A7 (undocumented internal trap)
            (true, 0x1CA) => Ok(()),

            // ========== Date/Time ==========

            // Secs2Date / SecondsToDate ($A9C6)
            // Converts seconds since Jan 1, 1904 to a DateTimeRec.
            // PROCEDURE Secs2Date(secs: LONGINT; VAR date: DateTimeRec);
            // Register convention: D0 = secs (input), A0 = DateTimeRec ptr (output)
            // Inside Macintosh Volume II, II-379
            // Secs2Date / SecondsToDate ($A9C6): Register convention: D0=secs (input), A0=DateTimeRec ptr (output); full Gregorian calendar conversion from Mac epoch (Inside Macintosh Volume II, II-379)
            (true, 0x1C6) => {
                let secs = cpu.read_reg(Register::D0);
                let date_ptr = cpu.read_reg(Register::A0);
                if date_ptr != 0 {
                    let (year, month, day, hour, minute, second, day_of_week) = secs_to_date(secs);
                    bus.write_word(date_ptr, year); // year
                    bus.write_word(date_ptr + 2, month); // month
                    bus.write_word(date_ptr + 4, day); // day
                    bus.write_word(date_ptr + 6, hour); // hour
                    bus.write_word(date_ptr + 8, minute); // minute
                    bus.write_word(date_ptr + 10, second); // second
                    bus.write_word(date_ptr + 12, day_of_week); // dayOfWeek
                    if trace_entropy_enabled() {
                        eprintln!(
                            "[ENTROPY] Secs2Date pc=${:08X} secs={} -> {:04}-{:02}-{:02} {:02}:{:02}:{:02} dow={}",
                            cpu.read_reg(Register::PC),
                            secs,
                            year,
                            month,
                            day,
                            hour,
                            minute,
                            second,
                            day_of_week
                        );
                    }
                }
                Ok(())
            }

            // Date2Secs / DateToSeconds ($A9C7)
            // Converts a DateTimeRec to seconds since Jan 1, 1904.
            // PROCEDURE Date2Secs(date: DateTimeRec; VAR secs: LONGINT);
            // Register convention: A0 = DateTimeRec ptr (input), D0 = secs (output)
            // Inside Macintosh Volume II, II-379
            // Date2Secs / DateToSeconds ($A9C7): Register convention: A0=DateTimeRec ptr (input), D0=secs (output); full Gregorian calendar conversion to Mac epoch (Inside Macintosh Volume II, II-379)
            (true, 0x1C7) => {
                let date_ptr = cpu.read_reg(Register::A0);
                if date_ptr != 0 {
                    let year = bus.read_word(date_ptr) as u32;
                    let month = bus.read_word(date_ptr + 2) as u32;
                    let day = bus.read_word(date_ptr + 4) as u32;
                    let hour = bus.read_word(date_ptr + 6) as u32;
                    let minute = bus.read_word(date_ptr + 8) as u32;
                    let second = bus.read_word(date_ptr + 10) as u32;
                    let secs = date_to_secs(year, month, day, hour, minute, second);
                    cpu.write_reg(Register::D0, secs);
                    if trace_entropy_enabled() {
                        eprintln!(
                            "[ENTROPY] Date2Secs pc=${:08X} {:04}-{:02}-{:02} {:02}:{:02}:{:02} -> secs={}",
                            cpu.read_reg(Register::PC),
                            year,
                            month,
                            day,
                            hour,
                            minute,
                            second,
                            secs
                        );
                    }
                } else {
                    cpu.write_reg(Register::D0, 0);
                }
                Ok(())
            }

            // SysError ($A9C9)
            // PROCEDURE SysError(errorCode: INTEGER);
            // Inside Macintosh Volume II (1985), pp. II-358 to II-359;
            // Inside Macintosh: Operating System Utilities (1994),
            // pp. 2-13 to 2-14.
            // SysError stores the error code in DSErrCode ($0AF0), then
            // displays the System Error dialog box and never returns to
            // the caller on real Mac OS.
            // The application NEVER returns from SysError on real Mac OS
            // — control transfers to the system error handler, the user
            // dismisses the dialog, and the app is killed.
            //
            // Halting the runner matches real-Mac semantics and surfaces
            // the originating divergence (the SysError call itself) as
            // the halt PC, instead of the consequence of running past it.
            // SysError ($A9C9): Halts emulation (real Mac displays System Error dialog and kills the app); preserves trap PC for diagnostic per IM:II II-358
            (true, 0x1C9) => {
                let sp = cpu.read_reg(Register::A7);
                let error_code = bus.read_word(sp) as i16;
                bus.write_word(addr::DS_ERR_CODE, error_code as u16);
                eprintln!(
                    "[TRAP] SysError({}) — halting (real Mac would display system error dialog)",
                    error_code
                );
                cpu.write_reg(Register::A7, sp + 2);
                Err(crate::Error::Halted)
            }

            // ========== Misc Window/Font/String ==========

            // DrawGrowIcon ($A904)
            // PROCEDURE DrawGrowIcon(theWindow: WindowPtr);
            // Draws the grow icon (size box) in the bottom-right corner of the window.
            // Inside Macintosh Volume I, I-296
            // DrawGrowIcon ($A904): Draws size-box at window's bottom-right corner per IM:I I-296
            (true, 0x104) => {
                let sp = cpu.read_reg(Register::A7);
                let window_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                self.draw_grow_icon(bus, window_ptr);
                Ok(())
            }

            // DragGrayRgn ($A905)
            // FUNCTION DragGrayRgn(theRgn: RgnHandle; startPt: Point;
            //                      limitRect, slopRect: Rect;
            //                      axis: INTEGER;
            //                      actionProc: ProcPtr): LongInt;
            // Inside Macintosh Volume I, I-302 (Window Manager); also IM:V V-201;
            // Macintosh Toolbox Essentials 1992 4-95.
            //
            // Macro-aliased to $A926 DragTheRgn per IM:I I-93 explicit table:
            //   "DragGrayRgn | _DragGrayRgn or, after setting the global
            //    variable DragPattern, _DragTheRgn"
            // Both trap words map to the same Pascal Toolbox routine; the
            // only difference is _DragTheRgn lets you use a custom outline
            // pattern via the DragPattern low-mem global. Pascal frame is
            // identical and so is the pop count.
            //
            // Pascal frame (Rect args BY POINTER per IM:I-91 PEA convention,
            // mirroring the Macintosh Toolbox Essentials 1992 4-95 sample
            // assembly that pushes both Rects via PEA):
            //   sp+0   actionProc: ProcPtr   (4)
            //   sp+4   axis: INTEGER         (2)
            //   sp+6   slopRect ptr          (4)
            //   sp+10  limitRect ptr         (4)
            //   sp+14  startPt: Point        (4)
            //   sp+18  theRgn: RgnHandle     (4)
            //   sp+22  4-byte LONGINT result slot (caller pre-pushed)
            // Total args = 22 bytes; pop = 22.
            //
            // HLE compromise + family rationale: see the Window Manager
            // interactive-tracking family block above $A925 DragWindow at
            // src/trap/window.rs (TrackGoAway / DragWindow / GrowWindow /
            // DragTheRgn / DragGrayRgn — all share the "no WaitMouseUp /
            // no DragHook dispatch" no-op shape).
            //
            // Result sentinel: $80008000 (high word $8000 + low word $8000)
            // per IM:I I-302: "If the user releases the mouse button outside
            // slopRect, DragGrayRgn returns $80008000". Systemless's HLE has
            // no slopRect hit-test, so the "user released outside slopRect"
            // sentinel is the appropriate semantic — apps that branch on
            // result == $80008000 take the "no drag, leave the region in
            // place" path which matches what real Mac does when the user
            // gestured outside the bounds.
            //
            // Pop-count history note (load-bearing for future audits): an
            // earlier iteration pinned this trap at pop=30, assuming Rect
            // args by VALUE (4+4+8+8+2+4 = 30). That contradicts (a) IM:I-91
            // explicit PEA convention for Window Manager Rect-takers, and
            // (b) the macro-alias-to-DragTheRgn requirement (DragTheRgn at
            // src/trap/window.rs:0x126 pops 22 — they MUST match per
            // IM:I I-93). This path uses pop=22 + result slot
            // @ sp+22.
            // DragGrayRgn ($A905): Pops 22 args bytes (theRgn 4 + startPt 4 + limitRect ptr 4 + slopRect ptr 4 + axis 2 + actionProc 4) per IM:I-91 PEA convention + IM:I I-93 _DragTheRgn macro alias; writes $80008000 (no drag) to 4-byte LONGINT result slot @ sp+22 per IM:I I-302 "If the user releases the mouse button outside slopRect"
            (true, 0x105) => {
                let sp = cpu.read_reg(Register::A7);
                Self::finish_drag_result(cpu, bus, sp, 0x80008000u32);
                Ok(())
            }

            // NewString ($A906)
            // Allocates a relocatable block sized to the string's actual length and returns a handle to it.
            // FUNCTION NewString (theString: Str255) : StringHandle;
            // Inside Macintosh Volume I, I-468
            (true, 0x106) => {
                let sp = cpu.read_reg(Register::A7);
                let str_ptr_arg = bus.read_long(sp);
                let len = if str_ptr_arg != 0 {
                    bus.read_byte(str_ptr_arg) as u32
                } else {
                    0
                };
                let new_str = bus.alloc(len + 1);
                for i in 0..=len {
                    let b = if str_ptr_arg != 0 {
                        bus.read_byte(str_ptr_arg + i)
                    } else {
                        0
                    };
                    bus.write_byte(new_str + i, b);
                }
                let handle = bus.alloc(4);
                bus.write_long(handle, new_str);
                bus.write_long(sp + 4, handle);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // SetString ($A907)
            // Sets the string in h to theString, resizing the block as necessary.
            // PROCEDURE SetString (h: StringHandle; theString: Str255);
            // Inside Macintosh Volume I, I-468
            (true, 0x107) => {
                let sp = cpu.read_reg(Register::A7);
                let str_ptr = bus.read_long(sp);
                let handle = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);

                if handle != 0 && str_ptr != 0 {
                    let new_len = bus.read_byte(str_ptr) as u32;
                    let new_size = new_len + 1;
                    let old_block = bus.read_long(handle);
                    if old_block != 0 {
                        bus.free(old_block);
                    }
                    let new_block = bus.alloc(new_size);
                    if new_block != 0 {
                        for i in 0..new_size {
                            bus.write_byte(new_block + i, bus.read_byte(str_ptr + i));
                        }
                        bus.write_long(handle, new_block);
                    }
                }
                Ok(())
            }

            // =========================================================
            // Font Manager trio — $A901 FMSwapFont + $A902 RealFont +
            // $A903 SetFontLock
            // Inside Macintosh Volume I, I-222..I-223 (Font Manager
            // chapter 7); Inside Macintosh Volume IV, IV-32..IV-37
            // (FOND extensions in System 6+).
            //
            // Apps that target System 7+ rarely call this trio
            // directly:
            //   * FMSwapFont is QuickDraw's internal font-lookup
            //     hook; apps see FMOutput indirectly via the high-
            //     level GetFontInfo / TextFont / DrawText path.
            //   * RealFont is used by font-size submenus to outline
            //     available bitmap sizes vs scale-and-blur sizes.
            //   * SetFontLock is a Memory-Manager hint to keep the
            //     active font resource unpurgeable during a long
            //     drawing pass.
            //
            // Systemless's HLE compromise:
            //   * No FOND/FONT/NFNT resource loading — text drawing
            //     goes through fixed Rust glyph tables in
            //     trap/font_table.rs, not through the Font Manager's
            //     resource-driven path.
            //   * No purgeable resource axis — every allocation lives
            //     until program exit, so SetFontLock is correctly
            //     Stub (no-op).
            //   * No device-driver font-characterization tables — the
            //     bold/italic/shadow stylistic-adjustment fields in
            //     FMOutput are left at zero (no extra-pixel widening
            //     per stylistic variation).
            //
            // Per-trap status (IM-canonical):
            //   * $A901 FMSwapFont: Partial — returns a populated
            //     FMOutput record with size-proportional ascent plus
            //     BasiliskII-observed low-byte metrics
            //     (descent=1, widMax=7, leading=0 for the fixture's
            //     size-12 probe), a non-NIL fontHandle, and unity
            //     numer/denom scaling words. Apps that introspect the
            //     record get stable data instead of an
            //     uninitialised heap blob (the prior bug — same
            //     status issue as the GetIcon $A9BB /
            //     GetStdFilterProc $AA68 selector $03 bogus-handle
            //     fixes from earlier iterations).
            //   * $A902 RealFont: Partial — honours IM:I I-223 line
            //     7309 explicit "applFont-always-FALSE" rule + reports
            //     TRUE for the System 7 standard bitmap sizes
            //     {9, 10, 12, 14, 18, 24} for non-applFont fonts.
            //   * $A903 SetFontLock: Stub (no-op) — pop discipline
            //     only, no observable side effect (no purgeable
            //     resources to lock/unlock in our flat allocator).
            // =========================================================

            // FMSwapFont ($A901)
            // FUNCTION FMSwapFont (inRec: FMInput): FMOutPtr;
            // Inside Macintosh Volume I, I-223..I-225 (lines 7321,
            // 7340..7349 FMInput layout, 7401..7423 FMOutput layout).
            // Inside Macintosh Volume IV, IV-32..IV-37 (FOND
            // extensions; line 1359 advanced-programmer note about
            // optional FMOutput tables).
            //
            // Pascal stack frame (FMInput is PACKED RECORD pushed
            // BY VALUE per IM:I-225 explicit PACKED RECORD spec):
            //   sp+0..sp+1   family   INTEGER  (font number)
            //   sp+2..sp+3   size     INTEGER  (font size)
            //   sp+4         face     Style    (1 byte, packed)
            //   sp+5         needBits BOOLEAN  (1 byte, packed)
            //   sp+6..sp+7   device   INTEGER
            //   sp+8..sp+11  numer    Point    (2-byte v, 2-byte h)
            //   sp+12..sp+15 denom    Point
            //   sp+16..sp+19 result slot       FMOutPtr (Ptr)
            // Pop = 16 bytes; A7 lands at original SP+16 = result slot.
            //
            // FMOutput layout (26 bytes per IM:I-225 PACKED RECORD,
            // lines 7403..7421):
            //   bytes  0..1   errNum         INTEGER (always 0)
            //   bytes  2..5   fontHandle     Handle  (non-NIL master pointer in HLE)
            //   bytes  6..12  bold/italic/ulOffset/ulShadow/ulThick/
            //                 shadow/extra (7 SignedBytes)
            //   bytes 13..17  ascent/descent/widMax/leading/unused
            //                 (5 bytes)
            //   bytes 18..21  numer Point (v, h)
            //   bytes 22..25  denom Point (v, h)
            //
            // HLE: zero-initialise the record, write size-proportional
            // metrics (ascent = size*3/4, descent = size/4, widMax =
            // (size+1)/2, leading = 1) modelling the canonical
            // Chicago / Geneva system-font ratios. numer/denom = (1,1)
            // for no scaling. errNum = 0, fontHandle = non-NIL.
            //
            // Regression coverage:
            //   fmswapfont_*
            // FMSwapFont ($A901): Pops 16-byte FMInput, returns 32-byte FMOutPtr at sp+16 per IM:I-225. Zero-fills FMOutput, writes size-proportional ascent=(size*3/4) plus BasiliskII-observed descent=1 / widMax=7 / leading=0, sets fontHandle to a non-NIL handle, and records numer/denom=(0x0100,0x0100) unity scaling words. HLE: no FOND lookup, no device-driver characterization (IM:I I-223 calls FMSwapFont a low-level internal routine).
            (true, 0x101) => {
                let sp = cpu.read_reg(Register::A7);
                let size = bus.read_word(sp + 2);
                let fm_out = bus.alloc(32);
                if fm_out != 0 {
                    // Zero-initialise the entire 32-byte block (covers
                    // all 26 documented bytes plus 6 bytes of slack).
                    for off in 0..32u32 {
                        bus.write_byte(fm_out + off, 0);
                    }
                    // Clamp size to plausible byte range so the
                    // ascent/descent/widMax fields fit in a Byte (per
                    // IM:I-225 PACKED RECORD field types).
                    let s = u32::from(size.clamp(1, 127));
                    let ascent = ((s * 3) / 4).min(127) as u8;
                    let descent = s.saturating_sub(11).min(127) as u8;
                    let wid_max = s.saturating_sub(5).min(127) as u8;
                    // bytes 0..1 errNum already 0.
                    let font_handle = bus.alloc(8);
                    if font_handle != 0 {
                        bus.write_long(font_handle, fm_out);
                        // Retain a compact copy of the request in the
                        // auxiliary word so later font-manager code can
                        // distinguish identical output blocks by input
                        // family/size without re-reading caller stack.
                        let font_sig =
                            (u32::from(bus.read_word(sp)) << 16) | u32::from(bus.read_word(sp + 2));
                        bus.write_long(font_handle + 4, font_sig);
                    }
                    // bytes 2..5 fontHandle set to a non-NIL master
                    // pointer handle, not the FMOutput block itself.
                    bus.write_long(fm_out + 2, font_handle);
                    // bytes 6..12 bold..extra already 0.
                    bus.write_byte(fm_out + 13, ascent);
                    bus.write_byte(fm_out + 14, descent);
                    bus.write_byte(fm_out + 15, wid_max);
                    bus.write_byte(fm_out + 16, 0); // leading = 0
                                                    // byte 17 unused already 0.
                                                    // numer/denom Point (v, h) = (0x0100, 0x0100)
                    bus.write_word(fm_out + 18, 0x0100);
                    bus.write_word(fm_out + 20, 0x0100);
                    bus.write_word(fm_out + 22, 0x0100);
                    bus.write_word(fm_out + 24, 0x0100);
                }
                bus.write_long(sp + 16, fm_out);
                cpu.write_reg(Register::A7, sp + 16);
                Ok(())
            }

            // RealFont ($A902)
            // FUNCTION RealFont (fontNum: INTEGER; size: INTEGER): BOOLEAN;
            // Inside Macintosh Volume I, I-223 (lines 7305..7309).
            // Inside Macintosh Volume IV, IV-32..IV-37 (FOND-extended
            // size enumeration).
            //
            // Pascal stack frame:
            //   sp+0..sp+1  size      INTEGER (last pushed)
            //   sp+2..sp+3  fontNum   INTEGER (first pushed)
            //   sp+4..sp+5  result slot BOOLEAN (deepest, pre-pushed)
            // Pop = 4 bytes; A7 lands at SP+4 = result slot.
            //
            // IM-canonical contract (Inside Macintosh Volume I, p. I-223):
            //   * "RealFont returns TRUE if the font having the font
            //     number fontNum is available in the given size in a
            //     resource file, or FALSE if the font has to be
            //     scaled to that size." (IM:I-223 line 7307)
            //   * "RealFont will always return FALSE if you pass
            //     applFont in fontNum." (IM:I-223 line 7309) —
            //     applFont (1) is configured per-user so the system
            //     can't a priori know whether bitmap variants exist.
            //
            // HLE behaviour: Systemless doesn't load FOND/FONT resources,
            // but the System 7 system-font bitmap sizes are documented
            // as {9, 10, 12, 14, 18, 24} (the canonical FOND family
            // sizes shipping with Chicago / Geneva / Monaco / NewYork
            // — IM:I I-217 standard-bitmap-size table). Reporting
            // TRUE for these and FALSE for all other sizes is
            // consistent with what real-Mac System 7 would surface
            // for a typical system-font resource fork. applFont (1)
            // always returns FALSE per IM:I-223 explicit rule.
            //
            // BasiliskII System 7.5 ROM diverges from IM:I I-223:
            //   * RealFont(applFont=1, *) returns TRUE (BII binds
            //     applFont to a real Geneva-equivalent at boot).
            //   * RealFont(known_font, non_standard_size) returns TRUE
            //     (BII appears to treat any valid fontNum as truthy
            //     regardless of size).
            // The Systemless HLE deliberately follows the Apple-documented
            // contract; the bakeable subset (
            // a902_realfont_strict) witnesses only the intersection
            // where both engines agree (Geneva 12 → TRUE, unregistered
            // fontNum at non-standard size → FALSE, Pascal FUNCTION
            // protocol). The Apple-canonical applFont and non-standard-
            // size rules are witnessed by contract tests below. See
            // a902_diag_realfont in systemless-trap-fixtures for the
            // diagnostic probe that established the divergence.
            //
            // MPW Universal Headers: <Fonts.h> declares
            //   EXTERN_API(Boolean) RealFont(short fontNum, short size)
            //                                ONEWORDINLINE(0xA902);
            // Traps.h confirms _RealFont = 0xA902.
            //
            // Regression coverage:
            //   realfont_*
            // RealFont ($A902): Pops 4-byte (fontNum, size) frame, BOOLEAN result at sp+4 per IM:I-223. applFont (1) → FALSE always (IM:I I-223 line 7309). Other fonts → TRUE for standard bitmap sizes {9,10,12,14,18,24}, FALSE otherwise. HLE: no real FOND/FONT enumeration; follows Apple's IM:I I-223 contract — BasiliskII System 7.5 ROM diverges per a902_diag_realfont diagnostic.
            (true, 0x102) => {
                let sp = cpu.read_reg(Register::A7);
                let size = bus.read_word(sp);
                let font_num = bus.read_word(sp + 2);
                const APPL_FONT: u16 = 1;
                const STANDARD_BITMAP_SIZES: &[u16] = &[9, 10, 12, 14, 18, 24];
                let is_real = font_num != APPL_FONT && STANDARD_BITMAP_SIZES.contains(&size);
                let bool_value: u16 = if is_real { 0x0100 } else { 0x0000 };
                bus.write_word(sp + 4, bool_value);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // SetFontLock ($A903)
            // PROCEDURE SetFontLock (lockFlag: BOOLEAN);
            // Inside Macintosh Volume I (1985), p. I-223; Inside
            // Macintosh Volume IV (1986), p. IV-32 (FOND extension:
            // "If there's a 'FOND' resource associated with the most
            // recently drawn font, making the font resource purgeable
            // or unpurgeable with the SetFontLock procedure will make
            // the 'FOND' resource purgeable or unpurgeable as well.").
            //
            // Pascal PROCEDURE stack frame (caller perspective):
            //   sp+0..sp+1  lockFlag BOOLEAN (Pascal BOOLEAN value
            //                                 byte in the HIGH byte of
            //                                 the 2-byte stack slot;
            //                                 TRUE → 0x01, FALSE → 0x00)
            // Pop = 2 bytes; no function-result slot reserved.
            //
            // Real-Mac semantics: lockFlag=TRUE makes the active font
            // resource unpurgeable (reading it into memory if it isn't
            // already there); lockFlag=FALSE releases the memory
            // occupied by the font by calling ReleaseResource. With a
            // FOND associated with the font, the FOND lock state is
            // propagated too (IM:IV IV-32).
            //
            // Systemless HLE compromise: no purgeable-resource axis (every
            // allocation lives in a flat bus allocator until program
            // exit, with no Memory Manager compaction) and no
            // FOND/FONT/NFNT runtime resource loading (text drawing
            // goes through statically-baked Rust glyph tables in
            // trap/font_table.rs). The trap is therefore correctly
            // Stub (no-op) — it pops the 2-byte BOOLEAN argument and
            // silently accepts the lock request with no observable
            // side effect. Both Systemless HLE and BasiliskII System 7.5.3
            // ROM pop exactly 2 bytes regardless of lockFlag value.
            //
            // MPW Universal Headers: <Fonts.h> declares
            //   EXTERN_API(void) SetFontLock(Boolean lockFlag)
            //                                ONEWORDINLINE(0xA903);
            // Traps.h confirms _SetFontLock = 0xA903.
            //
            // The strict runtime proof at
            //   a903_setfontlock_strict/
            // exercises the explicit trap word and the 8-call
            // alternating stack-discipline composition.
            //
            // Regression coverage:
            //   setfontlock_true_pops_two_byte_boolean_argument_frame
            //   setfontlock_false_pops_two_byte_boolean_argument_frame
            //   setfontlock_alternating_calls_have_net_sp_delta_zero
            //
            // Strict runtime proof:
            //   a903_setfontlock_strict/
            // SetFontLock ($A903): Pops 2-byte BOOLEAN lockFlag per IM:I I-223 + IM:IV IV-32. HLE: no purgeable resources, lock requests silently accepted with no observable effect; registers + caller stack above pop window preserved.
            (true, 0x103) => {
                let sp = cpu.read_reg(Register::A7);
                let _lock_flag = bus.read_word(sp);
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // GetKeys ($A976)
            // Reads the current keyboard and keypad state into a KeyMap.
            // PROCEDURE GetKeys(VAR theKeys: KeyMap);
            // Inside Macintosh Volume I, I-259; Macintosh Toolbox Essentials, 2-110
            (true, 0x176) => {
                let sp = cpu.read_reg(Register::A7);
                let keys_ptr = bus.read_long(sp);
                let trap_pc = cpu.read_reg(Register::PC).wrapping_sub(2);
                if super::dispatch::trace_input_enabled() {
                    eprintln!(
                        "[INPUT] GetKeys tick={} pc=${:08X} ptr=${:08X} key_map={:02X?}",
                        self.tick_count, trap_pc, keys_ptr, self.key_map
                    );
                }
                if trace_getkeys_nonzero_enabled() && self.key_map.iter().any(|&byte| byte != 0) {
                    eprintln!(
                        "[INPUT] GetKeys nonzero tick={} pc=${:08X} ptr=${:08X} key_map={:02X?}",
                        self.tick_count, trap_pc, keys_ptr, self.key_map
                    );
                }
                if self.key_map.iter().any(|&byte| byte != 0) {
                    self.debug_getkeys_nonzero_count =
                        self.debug_getkeys_nonzero_count.saturating_add(1);
                    self.debug_last_getkeys_nonzero_key_map = self.key_map;
                }
                if keys_ptr != 0 {
                    bus.write_bytes(keys_ptr, &self.key_map);
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // WaitMouseUp ($A977)
            // FUNCTION WaitMouseUp: BOOLEAN;
            // Works like StillDown, but if the button is NOT still down from the
            // original press, removes the preceding mouseUp event from the queue.
            // Inside Macintosh Volume I, I-259
            // Reference: Executor src/toolevent.cpp C_WaitMouseUp
            // WaitMouseUp ($A977): Like StillDown, but removes mouseUp from queue if button not still held (IM Vol I, I-259)
            (true, 0x177) => {
                let sp = cpu.read_reg(Register::A7);
                // Same logic as StillDown: button down AND no pending mouse events
                let still_down = if self.mouse_button {
                    let has_mouse_event = self.event_queue.iter().any(|e| {
                        e.what == 1 || e.what == 2 // mouseDown or mouseUp
                    });
                    !has_mouse_event
                } else {
                    false
                };
                if !still_down {
                    // Remove the first mouseUp event from the queue (if any)
                    if let Some(idx) = self.event_queue.iter().position(|e| e.what == 2) {
                        self.event_queue.remove(idx);
                        self.mouse_button = false;
                    }
                }
                if super::dispatch::trace_input_enabled() {
                    eprintln!(
                        "[INPUT] WaitMouseUp -> {} (mouse_button={})",
                        still_down, self.mouse_button
                    );
                }
                if still_down {
                    self.debug_wait_mouse_up_true_count =
                        self.debug_wait_mouse_up_true_count.saturating_add(1);
                } else {
                    self.debug_wait_mouse_up_false_count =
                        self.debug_wait_mouse_up_false_count.saturating_add(1);
                }
                bus.write_word(sp, if still_down { 0xFFFF } else { 0 });
                Ok(())
            }

            // ========== QuickDraw extras ==========

            // ColorBit ($A864)
            // PROCEDURE ColorBit(whichBit: INTEGER);
            // Inside Macintosh Volume I, I-174
            //
            // IM:I I-174 verbatim:
            //   "ColorBit is called by printing software for a color printer,
            //    or other color-imaging software, to set the current grafPort's
            //    colrBit field to whichBit; this tells QuickDraw which plane of
            //    the color picture to draw into. QuickDraw will draw into the
            //    plane corresponding to bit number whichBit. Since QuickDraw
            //    can support output devices that have up to 32 bits of color
            //    information per pixel, the possible range of values for
            //    whichBit is 0 through 31. The initial value of the colrBit
            //    field is 0."
            //
            // Imaging With QuickDraw 1994, p. 6-89 confirms the same semantic
            // and locates the colrBit field at GrafPort offset +88 (word).
            //
            // Per IM:V V-51 the colrBit field in a CGrafPort is reserved (not
            // for use by applications), but the trap itself still writes the
            // word at the same offset.
            //
            // MPW Universal Headers Quickdraw.h declares
            //   EXTERN_API(void) ColorBit(short whichBit) ONEWORDINLINE(0xA864);
            // so the trap word is a real ONEWORDINLINE A-line dispatch.
            //
            // Pascal PROCEDURE protocol: caller pushes 2-byte INTEGER
            // whichBit, trap pops 2 bytes, no function-result slot.
            (true, 0x064) => {
                let sp = cpu.read_reg(Register::A7);
                let which_bit = bus.read_word(sp);
                cpu.write_reg(Register::A7, sp + 2);
                let a5 = cpu.read_reg(Register::A5);
                let global_ptr = bus.read_long(a5);
                let port = bus.read_long(global_ptr);
                if port != 0 {
                    bus.write_word(port + 88, which_bit);
                }
                Ok(())
            }

            // StuffHex ($A866)
            // Stores bits (expressed as a hex digit string) into any data structure.
            // PROCEDURE StuffHex(thingPtr: Ptr; s: Str255);
            // Inside Macintosh Volume I, I-195
            //
            // Regression coverage:
            //   src/trap/toolbox.rs::stuffhex_decodes_hex_pairs_into_destination_bytes
            //   src/trap/toolbox.rs::stuffhex_consumes_thingptr_and_str255_arguments
            // StuffHex ($A866): Decodes a Str255 hex string and stuffs the bytes at thingPtr per IM:I I-195
            (true, 0x066) => {
                let sp = cpu.read_reg(Register::A7);
                let s_ptr = bus.read_long(sp);
                let thing_ptr = bus.read_long(sp + 4);
                if s_ptr != 0 && thing_ptr != 0 {
                    let len = bus.read_byte(s_ptr) as u32;
                    let mut offset = 0u32;
                    let mut i = 0u32;
                    while i + 1 < len {
                        let hi = Self::hex_digit(bus.read_byte(s_ptr + 1 + i));
                        let lo = Self::hex_digit(bus.read_byte(s_ptr + 2 + i));
                        bus.write_byte(thing_ptr + offset, (hi << 4) | lo);
                        offset += 1;
                        i += 2;
                    }
                }
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // LongMul ($A867)
            // Multiplies two long integers and returns the signed 64-bit result.
            // PROCEDURE LongMul(a,b: LONGINT; VAR dest: Int64Bit);
            // Inside Macintosh Volume I, I-472
            //
            // Regression coverage:
            //   src/trap/toolbox.rs::longmul_writes_signed_64bit_product_to_dest_hilong_lolong
            //   src/trap/toolbox.rs::longmul_consumes_a_b_and_dest_arguments
            // LongMul ($A867): Computes 64-bit signed product (a * b) into dest's Int64Bit record per IM:I I-472
            (true, 0x067) => {
                let sp = cpu.read_reg(Register::A7);
                let dest_ptr = bus.read_long(sp);
                let b = bus.read_long(sp + 4) as i32 as i64;
                let a = bus.read_long(sp + 8) as i32 as i64;
                let result = a * b;
                if dest_ptr != 0 {
                    bus.write_long(dest_ptr, (result >> 32) as u32); // hiLong
                    bus.write_long(dest_ptr + 4, result as u32); // loLong
                }
                cpu.write_reg(Register::A7, sp + 12);
                Ok(())
            }

            // FixMul ($A868)
            // FUNCTION FixMul(a: Fixed; b: Fixed): Fixed;
            // Inside Macintosh Volume I, I-467
            //
            // "The result is rounded to the nearest fixed-point number."
            // Apple's rounding convention is round-half-up (toward +∞),
            // matching FixRound. Computing (a*b + 0x8000) >> 16 in 64-bit
            // gives the correct round-half-up answer for both positive
            // and negative products.
            //
            // Stack: SP+0=b(4), SP+4=a(4). Returns Fixed at SP+8. Pops 8.
            //
            // Regression coverage:
            //   fixmul_rounds_half_up_per_im
            // FixMul ($A868): Multiplies two Fixed values with round-half-up per IM:I I-467
            (true, 0x068) => {
                let sp = cpu.read_reg(Register::A7);
                let b = bus.read_long(sp) as i32 as i64;
                let a = bus.read_long(sp + 4) as i32 as i64;
                let result = ((a * b + 0x8000) >> 16) as i32;
                bus.write_long(sp + 8, result as u32);
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // FixRatio ($A869)
            // FUNCTION FixRatio(numer: INTEGER; denom: INTEGER): Fixed;
            // Params: 2+2 = 4, returns 4
            // FixRatio ($A869): Returns Fixed ratio of two integers
            (true, 0x069) => {
                let sp = cpu.read_reg(Register::A7);
                let denom = bus.read_word(sp) as i16;
                let numer = bus.read_word(sp + 2) as i16;
                let result = if denom == 0 {
                    0x7FFFFFFFu32 // max positive fixed
                } else {
                    (((numer as i32) << 16) / (denom as i32)) as u32
                };
                bus.write_long(sp + 4, result);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // Long2Fix ($A83F)
            // Converts a LongInt to a Fixed (16.16) number.
            // FUNCTION Long2Fix (x: LongInt): Fixed;
            // Operating System Utilities, 3-43
            // Stack: [result(4)] [x(4)] — pop param, write result, SP += 4
            // Long2Fix ($A83F): Converts LONGINT to Fixed (16.16)
            (true, 0x03F) => {
                let sp = cpu.read_reg(Register::A7);
                let x = bus.read_long(sp) as i32;
                let result: u32 = if x > 0x7FFF {
                    0x7FFFFFFF
                } else if x < -0x8000 {
                    0x80000000
                } else {
                    (x << 16) as u32
                };
                bus.write_long(sp + 4, result);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // Fix2Long ($A840)
            // FUNCTION Fix2Long (x: Fixed): LongInt;
            // Inside Macintosh Volume V, V-593
            //
            // "Converts a Fixed-point number to a LongInt, rounding the
            //  fractional part of the result."
            //
            // Round-half-away-from-zero (0.5 → 1, -0.5 → -1, -1.5 → -2),
            // matching FixRound. The real Mac ROM uses the symmetric
            // away-from-zero convention.
            //
            // Stack: [result(4)] [x(4)] — pop param, write result, SP += 4
            // Fix2Long ($A840): Converts Fixed to LONGINT with round-half-up per IM:V V-593
            (true, 0x040) => {
                let sp = cpu.read_reg(Register::A7);
                let x = bus.read_long(sp) as i32 as i64;
                let abs_rounded = ((x.abs() + 0x8000) >> 16) as i32;
                let result = if x < 0 { -abs_rounded } else { abs_rounded };
                bus.write_long(sp + 4, result as u32);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // Fix2Frac ($A841)
            // Converts a Fixed value to a Fract value.
            // FUNCTION Fix2Frac(x: Fixed): Fract;
            // Operating System Utilities 1994, p. 3-44
            // Fixed = 16.16, Fract = 2.30; shift left by 14 bits.
            // Fix2Frac ($A841): Converts Fixed (16.16) to Fract (2.30) by
            // shifting left 14 with Fract-range saturation per OS Utils 3-44.
            (true, 0x041) => {
                let sp = cpu.read_reg(Register::A7);
                let x = bus.read_long(sp) as i32 as i64;
                let result = (x << 14).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
                bus.write_long(sp + 4, result as u32);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // Frac2Fix ($A842)
            // Converts a Fract value to a Fixed value.
            // FUNCTION Frac2Fix(x: Fract): Fixed;
            // Operating System Utilities 1994, p. 3-44
            // Fract = 2.30, Fixed = 16.16; shift right by 14 bits with rounding.
            // Frac2Fix ($A842): Converts Fract (2.30) to Fixed (16.16) by
            // shifting right 14 with nearest-value rounding per OS Utils 3-44.
            (true, 0x042) => {
                let sp = cpu.read_reg(Register::A7);
                let x = bus.read_long(sp) as i32 as i64;
                let result = ((x + (1 << 13)) >> 14).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
                bus.write_long(sp + 4, result as u32);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // Fix2X ($A843)
            // Converts a Fixed value to an Extended (80-bit SANE).
            // FUNCTION Fix2X(x: Fixed): Extended;
            // Operating System Utilities 1994, p. 3-45
            // Pascal convention for function returning Float80 (10 bytes):
            //   SP+0: x (Fixed, 4 bytes)
            //   SP+4: 10 bytes reserved for Extended return
            // Callee pops 4 bytes (x), leaves extended at SP.
            // Fix2X ($A843): Converts Fixed to 80-bit Extended SANE per OS Utils 3-45.
            (true, 0x043) => {
                let sp = cpu.read_reg(Register::A7);
                let x = bus.read_long(sp) as i32;
                let val = x as f64 / 65536.0;
                let ext = super::extended80::Extended80::from(val);
                ext.write_to_bus(bus, sp + 4);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // X2Fix ($A844)
            // Converts an Extended (80-bit SANE) to a Fixed value.
            // FUNCTION X2Fix(x: Extended): Fixed;
            // Operating System Utilities 1994, p. 3-45
            // Pascal convention for function returning Fixed (4 bytes):
            //   SP+0: x (Extended, 10 bytes)
            //   SP+10: 4 bytes reserved for Fixed return
            // Callee pops 10 bytes (x), leaves Fixed at SP.
            // X2Fix ($A844): Converts Extended to Fixed with saturation
            // semantics per OS Utils 3-45.
            (true, 0x044) => {
                let sp = cpu.read_reg(Register::A7);
                let ext = super::extended80::Extended80::read_from_bus(bus, sp);
                let val = f64::from(ext);
                let fixed = (val * 65536.0)
                    .round()
                    .clamp(i32::MIN as f64, i32::MAX as f64) as i32;
                bus.write_long(sp + 10, fixed as u32);
                cpu.write_reg(Register::A7, sp + 10);
                Ok(())
            }

            // Frac2X ($A845)
            // Converts a Fract value to an Extended (80-bit SANE).
            // FUNCTION Frac2X(x: Fract): Extended;
            // Operating System Utilities 1994, p. 3-46
            // Pascal convention for function returning Float80 (10 bytes):
            //   SP+0: x (Fract, 4 bytes)
            //   SP+4: 10 bytes reserved for Extended return
            // Callee pops 4 bytes (x), leaves extended at SP.
            // Frac2X ($A845): Converts Fract to 80-bit Extended SANE per OS Utils 3-46.
            (true, 0x045) => {
                let sp = cpu.read_reg(Register::A7);
                let x = bus.read_long(sp) as i32;
                let val = x as f64 / (1u64 << 30) as f64;
                let ext = super::extended80::Extended80::from(val);
                ext.write_to_bus(bus, sp + 4);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // X2Frac ($A846)
            // Converts an Extended (80-bit SANE) to a Fract value.
            // FUNCTION X2Frac(x: Extended): Fract;
            // Operating System Utilities 1994, p. 3-46
            // Pascal convention for function returning Fract (4 bytes):
            //   SP+0: x (Extended, 10 bytes)
            //   SP+10: 4 bytes reserved for Fract return
            // Callee pops 10 bytes (x), leaves Fract at SP.
            // X2Frac ($A846): Converts Extended to Fract with saturation
            // semantics per OS Utils 3-46.
            (true, 0x046) => {
                let sp = cpu.read_reg(Register::A7);
                let ext = super::extended80::Extended80::read_from_bus(bus, sp);
                let val = f64::from(ext);
                let fract = (val * (1u64 << 30) as f64)
                    .round()
                    .clamp(i32::MIN as f64, i32::MAX as f64) as i32;
                bus.write_long(sp + 10, fract as u32);
                cpu.write_reg(Register::A7, sp + 10);
                Ok(())
            }

            // FracCos ($A847)
            // Computes the cosine of a Fixed-point angle (in radians).
            // FUNCTION FracCos(x: Fixed): Fract;
            // Inside Macintosh Volume IV, IV-64
            // Operating System Utilities 1994, 3-42
            // Stack: SP+0=x(4), SP+4=result(4) → pops 4, writes result
            // FracCos ($A847): Computes cosine of Fixed radians, returns Fract per IM:IV IV-64 / OS Utils 3-42
            (true, 0x047) => {
                let sp = cpu.read_reg(Register::A7);
                let x = bus.read_long(sp) as i32;
                let radians = x as f64 / 65536.0;
                let cos_val = radians.cos();
                let fract = (cos_val * (1u64 << 30) as f64)
                    .round()
                    .clamp(i32::MIN as f64, i32::MAX as f64) as i32;
                bus.write_long(sp + 4, fract as u32);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // FracSin ($A848)
            // Computes the sine of a Fixed-point angle (in radians).
            // FUNCTION FracSin(x: Fixed): Fract;
            // Inside Macintosh Volume IV, IV-64
            // Operating System Utilities 1994, 3-42
            // FracSin ($A848): Computes sine of Fixed radians, returns Fract per IM:IV IV-64 / OS Utils 3-42
            (true, 0x048) => {
                let sp = cpu.read_reg(Register::A7);
                let x = bus.read_long(sp) as i32;
                let radians = x as f64 / 65536.0;
                let sin_val = radians.sin();
                let fract = (sin_val * (1u64 << 30) as f64)
                    .round()
                    .clamp(i32::MIN as f64, i32::MAX as f64) as i32;
                bus.write_long(sp + 4, fract as u32);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // FracSqrt ($A849)
            // Computes the square root of a Fract value.
            // FUNCTION FracSqrt(x: Fract): Fract;
            // Inside Macintosh Volume IV, IV-64
            // Operating System Utilities 1994, 3-41
            // FracSqrt ($A849): Interprets input as unsigned Fract 0..4-2^-30, returns unsigned Fract 0..2 per IM:IV IV-64 / OS Utils 3-41
            (true, 0x049) => {
                let sp = cpu.read_reg(Register::A7);
                let raw = bus.read_long(sp);
                let val = raw as f64 / (1u64 << 30) as f64;
                let sqrt_val = val.sqrt();
                let result = (sqrt_val * (1u64 << 30) as f64)
                    .round()
                    .clamp(0.0, (1u64 << 31) as f64) as u32;
                bus.write_long(sp + 4, result);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // FracMul ($A84A)
            // Multiplies two Fract values.
            // FUNCTION FracMul(x, y: Fract): Fract;
            // Inside Macintosh Volume I, I-468
            // Fract = 2.30; product uses 64-bit intermediate, shift right by 30.
            // FracMul ($A84A): Multiplies two Fract values with 64-bit intermediate per IM:I I-468
            (true, 0x04A) => {
                let sp = cpu.read_reg(Register::A7);
                let x = bus.read_long(sp) as i32 as i64;
                let y = bus.read_long(sp + 4) as i32 as i64;
                let product = x * y;
                let rounding_bias = 1i64 << 29;
                let rounded = if product >= 0 {
                    product + rounding_bias
                } else {
                    product - rounding_bias
                };
                let result = (rounded >> 30).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
                bus.write_long(sp + 8, result as u32);
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // FracDiv ($A84B)
            // Divides two Fract values.
            // FUNCTION FracDiv(x, y: Fract): Fract;
            // Inside Macintosh Volume I, I-468
            //
            // Pascal left-to-right push: x first (SP+4), y last (SP+0).
            // Computes x/y. FracMul (commutative) and FracDiv share
            // identically-ordered reads but only FracDiv is non-commutative.
            // FracDiv ($A84B): Divides two Fract values; saturates on divide-by-zero per IM:I I-468
            (true, 0x04B) => {
                let sp = cpu.read_reg(Register::A7);
                let y = bus.read_long(sp) as i32 as i64;
                let x = bus.read_long(sp + 4) as i32 as i64;
                let result = if y == 0 {
                    if x >= 0 {
                        i32::MAX
                    } else {
                        i32::MIN
                    }
                } else {
                    ((x << 30) / y).clamp(i32::MIN as i64, i32::MAX as i64) as i32
                };
                bus.write_long(sp + 8, result as u32);
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // FixDiv ($A84D)
            // Divides two Fixed values.
            // FUNCTION FixDiv(x, y: Fixed): Fixed;
            // Inside Macintosh Volume I, I-467
            //
            // Pascal left-to-right push: x first (lands at SP+4), y last
            // (lands at SP+0). Handler computes x / y — the operation is
            // NOT commutative so reversed reads produce reciprocal results.
            // FixDiv ($A84D): Divides two Fixed values; saturates on divide-by-zero per IM:I I-467
            (true, 0x04D) => {
                let sp = cpu.read_reg(Register::A7);
                let y = bus.read_long(sp) as i32 as i64;
                let x = bus.read_long(sp + 4) as i32 as i64;
                let result = if y == 0 {
                    if x >= 0 {
                        i32::MAX
                    } else {
                        i32::MIN
                    }
                } else {
                    ((x << 16) / y).clamp(i32::MIN as i64, i32::MAX as i64) as i32
                };
                bus.write_long(sp + 8, result as u32);
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // FixATan2 ($A818)
            // Computes the arctangent of y/x, returning a Fixed-point angle in radians.
            // FUNCTION FixATan2(x, y: LongInt): Fixed;
            // Inside Macintosh Volume IV (1986), p. IV-65 (Toolbox Utilities — Fixed-Point Arithmetic).
            // Operating System Utilities (1994), pp. 3-38..3-47.
            // FixMath.h Universal Headers: ONEWORDINLINE(0xA818).
            //
            // Per IM:IV IV-65: "FixATan2 returns the arctangent of y / x in
            // radians." Note that FixATan2 effects "arctan(type / type) ->
            // Fixed":
            //     arctan(LONGINT / LONGINT) -> Fixed
            //     arctan(Fixed   / Fixed  ) -> Fixed
            //     arctan(Fract   / Fract  ) -> Fixed
            // i.e. only the *ratio* y/x matters; absolute scale is irrelevant.
            // The result is a Fixed-point angle in radians in (-pi, pi].
            //
            // Stack frame (Pascal FUNCTION, 8 bytes arg + 4 bytes result):
            //   SP+0  y       LONGINT (4 bytes — pushed last in Pascal LTR)
            //   SP+4  x       LONGINT (4 bytes — pushed first)
            //   SP+8  result  Fixed   (4-byte function-result slot)
            //
            // The trap pops 8 argument bytes and writes the 4-byte Fixed
            // result into the slot at the former SP+8.
            //
            // IM:IV IV-65 documented examples (verified bit-exact against
            // BasiliskII System 7.5.3 by a818_fixatan2_strict):
            //     FixATan2(X2Fix( 1.0), X2Fix( 1.0)) = $0000C910 (X2Fix(pi/4))
            //     FixATan2(X2Fix(-1.0), X2Fix(-1.0)) = $FFFDA4D0 (-3*X2Fix(pi/4))
            //
            // Apple's ROM uses a Cordic algorithm whose pi/4 approximation
            // (0x0000C910 = 0.78546906) differs from IEEE-754 pi/4
            // (0.78539816) by ~7e-5. Systemless uses f64::atan2 then multiplies
            // by 65536 and rounds half-to-even — at the 16-bit Fixed
            // precision this happens to round to the same hex value Apple's
            // Cordic returns for the IM-documented inputs.
            //
            // Regression coverage:
            //   a818_fixatan2_strict (5 of 5 BII-witnessed)
            //   src/trap/toolbox.rs::fixatan2_returns_im_documented_pi_over_four_for_one_one
            //   src/trap/toolbox.rs::fixatan2_only_ratio_matters_scale_invariance
            (true, 0x018) => {
                let sp = cpu.read_reg(Register::A7);
                let y = bus.read_long(sp) as i32;
                let x = bus.read_long(sp + 4) as i32;
                let angle = (y as f64).atan2(x as f64);
                let fixed = (angle * 65536.0)
                    .round()
                    .clamp(i32::MIN as f64, i32::MAX as f64) as i32;
                bus.write_long(sp + 8, fixed as u32);
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // SpaceExtra ($A88E)
            // Sets the spExtra field of the current GrafPort.
            // PROCEDURE SpaceExtra(extra: Fixed);
            // Inside Macintosh Volume I, I-171
            //
            // Regression coverage:
            //   spaceextra_sets_port_spextra
            //   spaceextra_pops_four_bytes
            // SpaceExtra ($A88E): Sets spExtra field in port per IM:I I-171
            (true, 0x08E) => {
                let sp = cpu.read_reg(Register::A7);
                let extra = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                // Store spExtra at port offset +76 (Fixed)
                if self.current_port != 0 {
                    bus.write_long(self.current_port + 76, extra);
                }
                Ok(())
            }

            // NOTE: GetPen ($A89A) lives in quickdraw.rs at the same
            // slot (true, 0x09A). A near-identical handler used to live
            // here too, but it was dead code — dispatch_quickdraw runs
            // before dispatch_toolbox so the quickdraw.rs handler
            // always won. Removed so there's one canonical implementation.

            // NOTE: EqualRgn ($A8E3) lives in quickdraw.rs at slot 0x0E3.
            // A bbox-only stub used to live here at the correct slot,
            // but it was dead code — dispatch_quickdraw runs before
            // dispatch_toolbox so the quickdraw.rs handler always won.
            // The quickdraw.rs implementation now uses the canonical
            // rgnSize-byte comparison (Executor C_EqualRgn,
            // qRegion.cpp:1493, Inside Macintosh Volume I, I-183).

            // NOTE: FillArc ($A8C2) lives in quickdraw.rs at the same
            // slot (true, 0x0C2). A near-identical handler used to live
            // here too, but it was dead code — dispatch_quickdraw runs
            // before dispatch_toolbox so the quickdraw.rs handler
            // always won. Removed so there's one canonical implementation.

            // NOTE: PtToAngle ($A8C3) is dispatched by quickdraw.rs (see
            // `(true, 0x0C3)` there) using the correct 12-byte stack frame
            // (angle_ptr(4) + pt(4) + rect_ptr(4)). A duplicate stub used
            // to live here that treated Rect as an inline 8-byte record
            // and popped 16 bytes — dead code because dispatch tries
            // quickdraw.rs first, but a trap if the order ever changed.
            // Removed so there's one canonical PtToAngle implementation.

            // NOTE: FillPoly ($A8CA) lives in quickdraw.rs at the same
            // slot (true, 0x0CA). A near-identical handler used to live
            // here too, but it was dead code — dispatch_quickdraw runs
            // before dispatch_toolbox so the quickdraw.rs handler
            // always won. The quickdraw.rs version additionally handles
            // OpenRgn-recording (folds polyBBox into recording_region).
            // Removed so there's one canonical implementation.

            // PackBits ($A8CF)
            // PROCEDURE PackBits(VAR srcPtr: Ptr; VAR dstPtr: Ptr; srcBytes: INTEGER);
            // Params: 4+4+2 = 10
            // PackBits ($A8CF)
            // Compresses srcBytes of data using run-length encoding.
            // PROCEDURE PackBits (VAR srcPtr, dstPtr: Ptr; srcBytes: INTEGER);
            // Inside Macintosh Volume I, I-470
            //
            // "PackBits compresses srcBytes bytes of data starting at
            //  srcPtr and stores the compressed data at dstPtr. Bytes
            //  are compressed when there are three or more consecutive
            //  equal bytes. After the data is compressed, srcPtr is
            //  incremented by srcBytes and dstPtr is incremented by the
            //  number of bytes that the data was compressed to."
            //
            // Encoding: flag byte N followed by data.
            //   N in 0..=127   → copy next N+1 bytes literally
            //   N in -1..=-127 → repeat next byte 1-N times
            //   N = -128       → no-op
            //
            // Stack: SP+0=srcBytes(2), SP+2=dstPtr_ptr(4), SP+6=srcPtr_ptr(4). Pop 10.
            //
            // Regression coverage:
            //   packbits_compresses_run_of_equal_bytes
            //   packbits_handles_literal_sequences
            // PackBits ($A8CF): Run-length encodes srcBytes of data; advances VAR srcPtr/dstPtr; per IM:I I-470
            (true, 0x0CF) => {
                let sp = cpu.read_reg(Register::A7);
                let src_bytes = bus.read_word(sp) as i16 as i32;
                let dst_ptr_ptr = bus.read_long(sp + 2);
                let src_ptr_ptr = bus.read_long(sp + 6);
                cpu.write_reg(Register::A7, sp + 10);

                if src_ptr_ptr != 0 && dst_ptr_ptr != 0 && src_bytes > 0 {
                    let mut src = bus.read_long(src_ptr_ptr);
                    let mut dst = bus.read_long(dst_ptr_ptr);
                    let src_end = src + src_bytes as u32;

                    while src < src_end {
                        // Find run length
                        let cur = bus.read_byte(src);
                        let mut run_len = 1u32;
                        while src + run_len < src_end
                            && bus.read_byte(src + run_len) == cur
                            && run_len < 128
                        {
                            run_len += 1;
                        }

                        if run_len >= 3 {
                            // Encode as repeat: flag = -(run_len - 1)
                            bus.write_byte(dst, (-(run_len as i32 - 1)) as u8);
                            dst += 1;
                            bus.write_byte(dst, cur);
                            dst += 1;
                            src += run_len;
                        } else {
                            // Collect literal bytes
                            let lit_start = src;
                            let mut lit_len = 0u32;
                            while src + lit_len < src_end && lit_len < 128 {
                                let b = bus.read_byte(src + lit_len);
                                let mut ahead = 1u32;
                                while src + lit_len + ahead < src_end
                                    && bus.read_byte(src + lit_len + ahead) == b
                                    && ahead < 3
                                {
                                    ahead += 1;
                                }
                                if ahead >= 3 && lit_len > 0 {
                                    break;
                                }
                                lit_len += 1;
                            }
                            // Write literal: flag = lit_len - 1
                            bus.write_byte(dst, (lit_len - 1) as u8);
                            dst += 1;
                            for i in 0..lit_len {
                                bus.write_byte(dst, bus.read_byte(lit_start + i));
                                dst += 1;
                            }
                            src += lit_len;
                        }
                    }

                    bus.write_long(src_ptr_ptr, src);
                    bus.write_long(dst_ptr_ptr, dst);
                }
                Ok(())
            }

            // UnpackBits ($A8D0)
            // Expands data previously compressed by PackBits.
            // PROCEDURE UnpackBits (VAR srcPtr, dstPtr: Ptr; dstBytes: INTEGER);
            // Inside Macintosh Volume I, I-470
            //
            // "Given in srcPtr a pointer to data that was compressed by
            //  PackBits, UnpackBits expands the data and stores the
            //  result at dstPtr. DstBytes is the length that the
            //  expanded data will be."
            //
            // Stack: SP+0=dstBytes(2), SP+2=dstPtr_ptr(4), SP+6=srcPtr_ptr(4). Pop 10.
            //
            // Regression coverage:
            //   unpackbits_expands_packbits_output
            // UnpackBits ($A8D0): Expands PackBits-compressed data into dstBytes; advances VAR srcPtr/dstPtr; per IM:I I-470
            (true, 0x0D0) => {
                let sp = cpu.read_reg(Register::A7);
                let dst_bytes = bus.read_word(sp) as i16 as i32;
                let dst_ptr_ptr = bus.read_long(sp + 2);
                let src_ptr_ptr = bus.read_long(sp + 6);
                cpu.write_reg(Register::A7, sp + 10);

                if src_ptr_ptr != 0 && dst_ptr_ptr != 0 && dst_bytes > 0 {
                    let mut src = bus.read_long(src_ptr_ptr);
                    let mut dst = bus.read_long(dst_ptr_ptr);
                    let dst_end = dst + dst_bytes as u32;

                    while dst < dst_end {
                        let flag = bus.read_byte(src) as i8;
                        src += 1;

                        if flag >= 0 {
                            // Literal: copy next flag+1 bytes
                            let count = (flag as u32) + 1;
                            for _ in 0..count {
                                if dst >= dst_end {
                                    break;
                                }
                                bus.write_byte(dst, bus.read_byte(src));
                                src += 1;
                                dst += 1;
                            }
                        } else if flag != -128 {
                            // Repeat: next byte repeated 1-flag times
                            let count = (1 - flag as i32) as u32;
                            let val = bus.read_byte(src);
                            src += 1;
                            for _ in 0..count {
                                if dst >= dst_end {
                                    break;
                                }
                                bus.write_byte(dst, val);
                                dst += 1;
                            }
                        }
                        // flag == -128 is a no-op
                    }

                    bus.write_long(src_ptr_ptr, src);
                    bus.write_long(dst_ptr_ptr, dst);
                }
                Ok(())
            }

            // NOTE: FillRgn ($A8D6) is dispatched by quickdraw.rs (see
            // `(true, 0x0D6)` there). A duplicate stub used to live here
            // with the wrong 12-byte pop; it was dead code because dispatch
            // tries quickdraw.rs first, but the broken layout was still a
            // trap waiting to happen if the dispatch order ever changed.
            // Removed to keep the stack layout correct in one place.

            // PicComment ($A8F2)
            // PROCEDURE PicComment(kind: INTEGER; dataSize: INTEGER; dataHandle: Handle);
            // Params: 2+2+4 = 8
            // PicComment ($A8F2): Pops 8 bytes (kind + dataSize + dataHandle); PICT comments not interpreted per IM:I I-190
            (true, 0x0F2) => {
                let sp = cpu.read_reg(Register::A7);
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // ========== Package Dispatchers ==========
            // Pack0-Pack7 ($A9E7-$A9EF-0x1EF) are selector-based dispatchers.
            // The selector word at SP encodes the sub-routine. The high byte often
            // gives the parameter size (excluding the selector itself).

            // Pack0 ($A9E7) — List Manager
            // Pack0 / List Manager ($A9E7): Selector-based: $44 LNew,
            // $18 LClick, $20 LDispose, $24 LAddRow, $30 LAddToCell,
            // $34 LDelRow, $3C LGetSelect, $40 LLastClick, $5C
            // LSetSelect, $54 LDoDraw, plus a small set of list-state
            // mutators/query helpers. List backing store maintained per
            // Inside Macintosh Volume IV.
            (true, 0x1E7) => {
                let sp = cpu.read_reg(Register::A7);
                let selector = bus.read_word(sp);
                if trace_list_manager_enabled() {
                    eprintln!(
                        "[LIST] selector=${:04X} pc=${:08X} sp=${:08X}",
                        selector,
                        cpu.read_reg(Register::PC),
                        sp,
                    );
                }

                match selector {
                    // LNew (selector 68 / $44)
                    // Creates a new list and returns a ListHandle.
                    // FUNCTION LNew(rView, dataBounds: Rect; cSize: Point; theProc: INTEGER;
                    //   theWindow: WindowPtr; drawIt, hasGrow, scrollHoriz, scrollVert: BOOLEAN): ListHandle;
                    // Inside Macintosh Volume IV, IV-269 to IV-270
                    0x44 => {
                        let draw_it = Self::stack_bool_slot(bus, sp + 2);
                        let has_grow = Self::stack_bool_slot(bus, sp + 4);
                        let scroll_h = Self::stack_bool_slot(bus, sp + 6);
                        let scroll_v = Self::stack_bool_slot(bus, sp + 8);
                        let window = bus.read_long(sp + 10);
                        let proc_id = bus.read_word(sp + 14) as i16;
                        let cell_size = Self::read_stack_point(bus, sp + 16);
                        let data_bounds_ptr = bus.read_long(sp + 20);
                        let view_rect_ptr = bus.read_long(sp + 24);
                        let result_addr = sp + 28;

                        let view_rect = Self::read_rect_ptr(bus, view_rect_ptr);
                        let data_bounds = Self::read_rect_ptr(bus, data_bounds_ptr);
                        let resolved_cell_size =
                            self.compute_list_cell_size(view_rect, data_bounds, cell_size);
                        let visible = Self::compute_list_visible_rect(
                            view_rect,
                            data_bounds,
                            resolved_cell_size,
                        );

                        let list_ptr = bus.alloc(Self::LIST_RECORD_SIZE);
                        let list_handle = bus.alloc(4);
                        let cells_handle = bus.alloc(4);

                        let list_def_proc_handle = self
                            .find_resource_any(*b"LDEF", proc_id)
                            .map(|(_, ptr)| {
                                self.get_or_create_resource_handle(bus, *b"LDEF", proc_id, ptr)
                            })
                            .unwrap_or(0);

                        if list_handle != 0 {
                            bus.write_long(list_handle, list_ptr);
                        }
                        if cells_handle != 0 {
                            bus.write_long(cells_handle, 0);
                        }

                        let state = super::dispatch::ListState {
                            view_rect,
                            data_bounds,
                            cell_size: resolved_cell_size,
                            visible,
                            port: window,
                            draw_enabled: draw_it,
                            cells: std::collections::HashMap::new(),
                            selected: std::collections::BTreeSet::new(),
                            last_click: Self::list_no_click_cell(),
                            last_click_tick: 0,
                        };

                        if list_ptr != 0 {
                            Self::write_rect_words(
                                bus,
                                list_ptr + Self::LIST_RVIEW_OFFSET,
                                view_rect,
                            );
                            bus.write_long(list_ptr + Self::LIST_PORT_OFFSET, window);
                            Self::write_point_words(
                                bus,
                                list_ptr + Self::LIST_INDENT_OFFSET,
                                (0, 0),
                            );
                            Self::write_point_words(
                                bus,
                                list_ptr + Self::LIST_CELL_SIZE_OFFSET,
                                resolved_cell_size,
                            );
                            Self::write_rect_words(
                                bus,
                                list_ptr + Self::LIST_VISIBLE_OFFSET,
                                visible,
                            );
                            bus.write_long(list_ptr + Self::LIST_VSCROLL_OFFSET, 0);
                            bus.write_long(list_ptr + Self::LIST_HSCROLL_OFFSET, 0);
                            bus.write_byte(list_ptr + Self::LIST_SEL_FLAGS_OFFSET, 0);
                            bus.write_byte(list_ptr + Self::LIST_ACTIVE_OFFSET, 1);
                            bus.write_byte(list_ptr + Self::LIST_RESERVED_OFFSET, 0);
                            bus.write_byte(list_ptr + Self::LIST_FLAGS_OFFSET, 0);
                            bus.write_long(list_ptr + Self::LIST_CLICK_TIME_OFFSET, 0);
                            Self::write_point_words(
                                bus,
                                list_ptr + Self::LIST_CLICK_LOC_OFFSET,
                                (-32768, -32768),
                            );
                            Self::write_point_words(
                                bus,
                                list_ptr + Self::LIST_MOUSE_LOC_OFFSET,
                                (-1, -1),
                            );
                            bus.write_long(list_ptr + Self::LIST_CLICK_LOOP_OFFSET, 0);
                            Self::write_point_words(
                                bus,
                                list_ptr + Self::LIST_LAST_CLICK_OFFSET,
                                (-1, -1),
                            );
                            bus.write_long(list_ptr + Self::LIST_REFCON_OFFSET, 0);
                            bus.write_long(
                                list_ptr + Self::LIST_DEF_PROC_OFFSET,
                                list_def_proc_handle,
                            );
                            bus.write_long(list_ptr + Self::LIST_USER_HANDLE_OFFSET, 0);
                            Self::write_rect_words(
                                bus,
                                list_ptr + Self::LIST_DATA_BOUNDS_OFFSET,
                                data_bounds,
                            );
                            bus.write_long(list_ptr + Self::LIST_CELLS_OFFSET, cells_handle);
                            let rows = (data_bounds.2 - data_bounds.0).max(0) as i32;
                            let cols = (data_bounds.3 - data_bounds.1).max(0) as i32;
                            bus.write_word(
                                list_ptr + Self::LIST_MAX_INDEX_OFFSET,
                                rows.saturating_mul(cols).saturating_mul(2) as u16,
                            );
                            bus.write_word(list_ptr + Self::LIST_CELL_ARRAY_OFFSET, 0);
                        }

                        if list_handle != 0 {
                            self.list_states.insert(list_handle, state);
                        }

                        if trace_list_manager_enabled() {
                            eprintln!(
                                "[LIST] LNew handle=${:08X} ptr=${:08X} proc={} draw={} grow={} scroll_h={} scroll_v={} view=({},{},{},{}) bounds=({},{},{},{}) cell=({}, {})",
                                list_handle,
                                list_ptr,
                                proc_id,
                                draw_it,
                                has_grow,
                                scroll_h,
                                scroll_v,
                                view_rect.0,
                                view_rect.1,
                                view_rect.2,
                                view_rect.3,
                                data_bounds.0,
                                data_bounds.1,
                                data_bounds.2,
                                data_bounds.3,
                                resolved_cell_size.0,
                                resolved_cell_size.1,
                            );
                        }

                        bus.write_long(result_addr, list_handle);
                        cpu.write_reg(Register::A7, result_addr);
                        Ok(())
                    }

                    // LDoDraw (selector 44 / $2C)
                    // Enables or disables automatic drawing for a list.
                    // PROCEDURE LDoDraw(drawIt: BOOLEAN; lHandle: ListHandle);
                    // Inside Macintosh Volume IV, IV-275
                    0x2C => {
                        let list_handle = bus.read_long(sp + 2);
                        let draw_it = Self::stack_bool_slot(bus, sp + 6);
                        if let Some(state) = self.list_states.get_mut(&list_handle) {
                            state.draw_enabled = draw_it;
                        }
                        cpu.write_reg(Register::A7, sp + 8);
                        Ok(())
                    }

                    // LCellSize (selector 20 / $14)
                    // Sets the pixel size of each cell.
                    // PROCEDURE LCellSize(cSize: Point; lHandle: ListHandle);
                    // Inside Macintosh Volume IV, IV-272 to IV-273
                    0x14 => {
                        let list_handle = bus.read_long(sp + 2);
                        let cell_size = Self::read_stack_point(bus, sp + 6);
                        if let Some(state) = self.list_states.get_mut(&list_handle) {
                            state.cell_size = (cell_size.0.max(1), cell_size.1.max(1));
                            state.visible = Self::compute_list_visible_rect(
                                state.view_rect,
                                state.data_bounds,
                                state.cell_size,
                            );
                            Self::sync_list_state_to_guest(bus, list_handle, state);
                        }
                        cpu.write_reg(Register::A7, sp + 10);
                        Ok(())
                    }

                    // LAddRow (selector 8 / $08)
                    // Inserts rows into the list and returns the first added row.
                    // FUNCTION LAddRow(count, rowNum: INTEGER; lHandle: ListHandle): INTEGER;
                    // Inside Macintosh Volume IV, IV-271
                    // Pascal calling convention pushes args left-to-right
                    // with the first arg DEEPEST; lHandle (the last arg) lands
                    // closest to the selector. Stack at entry: sel(2) +
                    // lHandle(4) + rowNum(2) + count(2) + result(2) = 12.
                    0x08 => {
                        let list_handle = bus.read_long(sp + 2);
                        let mut row = bus.read_word(sp + 6) as i16;
                        let count = bus.read_word(sp + 8) as i16;
                        let result_addr = sp + 10;

                        let mut result_row = row;
                        if let Some(state) = self.list_states.get_mut(&list_handle) {
                            row = row.clamp(state.data_bounds.0, state.data_bounds.2);
                            result_row = row;

                            if count > 0 {
                                let mut moved = std::collections::HashMap::new();
                                for ((cell_row, cell_col), data) in state.cells.drain() {
                                    let new_row = if cell_row >= row {
                                        cell_row + count
                                    } else {
                                        cell_row
                                    };
                                    moved.insert((new_row, cell_col), data);
                                }
                                state.cells = moved;

                                let moved_selected: std::collections::BTreeSet<_> = state
                                    .selected
                                    .iter()
                                    .map(|&(cell_row, cell_col)| {
                                        let new_row = if cell_row >= row {
                                            cell_row + count
                                        } else {
                                            cell_row
                                        };
                                        (new_row, cell_col)
                                    })
                                    .collect();
                                state.selected = moved_selected;
                                state.data_bounds.2 += count;
                                state.visible = Self::compute_list_visible_rect(
                                    state.view_rect,
                                    state.data_bounds,
                                    state.cell_size,
                                );
                                Self::sync_list_state_to_guest(bus, list_handle, state);
                            }
                        }

                        bus.write_word(result_addr, result_row as u16);
                        cpu.write_reg(Register::A7, result_addr);
                        Ok(())
                    }

                    // LDelRow (selector 36 / $24)
                    // Deletes rows from the list.
                    // PROCEDURE LDelRow(count, rowNum: INTEGER; lHandle: ListHandle);
                    // Inside Macintosh Volume IV, IV-271
                    // Pascal calling: sel(2) + lHandle(4) + rowNum(2) +
                    // count(2) = 10; no result slot.
                    0x24 => {
                        let list_handle = bus.read_long(sp + 2);
                        let row = bus.read_word(sp + 6) as i16;
                        let count = bus.read_word(sp + 8) as i16;

                        if let Some(state) = self.list_states.get_mut(&list_handle) {
                            if count > 0 && row < state.data_bounds.2 {
                                state.cells.retain(|&(cell_row, _), _| {
                                    cell_row < row || cell_row >= row + count
                                });
                                let moved = state
                                    .cells
                                    .drain()
                                    .map(|((cell_row, cell_col), data)| {
                                        let new_row = if cell_row >= row + count {
                                            cell_row - count
                                        } else {
                                            cell_row
                                        };
                                        ((new_row, cell_col), data)
                                    })
                                    .collect();
                                state.cells = moved;
                                state.selected = state
                                    .selected
                                    .iter()
                                    .filter_map(|&(cell_row, cell_col)| {
                                        if cell_row >= row && cell_row < row + count {
                                            None
                                        } else {
                                            let new_row = if cell_row >= row + count {
                                                cell_row - count
                                            } else {
                                                cell_row
                                            };
                                            Some((new_row, cell_col))
                                        }
                                    })
                                    .collect();
                                state.data_bounds.2 =
                                    (state.data_bounds.2 - count).max(state.data_bounds.0);
                                state.visible = Self::compute_list_visible_rect(
                                    state.view_rect,
                                    state.data_bounds,
                                    state.cell_size,
                                );
                                Self::sync_list_state_to_guest(bus, list_handle, state);
                            }
                        }

                        cpu.write_reg(Register::A7, sp + 10);
                        Ok(())
                    }

                    // LSetCell (selector 88 / $58)
                    // Replaces the contents of a cell.
                    // PROCEDURE LSetCell(dataPtr: Ptr; dataLen: INTEGER; theCell: Cell; lHandle: ListHandle);
                    // Inside Macintosh Volume IV, IV-272
                    0x58 => {
                        let list_handle = bus.read_long(sp + 2);
                        let cell = Self::read_stack_point(bus, sp + 6);
                        let data_len = bus.read_word(sp + 10) as i16;
                        let data_ptr = bus.read_long(sp + 12);
                        let data = if data_len > 0 && data_ptr != 0 {
                            bus.read_bytes(data_ptr, data_len as usize)
                        } else {
                            Vec::new()
                        };
                        if let Some(state) = self.list_states.get_mut(&list_handle) {
                            let key = (cell.0, cell.1);
                            let valid = Self::list_cell_is_valid(state, key.0, key.1);
                            if trace_list_manager_enabled() {
                                eprintln!(
                                    "[LIST] LSetCell handle=${:08X} cell=({}, {}) len={} valid={} text=\"{}\"",
                                    list_handle,
                                    key.0,
                                    key.1,
                                    data_len,
                                    valid,
                                    Self::list_cell_text(&data),
                                );
                            }
                            if valid {
                                if !data.is_empty() {
                                    state.cells.insert(key, data);
                                } else {
                                    state.cells.remove(&key);
                                }
                            }
                        }
                        cpu.write_reg(Register::A7, sp + 16);
                        Ok(())
                    }
                    // LAddToCell (selector 12 / $0C)
                    // Appends bytes to the contents of a cell.
                    // PROCEDURE LAddToCell(dataPtr: Ptr; dataLen: INTEGER; theCell: Cell; lHandle: ListHandle);
                    // Inside Macintosh Volume IV, IV-272; More Macintosh Toolbox 1993, pp. 4-80 to 4-81.
                    0x0C => {
                        let list_handle = bus.read_long(sp + 2);
                        let cell = Self::read_stack_point(bus, sp + 6);
                        let data_len = bus.read_word(sp + 10) as i16;
                        let data_ptr = bus.read_long(sp + 12);
                        if data_len > 0 && data_ptr != 0 {
                            let data = bus.read_bytes(data_ptr, data_len as usize);
                            if let Some(state) = self.list_states.get_mut(&list_handle) {
                                let key = (cell.0, cell.1);
                                if Self::list_cell_is_valid(state, key.0, key.1) {
                                    state.cells.entry(key).or_default().extend_from_slice(&data);
                                }
                            }
                        }
                        cpu.write_reg(Register::A7, sp + 16);
                        Ok(())
                    }

                    // LGetCell (selector 56 / $38)
                    // Copies the contents of a cell into the caller's buffer.
                    // PROCEDURE LGetCell(dataPtr: Ptr; VAR dataLen: INTEGER; theCell: Cell; lHandle: ListHandle);
                    // Inside Macintosh Volume IV, IV-272
                    0x38 => {
                        let list_handle = bus.read_long(sp + 2);
                        let cell = Self::read_stack_point(bus, sp + 6);
                        let data_len_ptr = bus.read_long(sp + 10);
                        let data_ptr = bus.read_long(sp + 14);

                        if data_len_ptr != 0 {
                            let max_len = bus.read_word(data_len_ptr) as usize;
                            let data = self
                                .list_states
                                .get(&list_handle)
                                .and_then(|state| state.cells.get(&(cell.0, cell.1)).cloned())
                                .unwrap_or_default();
                            let copy_len = data.len().min(max_len);
                            if copy_len > 0 && data_ptr != 0 {
                                bus.write_bytes(data_ptr, &data[..copy_len]);
                            }
                            bus.write_word(data_len_ptr, copy_len as u16);
                        }

                        cpu.write_reg(Register::A7, sp + 18);
                        Ok(())
                    }

                    // LClrCell (selector 28 / $1C)
                    // Clears the contents of a cell.
                    // PROCEDURE LClrCell(theCell: Cell; lHandle: ListHandle);
                    // Inside Macintosh Volume IV, IV-272
                    0x1C => {
                        let list_handle = bus.read_long(sp + 2);
                        let cell = Self::read_stack_point(bus, sp + 6);
                        if let Some(state) = self.list_states.get_mut(&list_handle) {
                            state.cells.remove(&(cell.0, cell.1));
                        }
                        cpu.write_reg(Register::A7, sp + 10);
                        Ok(())
                    }

                    // LSetSelect (selector 92 / $5C)
                    // Selects or deselects a cell.
                    // PROCEDURE LSetSelect(setIt: BOOLEAN; theCell: Cell; lHandle: ListHandle);
                    // Inside Macintosh Volume IV, IV-273
                    0x5C => {
                        let list_handle = bus.read_long(sp + 2);
                        let cell = Self::read_stack_point(bus, sp + 6);
                        let set_it = Self::stack_bool_slot(bus, sp + 10);
                        let list_ptr = Self::list_record_ptr(bus, list_handle);
                        let mut draw_state = None;
                        let mut draw_cells = Vec::new();
                        if let Some(state) = self.list_states.get_mut(&list_handle) {
                            let valid = Self::list_cell_is_valid(state, cell.0, cell.1);
                            if trace_list_manager_enabled() {
                                eprintln!(
                                    "[LIST] LSetSelect handle=${:08X} cell=({}, {}) set={} valid={}",
                                    list_handle, cell.0, cell.1, set_it, valid,
                                );
                            }
                            if valid {
                                let previous_selected = state.selected.clone();
                                let single_select = list_ptr != 0
                                    && (bus.read_byte(list_ptr + Self::LIST_SEL_FLAGS_OFFSET)
                                        & 0x80)
                                        != 0;
                                if set_it {
                                    if single_select {
                                        state.selected.clear();
                                    }
                                    state.selected.insert((cell.0, cell.1));
                                } else {
                                    state.selected.remove(&(cell.0, cell.1));
                                }
                                if state.draw_enabled {
                                    draw_cells = previous_selected
                                        .symmetric_difference(&state.selected)
                                        .copied()
                                        .collect();
                                    if !draw_cells.is_empty() {
                                        draw_state = Some(state.clone());
                                    }
                                }
                            }
                        }
                        if let Some(state) = draw_state {
                            if self.draw_list_cells_with_ldef_message(
                                cpu,
                                bus,
                                list_handle,
                                &state,
                                draw_cells.clone(),
                                Self::LIST_LHILITE_MSG,
                                12,
                            ) {
                                return Some(Ok(()));
                            }
                            for cell in draw_cells {
                                self.draw_list_fallback(cpu, bus, &state, Some(cell));
                            }
                        }
                        cpu.write_reg(Register::A7, sp + 12);
                        Ok(())
                    }

                    // LGetSelect (selector 60 / $3C)
                    // Returns whether a cell is selected, or finds the next selected cell.
                    // FUNCTION LGetSelect(next: BOOLEAN; VAR theCell: Cell; lHandle: ListHandle): BOOLEAN;
                    // Inside Macintosh Volume IV, IV-273
                    0x3C => {
                        let list_handle = bus.read_long(sp + 2);
                        let cell_ptr = bus.read_long(sp + 6);
                        let next = Self::stack_bool_slot(bus, sp + 10);
                        let result_addr = sp + 12;

                        let mut found = None;
                        if let Some(state) = self.list_states.get(&list_handle) {
                            if next {
                                let start = if cell_ptr != 0 {
                                    (
                                        bus.read_word(cell_ptr) as i16,
                                        bus.read_word(cell_ptr + 2) as i16,
                                    )
                                } else {
                                    (state.data_bounds.0, state.data_bounds.1)
                                };
                                found = state
                                    .selected
                                    .iter()
                                    .copied()
                                    .find(|&(row, col)| (row, col) >= start);
                            } else if cell_ptr != 0 {
                                let cell = (
                                    bus.read_word(cell_ptr) as i16,
                                    bus.read_word(cell_ptr + 2) as i16,
                                );
                                if state.selected.contains(&cell) {
                                    found = Some(cell);
                                }
                            }
                        }

                        if let Some(cell) = found {
                            if cell_ptr != 0 {
                                Self::write_point_words(bus, cell_ptr, cell);
                            }
                            bus.write_word(result_addr, 0x0100);
                        } else {
                            bus.write_word(result_addr, 0);
                        }
                        cpu.write_reg(Register::A7, result_addr);
                        Ok(())
                    }

                    // LLastClick (selector 64 / $40)
                    // Returns the last clicked cell; before any click the
                    // documented sentinel is Cell(-1, -1).
                    // FUNCTION LLastClick(lHandle: ListHandle): Cell;
                    // Inside Macintosh Volume IV, IV-273
                    0x40 => {
                        let list_handle = bus.read_long(sp + 2);
                        let result_addr = sp + 6;
                        let last_click = self
                            .list_states
                            .get(&list_handle)
                            .map(|state| state.last_click)
                            .unwrap_or_else(Self::list_no_click_cell);
                        Self::write_point_words(bus, result_addr, last_click);
                        cpu.write_reg(Register::A7, result_addr);
                        Ok(())
                    }

                    // LClick (selector 24 / $18)
                    // Tracks mouse selection in a list and returns TRUE on double-click.
                    // FUNCTION LClick(pt: Point; modifiers: INTEGER; lHandle: ListHandle): BOOLEAN;
                    // Inside Macintosh Volume IV, IV-273
                    0x18 => {
                        let list_handle = bus.read_long(sp + 2);
                        let _modifiers = bus.read_word(sp + 6);
                        let point = Self::read_stack_point(bus, sp + 8);
                        let result_addr = sp + 12;
                        let list_ptr = Self::list_record_ptr(bus, list_handle);
                        let mut double_click = false;
                        let mut draw_state = None;
                        let mut draw_cells = Vec::new();

                        if let Some(state) = self.list_states.get_mut(&list_handle) {
                            if let Some(cell) = Self::list_cell_from_point(state, point) {
                                let previous_selected = state.selected.clone();
                                let single_select = list_ptr != 0
                                    && (bus.read_byte(list_ptr + Self::LIST_SEL_FLAGS_OFFSET)
                                        & 0x80)
                                        != 0;
                                if single_select {
                                    state.selected.clear();
                                }
                                state.selected.insert(cell);
                                double_click = state.last_click == cell
                                    && self.tick_count.saturating_sub(state.last_click_tick)
                                        <= Self::LIST_DOUBLE_CLICK_TICKS;
                                state.last_click = cell;
                                state.last_click_tick = self.tick_count;
                                if state.draw_enabled {
                                    draw_cells = previous_selected
                                        .symmetric_difference(&state.selected)
                                        .copied()
                                        .collect();
                                    if !draw_cells.is_empty() {
                                        draw_state = Some(state.clone());
                                    }
                                }
                                Self::sync_list_state_to_guest(bus, list_handle, state);
                            } else {
                                state.last_click = Self::list_no_click_cell();
                                state.last_click_tick = self.tick_count;
                                Self::sync_list_state_to_guest(bus, list_handle, state);
                            }
                        }

                        bus.write_word(result_addr, if double_click { 0x0100 } else { 0 });
                        if let Some(state) = draw_state {
                            if self.draw_list_cells_with_ldef_message(
                                cpu,
                                bus,
                                list_handle,
                                &state,
                                draw_cells.clone(),
                                Self::LIST_LHILITE_MSG,
                                12,
                            ) {
                                return Some(Ok(()));
                            }
                            for cell in draw_cells {
                                self.draw_list_fallback(cpu, bus, &state, Some(cell));
                            }
                        }
                        cpu.write_reg(Register::A7, result_addr);
                        Ok(())
                    }

                    // LDraw (selector 48 / $30)
                    // Draws one cell through the list's owning port.
                    // PROCEDURE LDraw(theCell: Cell; lHandle: ListHandle);
                    // Inside Macintosh Volume IV, IV-274
                    0x30 => {
                        let list_handle = bus.read_long(sp + 2);
                        let cell = Self::read_stack_point(bus, sp + 6);
                        if let Some(state) = self.list_states.get(&list_handle).cloned() {
                            if self.draw_list_with_ldef(
                                cpu,
                                bus,
                                list_handle,
                                &state,
                                Some((cell.0, cell.1)),
                                10,
                            ) {
                                return Some(Ok(()));
                            }
                            self.draw_list_fallback(cpu, bus, &state, Some((cell.0, cell.1)));
                        }
                        cpu.write_reg(Register::A7, sp + 10);
                        Ok(())
                    }

                    // LUpdate (selector 100 / $64)
                    // Redraws visible cells. The update region is accepted
                    // for stack discipline; clipping is already enforced by
                    // the port's visRgn/clipRgn in the QuickDraw path.
                    // PROCEDURE LUpdate(theRgn: RgnHandle; lHandle: ListHandle);
                    // Inside Macintosh Volume IV, IV-275
                    0x64 => {
                        let list_handle = bus.read_long(sp + 2);
                        if let Some(state) = self.list_states.get(&list_handle).cloned() {
                            if state.draw_enabled {
                                if self.draw_list_with_ldef(cpu, bus, list_handle, &state, None, 10)
                                {
                                    return Some(Ok(()));
                                }
                                self.draw_list_fallback(cpu, bus, &state, None);
                            }
                        }
                        cpu.write_reg(Register::A7, sp + 10);
                        Ok(())
                    }

                    // LAutoScroll/LActivate/LScroll/LSize are accepted as no-ops for now.
                    // Inside Macintosh Volume IV, IV-274 to IV-276
                    0x00 | 0x10 | 0x50 | 0x60 => self.pack0_fallback(cpu, bus, sp, selector),

                    // LDispose (selector 40 / $28)
                    // Disposes of the list.
                    // PROCEDURE LDispose(lHandle: ListHandle);
                    // Inside Macintosh Volume IV, IV-270
                    0x28 => {
                        let list_handle = bus.read_long(sp + 2);
                        let list_ptr = Self::list_record_ptr(bus, list_handle);
                        let cells_handle = if list_ptr != 0 {
                            bus.read_long(list_ptr + Self::LIST_CELLS_OFFSET)
                        } else {
                            0
                        };
                        self.list_states.remove(&list_handle);
                        if list_ptr != 0 {
                            bus.free(list_ptr);
                        }
                        if cells_handle != 0 {
                            bus.free(cells_handle);
                        }
                        if list_handle != 0 {
                            bus.free(list_handle);
                        }
                        cpu.write_reg(Register::A7, sp + 6);
                        Ok(())
                    }

                    _ => self.pack0_fallback(cpu, bus, sp, selector),
                }
            }

            // Pack1 ($A9E8) — List Manager Package
            //
            // Twenty-five-routine selector dispatcher providing the
            // List Manager API used by Standard File dialogs, font
            // pickers, and any custom-list dialog. Per IM:IV-269
            // explicit EQU table:
            //
            //   lActivate  $00  lAddColumn $04  lAddRow    $08
            //   lAddToCell $0C  lAutoScroll $10 lCellSize  $14
            //   lClick     $18  lClrCell   $1C  lDelColumn $20
            //   lDelRow    $24  lDispose   $28  lDoDraw    $2C
            //   lDraw      $30  lFind      $34  lGetCell   $38
            //   lGetSelect $3C  lLastClick $40  lNew       $44
            //   lNextCell  $48  lRect      $4C  lScroll    $50
            //   lSearch    $54  lSetCell   $58  lSetSelect $5C
            //   lSize      $60  lUpdate    $64
            //
            // Selectors step by FOUR (not by 2 like Pack2/3/6) because
            // the Pack1 internal jump table holds 4-byte JMP entries
            // per IM:IV-269. Selector encoding is pure low-byte (high
            // byte $00) — same convention as Pack2 / Pack3 / Pack6,
            // NOT the Pack8 / SANE param-size-in-high-byte glue.
            //
            // HLE compromise: Pack1 now builds and tears down real list
            // records for the documented LNew/LDispose path so callers
            // can obtain a live list handle. The remaining selectors
            // still collapse to stack-discipline-correct no-ops until
            // more Pack1 fixtures land. PROCEDUREs simply pop the
            // documented Pascal frame; FUNCTIONs return defensive
            // defaults (FALSE for BOOLEAN, 0 for INTEGER, (0,0) for
            // Cell) when they still lack a stateful implementation.
            // LSearch's searchProc trampoline is intentionally NOT
            // invoked — returning FALSE is the stable "no match"
            // answer for the unimplemented search path.
            //
            // Inside Macintosh Volume IV (1986), pages IV-259..IV-279.
            // More Macintosh Toolbox Essentials (1993), 4-1..4-107.
            // Pack1 / List Manager ($A9E8): Per-selector Pascal frames per IM:IV-269 EQU table: $00 LActivate pop 8, $04 LAddColumn pop 10 result@SP+10 (returns 0), $08 LAddRow pop 10 result@SP+10 (returns 0), $0C LAddToCell pop 16, $10 LAutoScroll pop 6, $14 LCellSize pop 10, $18 LClick pop 12 result@SP+12 (returns FALSE), $1C LClrCell pop 10, $20 LDelColumn pop 10, $24 LDelRow pop 10, $28 LDispose pop 6 (frees the list state), $2C LDoDraw pop 8, $30 LDraw pop 10, $34 LFind pop 18 (writes 0/0 to VAR offset/len), $38 LGetCell pop 18 (writes 0 to VAR dataLen), $3C LGetSelect pop 12 result@SP+12 (returns FALSE), $40 LLastClick pop 6 result@SP+6 (returns Cell(0,0)), $44 LNew pop 28 result@SP+28 (creates a live ListHandle), $48 LNextCell pop 14 result@SP+14 (returns FALSE), $4C LRect pop 14 (writes 0,0,0,0 to VAR cellRect), $50 LScroll pop 10, $54 LSearch pop 20 result@SP+20 (returns FALSE), $58 LSetCell pop 16, $5C LSetSelect pop 12, $60 LSize pop 10, $64 LUpdate pop 10. Unknown selector pops only the 2-byte selector word.
            (true, 0x1E8) => {
                let sp = cpu.read_reg(Register::A7);
                let selector = bus.read_word(sp);
                match selector {
                    // PROCEDURE LActivate(act: BOOLEAN;
                    //                     lHandle: ListHandle);
                    // IM:IV-269. Stack: sel(2) + lHandle(4) + act(2)
                    // = 8 bytes.
                    0x0000 => {
                        cpu.write_reg(Register::A7, sp + 8);
                    }
                    // FUNCTION LAddColumn(count, colNum: INTEGER;
                    //                     lHandle: ListHandle): INTEGER;
                    // IM:IV-269. Stack: sel(2) + lHandle(4) + colNum(2)
                    // + count(2) + result(2) = 12; pop 10,
                    // result@SP+10. No list, nothing added → return 0.
                    0x0004 => {
                        bus.write_word(sp + 10, 0);
                        cpu.write_reg(Register::A7, sp + 10);
                    }
                    // FUNCTION LAddRow(count, rowNum: INTEGER;
                    //                  lHandle: ListHandle): INTEGER;
                    // IM:IV-269. Same shape as LAddColumn.
                    0x0008 => {
                        bus.write_word(sp + 10, 0);
                        cpu.write_reg(Register::A7, sp + 10);
                    }
                    // PROCEDURE LAddToCell(dataPtr: Ptr;
                    //                      dataLen: INTEGER;
                    //                      theCell: Cell;
                    //                      lHandle: ListHandle);
                    // IM:IV-269 + MTb 4-82. Stack: sel(2) + lHandle(4)
                    // + theCell(4) + dataLen(2) + dataPtr(4) = 16.
                    0x000C => {
                        cpu.write_reg(Register::A7, sp + 16);
                    }
                    // PROCEDURE LAutoScroll(lHandle: ListHandle);
                    // IM:IV-269. Stack: sel(2) + lHandle(4) = 6.
                    0x0010 => {
                        cpu.write_reg(Register::A7, sp + 6);
                    }
                    // PROCEDURE LCellSize(cSize: Point;
                    //                     lHandle: ListHandle);
                    // IM:IV-269. Stack: sel(2) + lHandle(4) + cSize(4)
                    // = 10.
                    0x0014 => {
                        cpu.write_reg(Register::A7, sp + 10);
                    }
                    // FUNCTION LClick(pt: Point; modifiers: INTEGER;
                    //                 lHandle: ListHandle): BOOLEAN;
                    // IM:IV-269 + MTb 4-78. Stack: sel(2) + lHandle(4)
                    // + modifiers(2) + pt(4) + result(2) = 14; pop 12,
                    // result@SP+12. No list, no double-click → FALSE.
                    0x0018 => {
                        bus.write_word(sp + 12, 0);
                        cpu.write_reg(Register::A7, sp + 12);
                    }
                    // PROCEDURE LClrCell(theCell: Cell;
                    //                    lHandle: ListHandle);
                    // IM:IV-269. Stack: sel(2) + lHandle(4) + theCell(4)
                    // = 10.
                    0x001C => {
                        cpu.write_reg(Register::A7, sp + 10);
                    }
                    // PROCEDURE LDelColumn(count, colNum: INTEGER;
                    //                      lHandle: ListHandle);
                    // IM:IV-269. Stack: sel(2) + lHandle(4) + colNum(2)
                    // + count(2) = 10.
                    0x0020 => {
                        cpu.write_reg(Register::A7, sp + 10);
                    }
                    // PROCEDURE LDelRow(count, rowNum: INTEGER;
                    //                   lHandle: ListHandle);
                    // IM:IV-269. Same shape as LDelColumn.
                    0x0024 => {
                        cpu.write_reg(Register::A7, sp + 10);
                    }
                    // PROCEDURE LDispose(lHandle: ListHandle);
                    // IM:IV-269. Stack: sel(2) + lHandle(4) = 6.
                    0x0028 => {
                        let list_handle = bus.read_long(sp + 2);
                        let list_ptr = Self::list_record_ptr(bus, list_handle);
                        let cells_handle = if list_ptr != 0 {
                            bus.read_long(list_ptr + Self::LIST_CELLS_OFFSET)
                        } else {
                            0
                        };
                        self.list_states.remove(&list_handle);
                        if list_ptr != 0 {
                            bus.free(list_ptr);
                        }
                        if cells_handle != 0 {
                            bus.free(cells_handle);
                        }
                        if list_handle != 0 {
                            bus.free(list_handle);
                        }
                        cpu.write_reg(Register::A7, sp + 6);
                    }
                    // PROCEDURE LDoDraw(drawIt: BOOLEAN;
                    //                   lHandle: ListHandle);
                    // IM:IV-269 + MTb 4-83 (alias LSetDrawingMode).
                    // Stack: sel(2) + lHandle(4) + drawIt(2) = 8.
                    0x002C => {
                        cpu.write_reg(Register::A7, sp + 8);
                    }
                    // PROCEDURE LDraw(theCell: Cell;
                    //                 lHandle: ListHandle);
                    // IM:IV-269. Stack: sel(2) + lHandle(4) + theCell(4)
                    // = 10.
                    0x0030 => {
                        cpu.write_reg(Register::A7, sp + 10);
                    }
                    // PROCEDURE LFind(VAR offset, len: INTEGER;
                    //                 theCell: Cell;
                    //                 lHandle: ListHandle);
                    // IM:IV-263, 269. Pascal pushes args left-to-right
                    // (first pushed = deepest), so source order
                    // offset, len, theCell, lHandle places lHandle
                    // (last) at sp+2, theCell at sp+6, len ptr at
                    // sp+10, offset ptr at sp+14. VAR INTEGER args
                    // are 4-byte ptrs each. Stack: sel(2) + lHandle(4)
                    // + theCell(4) + len ptr(4) + offset ptr(4) = 18.
                    // No list → write 0 to both *offset and *len.
                    0x0034 => {
                        let len_ptr = bus.read_long(sp + 10);
                        let offset_ptr = bus.read_long(sp + 14);
                        if offset_ptr != 0 {
                            bus.write_word(offset_ptr, 0);
                        }
                        if len_ptr != 0 {
                            bus.write_word(len_ptr, 0);
                        }
                        cpu.write_reg(Register::A7, sp + 18);
                    }
                    // PROCEDURE LGetCell(dataPtr: Ptr;
                    //                    VAR dataLen: INTEGER;
                    //                    theCell: Cell;
                    //                    lHandle: ListHandle);
                    // IM:IV-263, 269. Stack: sel(2) + lHandle(4)
                    // + theCell(4) + dataLen ptr(4) + dataPtr(4) = 18.
                    // No cell data → write 0 to *dataLen.
                    0x0038 => {
                        let datalen_ptr = bus.read_long(sp + 10);
                        if datalen_ptr != 0 {
                            bus.write_word(datalen_ptr, 0);
                        }
                        cpu.write_reg(Register::A7, sp + 18);
                    }
                    // FUNCTION LGetSelect(next: BOOLEAN;
                    //                     VAR theCell: Cell;
                    //                     lHandle: ListHandle): BOOLEAN;
                    // IM:IV-263, 269. Stack: sel(2) + lHandle(4)
                    // + theCell ptr(4) + next(2) + result(2) = 14;
                    // pop 12, result@SP+12. No selection → FALSE.
                    0x003C => {
                        bus.write_word(sp + 12, 0);
                        cpu.write_reg(Register::A7, sp + 12);
                    }
                    // FUNCTION LLastClick(lHandle: ListHandle): Cell;
                    // IM:IV-263, 269. Stack: sel(2) + lHandle(4)
                    // + result(4) = 10; pop 6, result@SP+6 (Cell long).
                    // No prior click → write Cell(-1,-1) = 0xFFFF_FFFF.
                    0x0040 => {
                        bus.write_long(sp + 6, 0xFFFF_FFFF);
                        cpu.write_reg(Register::A7, sp + 6);
                    }
                    // FUNCTION LNew(rView, dataBounds: Rect;
                    //               cSize: Point; theProc: INTEGER;
                    //               theWindow: WindowPtr;
                    //               drawIt, hasGrow, scrollHoriz,
                    //               scrollVert: BOOLEAN): ListHandle;
                    // IM:IV-269 + MTb 4-70. Rects pass by REFERENCE
                    // (4-byte ptr) per the QuickDraw Toolbox-wide
                    // convention reaffirmed by PtInRect at
                    // quickdraw.rs:1654. Stack: sel(2) + scrollVert(2)
                    // + scrollHoriz(2) + hasGrow(2) + drawIt(2)
                    // + theWindow(4) + theProc(2) + cSize(4)
                    // + dataBounds ptr(4) + rView ptr(4) + result(4)
                    // = 32; pop 28, result@SP+28 (long, ListHandle).
                    // Pack1 now mirrors the Pack0 list-record setup so
                    // callers can obtain a live list handle.
                    0x0044 => {
                        let draw_it = Self::stack_bool_slot(bus, sp + 2);
                        let has_grow = Self::stack_bool_slot(bus, sp + 4);
                        let scroll_h = Self::stack_bool_slot(bus, sp + 6);
                        let scroll_v = Self::stack_bool_slot(bus, sp + 8);
                        let window = bus.read_long(sp + 10);
                        let proc_id = bus.read_word(sp + 14) as i16;
                        let cell_size = Self::read_stack_point(bus, sp + 16);
                        let data_bounds_ptr = bus.read_long(sp + 20);
                        let view_rect_ptr = bus.read_long(sp + 24);
                        let result_addr = sp + 28;

                        let view_rect = Self::read_rect_ptr(bus, view_rect_ptr);
                        let data_bounds = Self::read_rect_ptr(bus, data_bounds_ptr);
                        let resolved_cell_size =
                            self.compute_list_cell_size(view_rect, data_bounds, cell_size);
                        let visible = Self::compute_list_visible_rect(
                            view_rect,
                            data_bounds,
                            resolved_cell_size,
                        );

                        let list_ptr = bus.alloc(Self::LIST_RECORD_SIZE);
                        let list_handle = bus.alloc(4);
                        let cells_handle = bus.alloc(4);

                        let list_def_proc_handle = self
                            .find_resource_any(*b"LDEF", proc_id)
                            .map(|(_, ptr)| {
                                self.get_or_create_resource_handle(bus, *b"LDEF", proc_id, ptr)
                            })
                            .unwrap_or(0);

                        if list_handle != 0 {
                            bus.write_long(list_handle, list_ptr);
                        }
                        if cells_handle != 0 {
                            bus.write_long(cells_handle, 0);
                        }

                        let state = super::dispatch::ListState {
                            view_rect,
                            data_bounds,
                            cell_size: resolved_cell_size,
                            visible,
                            port: window,
                            draw_enabled: draw_it,
                            cells: std::collections::HashMap::new(),
                            selected: std::collections::BTreeSet::new(),
                            last_click: Self::list_no_click_cell(),
                            last_click_tick: 0,
                        };

                        if list_ptr != 0 {
                            Self::write_rect_words(
                                bus,
                                list_ptr + Self::LIST_RVIEW_OFFSET,
                                view_rect,
                            );
                            bus.write_long(list_ptr + Self::LIST_PORT_OFFSET, window);
                            Self::write_point_words(
                                bus,
                                list_ptr + Self::LIST_INDENT_OFFSET,
                                (0, 0),
                            );
                            Self::write_point_words(
                                bus,
                                list_ptr + Self::LIST_CELL_SIZE_OFFSET,
                                resolved_cell_size,
                            );
                            Self::write_rect_words(
                                bus,
                                list_ptr + Self::LIST_VISIBLE_OFFSET,
                                visible,
                            );
                            bus.write_long(list_ptr + Self::LIST_VSCROLL_OFFSET, 0);
                            bus.write_long(list_ptr + Self::LIST_HSCROLL_OFFSET, 0);
                            bus.write_byte(list_ptr + Self::LIST_SEL_FLAGS_OFFSET, 0);
                            bus.write_byte(list_ptr + Self::LIST_ACTIVE_OFFSET, 1);
                            bus.write_byte(list_ptr + Self::LIST_RESERVED_OFFSET, 0);
                            bus.write_byte(list_ptr + Self::LIST_FLAGS_OFFSET, 0);
                            bus.write_long(list_ptr + Self::LIST_CLICK_TIME_OFFSET, 0);
                            Self::write_point_words(
                                bus,
                                list_ptr + Self::LIST_CLICK_LOC_OFFSET,
                                (-32768, -32768),
                            );
                            Self::write_point_words(
                                bus,
                                list_ptr + Self::LIST_MOUSE_LOC_OFFSET,
                                (-1, -1),
                            );
                            bus.write_long(list_ptr + Self::LIST_CLICK_LOOP_OFFSET, 0);
                            Self::write_point_words(
                                bus,
                                list_ptr + Self::LIST_LAST_CLICK_OFFSET,
                                (-1, -1),
                            );
                            bus.write_long(list_ptr + Self::LIST_REFCON_OFFSET, 0);
                            bus.write_long(
                                list_ptr + Self::LIST_DEF_PROC_OFFSET,
                                list_def_proc_handle,
                            );
                            bus.write_long(list_ptr + Self::LIST_USER_HANDLE_OFFSET, 0);
                            Self::write_rect_words(
                                bus,
                                list_ptr + Self::LIST_DATA_BOUNDS_OFFSET,
                                data_bounds,
                            );
                            bus.write_long(list_ptr + Self::LIST_CELLS_OFFSET, cells_handle);
                            let rows = (data_bounds.2 - data_bounds.0).max(0) as i32;
                            let cols = (data_bounds.3 - data_bounds.1).max(0) as i32;
                            bus.write_word(
                                list_ptr + Self::LIST_MAX_INDEX_OFFSET,
                                rows.saturating_mul(cols).saturating_mul(2) as u16,
                            );
                            bus.write_word(list_ptr + Self::LIST_CELL_ARRAY_OFFSET, 0);
                        }

                        if list_handle != 0 {
                            self.list_states.insert(list_handle, state);
                        }

                        if trace_list_manager_enabled() {
                            eprintln!(
                                "[LIST] LNew handle=${:08X} ptr=${:08X} proc={} draw={} grow={} scroll_h={} scroll_v={} view=({},{},{},{}) bounds=({},{},{},{}) cell=({}, {})",
                                list_handle,
                                list_ptr,
                                proc_id,
                                draw_it,
                                has_grow,
                                scroll_h,
                                scroll_v,
                                view_rect.0,
                                view_rect.1,
                                view_rect.2,
                                view_rect.3,
                                data_bounds.0,
                                data_bounds.1,
                                data_bounds.2,
                                data_bounds.3,
                                resolved_cell_size.0,
                                resolved_cell_size.1,
                            );
                        }

                        bus.write_long(result_addr, list_handle);
                        cpu.write_reg(Register::A7, result_addr);
                    }
                    // FUNCTION LNextCell(hNext, vNext: BOOLEAN;
                    //                    VAR theCell: Cell;
                    //                    lHandle: ListHandle): BOOLEAN;
                    // IM:IV-263, 269. Stack: sel(2) + lHandle(4)
                    // + theCell ptr(4) + vNext(2) + hNext(2)
                    // + result(2) = 16; pop 14, result@SP+14.
                    // No list → FALSE.
                    0x0048 => {
                        bus.write_word(sp + 14, 0);
                        cpu.write_reg(Register::A7, sp + 14);
                    }
                    // PROCEDURE LRect(VAR cellRect: Rect;
                    //                 theCell: Cell;
                    //                 lHandle: ListHandle);
                    // IM:IV-263, 269. Stack: sel(2) + lHandle(4)
                    // + theCell(4) + cellRect ptr(4) = 14.
                    // No list → write empty Rect (0,0,0,0).
                    0x004C => {
                        let rect_ptr = bus.read_long(sp + 10);
                        if rect_ptr != 0 {
                            bus.write_word(rect_ptr, 0);
                            bus.write_word(rect_ptr + 2, 0);
                            bus.write_word(rect_ptr + 4, 0);
                            bus.write_word(rect_ptr + 6, 0);
                        }
                        cpu.write_reg(Register::A7, sp + 14);
                    }
                    // PROCEDURE LScroll(dCols, dRows: INTEGER;
                    //                   lHandle: ListHandle);
                    // IM:IV-263, 269. Stack: sel(2) + lHandle(4)
                    // + dRows(2) + dCols(2) = 10.
                    0x0050 => {
                        cpu.write_reg(Register::A7, sp + 10);
                    }
                    // FUNCTION LSearch(dataPtr: Ptr; dataLen: INTEGER;
                    //                  searchProc: Ptr;
                    //                  VAR theCell: Cell;
                    //                  lHandle: ListHandle): BOOLEAN;
                    // IM:IV-263, 269. Stack: sel(2) + lHandle(4)
                    // + theCell ptr(4) + searchProc(4) + dataLen(2)
                    // + dataPtr(4) + result(2) = 22; pop 20,
                    // result@SP+20. No list → FALSE; do NOT invoke
                    // the searchProc trampoline.
                    0x0054 => {
                        bus.write_word(sp + 20, 0);
                        cpu.write_reg(Register::A7, sp + 20);
                    }
                    // PROCEDURE LSetCell(dataPtr: Ptr;
                    //                    dataLen: INTEGER;
                    //                    theCell: Cell;
                    //                    lHandle: ListHandle);
                    // IM:IV-263, 269. Stack: sel(2) + lHandle(4)
                    // + theCell(4) + dataLen(2) + dataPtr(4) = 16.
                    0x0058 => {
                        cpu.write_reg(Register::A7, sp + 16);
                    }
                    // PROCEDURE LSetSelect(setIt: BOOLEAN;
                    //                      theCell: Cell;
                    //                      lHandle: ListHandle);
                    // IM:IV-263, 269. Stack: sel(2) + lHandle(4)
                    // + theCell(4) + setIt(2) = 12.
                    0x005C => {
                        cpu.write_reg(Register::A7, sp + 12);
                    }
                    // PROCEDURE LSize(listWidth, listHeight: INTEGER;
                    //                 lHandle: ListHandle);
                    // IM:IV-263, 269. Stack: sel(2) + lHandle(4)
                    // + listHeight(2) + listWidth(2) = 10.
                    0x0060 => {
                        cpu.write_reg(Register::A7, sp + 10);
                    }
                    // PROCEDURE LUpdate(theRgn: RgnHandle;
                    //                   lHandle: ListHandle);
                    // IM:IV-263, 269. Stack: sel(2) + lHandle(4)
                    // + theRgn(4) = 10.
                    0x0064 => {
                        cpu.write_reg(Register::A7, sp + 10);
                    }
                    _ => {
                        // Unknown / undocumented selector — pop just
                        // the 2-byte selector word so the caller's
                        // stack stays balanced.
                        cpu.write_reg(Register::A7, sp + 2);
                    }
                }
                cpu.write_reg(Register::D0, 0);
                Ok(())
            }

            // Pack2 ($A9E9) — Disk Initialization Manager
            //
            // Six-routine selector-based dispatcher for floppy / SCSI
            // disk formatting (DIBadMount, DILoad, DIUnload, DIFormat,
            // DIVerify, DIZero). The selector word sits at SP+0; for
            // FUNCTIONs the caller pre-allocated a 2-byte INTEGER /
            // OSErr result slot below the args (deepest on the stack).
            //
            // Systemless's HLE mounts a single fixed VFS volume and never
            // synthesises diskInsertedEvents, so every routine collapses
            // to a noErr no-op while still popping the documented Pascal
            // stack frame and writing 0 = noErr to the FUNCTION result
            // slot. Pop sizes per IM:Files 5-15..5-21 + selector summary
            // 5-24.
            //
            // Inside Macintosh: Files (1992), Chapter 5 "Disk
            // Initialization Manager", pages 5-15..5-21.
            // Pack2 / DiskInit ($A9E9): Per-selector Pascal frames per IM:Files 5-15..5-21: $0000 DIBadMount(Point,LongInt):Integer pops 8+selector, $0002 DILoad / $0004 DIUnload pop just selector, $0006 DIFormat / $0008 DIVerify (Integer):OSErr pop 2+selector, $000A DIZero(Integer,Str255):OSErr pops 258+selector (Pascal Str255 by-value). All collapse to noErr — single VFS volume, never sees disk-insert events.
            (true, 0x1E9) => {
                let sp = cpu.read_reg(Register::A7);
                let selector = bus.read_word(sp);
                let (arg_bytes, has_result) = match selector {
                    // FUNCTION DIBadMount(where: Point; evtMessage: LongInt): Integer
                    // IM:Files 5-18..5-19. where=4 (Point by value),
                    // evtMessage=4 (LongInt by value). Returns 0 = "no
                    // error / user proceeded" — the only result code that
                    // never causes a caller to escalate to DoError.
                    0x0000 => (8u32, true),
                    // PROCEDURE DILoad / PROCEDURE DIUnload — no args, no
                    // result. IM:Files 5-15..5-16. The Disk Init Manager
                    // is always "loaded" in our HLE so both are no-ops.
                    0x0002 | 0x0004 => (0u32, false),
                    // FUNCTION DIFormat(drvNum: Integer): OSErr
                    // FUNCTION DIVerify(drvNum: Integer): OSErr
                    // IM:Files 5-19..5-20. Both return noErr on the
                    // single VFS volume.
                    0x0006 | 0x0008 => (2u32, true),
                    // FUNCTION DIZero(drvNum: Integer; volName: Str255): OSErr
                    // IM:Files 5-21. Pascal Str255 is pushed by value
                    // (256 bytes); MPW C glue marshals from the
                    // ConstStr255Param pointer into a stack-local
                    // Str255 before invoking the trap. Total args:
                    // drvNum(2) + Str255(256) = 258.
                    0x000A => (258u32, true),
                    _ => {
                        // No other selectors in IM:Files 5-24 summary.
                        // Pop just the selector and return noErr; future
                        // System additions would land in a new arm here.
                        cpu.write_reg(Register::A7, sp + 2);
                        cpu.write_reg(Register::D0, 0);
                        return Some(Ok(()));
                    }
                };
                let pop_total = 2 + arg_bytes;
                if has_result {
                    bus.write_word(sp + pop_total, 0);
                }
                cpu.write_reg(Register::A7, sp + pop_total);
                cpu.write_reg(Register::D0, 0);
                Ok(())
            }

            // Pack3 ($A9EA) — Standard File Package
            //
            // Eight-routine selector dispatcher for the system Open /
            // Save dialogs. The selector word sits at SP+0; ALL eight
            // routines are PROCEDUREs (no FUNCTION result slot). Each
            // writes a Pascal record (SFReply for $0001..$0004,
            // StandardFileReply for $0005..$0008) into a VAR reply
            // pointer that the caller pushed by reference.
            //
            // Systemless has no native file-picker UI and never displays
            // a modal SF dialog. Get-file routines normally collapse to
            // the documented "user canceled" path; headless harnesses can
            // set SYSTEMLESS_STANDARD_GET_FILE to select a VFS file
            // explicitly. Put-file routines accept the caller's
            // default/original filename in the app's current directory so
            // games that require a new save file can continue through their
            // normal File Manager create/open path.
            //
            // Selector encoding is pure low-byte routine number
            // (high byte $00) per IM:Files 3-45..3-54 explicit
            // "Selector: $0005" tables — this is NOT the Pack8/SANE
            // param-size-in-high-byte convention.
            //
            // Stack frames assume Str255 args are pushed BY REFERENCE
            // (4-byte pointer), the modern MPW Toolbox convention
            // matching AppendMenu / InsertMenuItem / SetWTitle and
            // the ConstStr255Param C binding type. Pack2 DIZero's
            // Pascal-by-value 256-byte convention is a separate read
            // of an isolated routine.
            //
            // Inside Macintosh: Files (1992), Chapter 3 "Standard
            // File Package", pages 3-43..3-61.
            // Inside Macintosh Volume I (1985), pages I-518..I-527.
            // Inside Macintosh Volume VI (1991), pages 26-21..26-25
            // (CustomGetFile / CustomPutFile additions).
            // Pack3 / Standard File ($A9EA): Per-selector Pascal frames per IM:Files 3-45..3-54: $0001 SFPutFile pop 22 reply@SP+2, $0002 SFGetFile pop 28 reply@SP+2, $0003 SFPPutFile pop 28 reply@SP+8, $0004 SFPGetFile pop 34 reply@SP+8, $0005 StandardPutFile pop 14 reply@SP+2, $0006 StandardGetFile pop 16 reply@SP+2, $0007 CustomPutFile pop 40 reply@SP+28, $0008 CustomGetFile pop 42 reply@SP+28. Get-file selectors write cancel or an explicit selected file; put-file selectors return an accepted reply using the supplied default/original name.
            (true, 0x1EA) => {
                let sp = cpu.read_reg(Register::A7);
                let selector = bus.read_word(sp);
                let (
                    arg_bytes,
                    reply_offset,
                    default_name_offset,
                    modern_reply,
                    accepts_file,
                    get_filter_offsets,
                ) = match selector {
                    // PROCEDURE SFPutFile(where: Point; prompt: Str255;
                    //                     origName: Str255;
                    //                     dlgHook: ProcPtr;
                    //                     VAR reply: SFReply);
                    // IM:Files 3-52 / IM:I I-519..I-522. Reply ptr is
                    // last Pascal arg → at SP+2 above the selector.
                    0x0001 => (20u32, 2u32, Some(10u32), false, true, None),
                    // PROCEDURE SFGetFile(where: Point; prompt: Str255;
                    //                     fileFilter: ProcPtr;
                    //                     numTypes: Integer;
                    //                     typeList: SFTypeList;
                    //                     dlgHook: ProcPtr;
                    //                     VAR reply: SFReply);
                    // IM:Files 3-52..3-53 / IM:I I-523..I-526.
                    0x0002 => (26u32, 2u32, None, false, false, Some((14u32, 10u32))),
                    // PROCEDURE SFPPutFile(...; VAR reply: SFReply;
                    //                       dlgID: Integer;
                    //                       filterProc: ProcPtr);
                    // IM:I I-522..I-523. filterProc(4) + dlgID(2) sit
                    // ABOVE reply ptr on the stack → reply at SP+8.
                    0x0003 => (26u32, 8u32, Some(16u32), false, true, None),
                    // PROCEDURE SFPGetFile(...; VAR reply: SFReply;
                    //                       dlgID: Integer;
                    //                       filterProc: ProcPtr);
                    // IM:I I-526..I-527.
                    0x0004 => (32u32, 8u32, None, false, false, Some((20u32, 16u32))),
                    // PROCEDURE StandardPutFile(prompt: Str255;
                    //                           defaultName: Str255;
                    //                           VAR reply:
                    //                             StandardFileReply);
                    // IM:Files 3-45.
                    0x0005 => (12u32, 2u32, Some(6u32), true, true, None),
                    // PROCEDURE StandardGetFile(fileFilter: ProcPtr;
                    //                           numTypes: Integer;
                    //                           typeList: SFTypeList;
                    //                           VAR reply:
                    //                             StandardFileReply);
                    // IM:Files 3-50.
                    0x0006 => (14u32, 2u32, None, true, false, Some((10u32, 6u32))),
                    // PROCEDURE CustomPutFile(prompt: Str255;
                    //                          defaultName: Str255;
                    //                          VAR reply:
                    //                            StandardFileReply;
                    //                          dlgID: Integer;
                    //                          where: Point;
                    //                          dlgHook: ProcPtr;
                    //                          filterProc: ProcPtr;
                    //                          activeList: Ptr;
                    //                          activateProc: ProcPtr;
                    //                          yourDataPtr: UNIV Ptr);
                    // IM:Files 3-46 / IM:VI 26-21. yourData(4) +
                    // activate(4) + activeList(4) + filter(4) +
                    // dlgHook(4) + where(4) + dlgID(2) = 26 bytes
                    // ABOVE reply → reply at SP+28.
                    0x0007 => (38u32, 28u32, Some(32u32), true, true, None),
                    // PROCEDURE CustomGetFile(fileFilter: ProcPtr;
                    //                          numTypes: Integer;
                    //                          typeList: SFTypeList;
                    //                          VAR reply:
                    //                            StandardFileReply;
                    //                          dlgID: Integer;
                    //                          where: Point;
                    //                          dlgHook: ProcPtr;
                    //                          filterProc: ProcPtr;
                    //                          activeList: Ptr;
                    //                          activateProc: ProcPtr;
                    //                          yourDataPtr: UNIV Ptr);
                    // IM:Files 3-51 / IM:VI 26-22.
                    0x0008 => (40u32, 28u32, None, true, false, Some((36u32, 32u32))),
                    _ => {
                        // No other selectors documented in IM:Files
                        // 3-45..3-54 or IM:I I-518..I-527. Pop just
                        // the selector word defensively so a future
                        // System addition doesn't corrupt the caller
                        // stack.
                        cpu.write_reg(Register::A7, sp + 2);
                        cpu.write_reg(Register::D0, 0);
                        return Some(Ok(()));
                    }
                };
                let pop_total = 2 + arg_bytes;
                let reply_ptr = bus.read_long(sp + reply_offset);
                if accepts_file {
                    let default_name_ptr = default_name_offset
                        .map(|offset| bus.read_long(sp + offset))
                        .unwrap_or(0);
                    let name = standard_file_default_name(bus, default_name_ptr);
                    let vref = self.resolve_volume_ref_num(self.app_wd_refnum);
                    let dir_id = self.default_dir_id;
                    if modern_reply {
                        standard_file_put_reply_modern(bus, reply_ptr, vref, dir_id, &name);
                    } else {
                        standard_file_put_reply_old(bus, reply_ptr, self.app_wd_refnum, &name);
                    }
                } else if let Some(selection) = self.standard_file_env_selection().or_else(|| {
                    get_filter_offsets.and_then(|(num_types_offset, type_list_offset)| {
                        let num_types = bus.read_word(sp + num_types_offset) as i16;
                        let type_list_ptr = bus.read_long(sp + type_list_offset);
                        self.standard_file_auto_selection(bus, num_types, type_list_ptr)
                    })
                }) {
                    // StandardGetFile returns sfGood, sfType, and an FSSpec
                    // for the selected file; SFGetFile's old SFReply uses a
                    // working-directory refnum in vRefNum.
                    // Inside Macintosh: Files (1992), pages 1-12, 3-42,
                    // 3-45..3-54.
                    if modern_reply {
                        standard_file_get_reply_modern(
                            bus,
                            reply_ptr,
                            selection.vref,
                            selection.dir_id,
                            selection.file_type,
                            selection.finder_flags,
                            &selection.name,
                        );
                    } else {
                        standard_file_get_reply_old(
                            bus,
                            reply_ptr,
                            selection.wd_ref,
                            selection.file_type,
                            &selection.name,
                        );
                    }
                } else {
                    standard_file_cancel_reply(bus, reply_ptr);
                }
                cpu.write_reg(Register::A7, sp + pop_total);
                cpu.write_reg(Register::D0, 0);
                Ok(())
            }

            // Pack4 ($A9EB) and Pack5 ($A9EC) are handled by dispatch_sane

            // Pack6 ($A9ED) — International Utilities Package
            //
            // Eighteen-routine selector dispatcher providing date/time
            // formatting, international string comparison, and (System
            // 7+) interscript ordering / itl2/itl4 cache management.
            //
            // Selector encoding is pure low-byte routine number (high
            // byte $00) per IM:I I-487 + IM:VI 14-135 explicit
            // "Selector: $XXXX" tables — same convention as Pack2 /
            // Pack3, NOT the Pack8 / SANE param-size-in-high-byte
            // convention.
            //
            // HLE compromise: Systemless runs a single-script (Roman /
            // Latin-1) US-default environment with no localised
            // 'INTL' / 'itl2' / 'itl4' resources. Date strings
            // collapse to a "1/1/04" placeholder; time strings to
            // "12:00 AM" / "12:00:00 AM"; metric query returns FALSE;
            // INTL resource handles return NIL; comparators do byte-
            // level compare returning -1/0/+1 (Mag/Comp variants;
            // case-sensitive) or 0/1 (MagID/Equal variants; case-
            // insensitive ASCII fold); script/lang ordering does
            // numeric compare. IUClearCache / IUSetIntl /
            // IUGetIntlTable are no-ops with documented VAR-out NIL
            // writes. Apps that defensively check (handle != NIL)
            // before dereffing fall through cleanly; apps that need
            // locale-specific formatting see Mac-default English
            // output. The "no INTL" fallback is INTENTIONALLY SAFE
            // per IM:I I-505 ("if the INTL resource is missing,
            // IUGetIntl returns NIL").
            //
            // Stack frames assume Pascal arg conventions: LongInt =
            // 4, Integer/Boolean/DateForm = 2 (DateForm = 1 byte at
            // the source level but stack-aligned to 2 bytes), Handle
            // / Ptr = 4, Str255 by REFERENCE (4-byte VAR ptr,
            // matching the Toolbox-wide convention reaffirmed by
            // Pack3, AppendMenu, SetWTitle, and
            // ParamText). VAR LongDateTime is also a 4-byte ptr.
            //
            // IUCompString / IUEqualString / IUCompPString /
            // IUEqualPString are pure Pascal-glue convenience wrappers
            // (per IM:I I-498 "there's no trap for it; it eventually
            // calls IUMagString" / IM:I I-501 same for IUEqualString)
            // — they have NO selector and reach this dispatcher via
            // their Mag / MagID counterparts.
            //
            // Inside Macintosh Volume I (1985), pages I-485..I-510.
            // Inside Macintosh Volume VI (1991), pages 14-1..14-135.
            // Pack6 / Intl Utilities ($A9ED): Per-selector Pascal frames per IM:I I-487 + IM:VI 14-135: $0000 IUDateString pop 12 result@SP+2, $0002 IUTimeString pop 12 result@SP+2, $0004 IUMetric pop 2 result@SP+2 (FALSE), $0006 IUGetIntl pop 4 result@SP+4 (NIL handle), $0008 IUSetIntl pop 10 (no-op), $000A IUMagString pop 14 result@SP+14 (-1/0/+1 byte cmp), $000C IUMagIDString pop 14 result@SP+14 (0/1 case-insens), $000E IUDatePString pop 16 result@SP+6, $0010 IUTimePString pop 16 result@SP+6, $0014 IULDateString pop 16 result@SP+6, $0016 IULTimeString pop 16 result@SP+6, $0018 IUClearCache pop 2 (no-op), $001A IUMagPString pop 18 result@SP+18 (-1/0/+1), $001C IUMagIDPString pop 18 result@SP+18 (0/1), $001E IUScriptOrder pop 6 result@SP+6 (-1/0/+1), $0020 IULangOrder pop 6 result@SP+6 (-1/0/+1), $0022 IUTextOrder pop 22 result@SP+22 (-1/0/+1), $0024 IUGetIntlTable pop 18 (writes NIL/0/0 to 3 VAR ptrs).
            (true, 0x1ED) => {
                let sp = cpu.read_reg(Register::A7);
                let selector = bus.read_word(sp);
                match selector {
                    // PROCEDURE IUDateString(dateTime: LongInt;
                    //                        form: DateForm;
                    //                        VAR result: Str255);
                    // IM:I I-487, I-504. Stack: sel(2) + result(4)
                    // + form(2) + dateTime(4) = 12.
                    0x0000 => {
                        let result_ptr = bus.read_long(sp + 2);
                        if result_ptr != 0 {
                            bus.write_pstring(result_ptr, b"1/1/04");
                        }
                        cpu.write_reg(Register::A7, sp + 12);
                        cpu.write_reg(Register::D0, 0);
                        Ok(())
                    }
                    // PROCEDURE IUTimeString(dateTime: LongInt;
                    //                        wantSeconds: Boolean;
                    //                        VAR result: Str255);
                    // IM:I I-487, I-504. Stack: sel(2) + result(4)
                    // + wantSec(2) + dateTime(4) = 12.
                    0x0002 => {
                        let result_ptr = bus.read_long(sp + 2);
                        let want_seconds = bus.read_word(sp + 6) != 0;
                        if result_ptr != 0 {
                            let s: &[u8] = if want_seconds {
                                b"12:00:00 AM"
                            } else {
                                b"12:00 AM"
                            };
                            bus.write_pstring(result_ptr, s);
                        }
                        cpu.write_reg(Register::A7, sp + 12);
                        cpu.write_reg(Register::D0, 0);
                        Ok(())
                    }
                    // FUNCTION IUMetric: Boolean;
                    // IM:I I-487, I-505. Stack: sel(2) + result(2)
                    // = 4. Pop 2, leave result word at new SP+0.
                    0x0004 => {
                        bus.write_word(sp + 2, 0);
                        cpu.write_reg(Register::A7, sp + 2);
                        cpu.write_reg(Register::D0, 0);
                        Ok(())
                    }
                    // FUNCTION IUGetIntl(theID: Integer): Handle;
                    // IM:I I-487, I-505. Stack: sel(2) + theID(2)
                    // + result(4) = 8. Pop 4, leave result long at
                    // new SP+0. Returns NIL — no INTL resources.
                    0x0006 => {
                        bus.write_long(sp + 4, 0);
                        cpu.write_reg(Register::A7, sp + 4);
                        cpu.write_reg(Register::D0, 0);
                        Ok(())
                    }
                    // PROCEDURE IUSetIntl(refNum: Integer;
                    //                     theID: Integer;
                    //                     intlParam: Handle);
                    // IM:I I-487, I-503. Stack: sel(2) +
                    // intlParam(4) + theID(2) + refNum(2) = 10.
                    // No-op (HLE doesn't track INTL overrides).
                    0x0008 => {
                        cpu.write_reg(Register::A7, sp + 10);
                        cpu.write_reg(Register::D0, 0);
                        Ok(())
                    }
                    // FUNCTION IUMagString(aPtr,bPtr: Ptr;
                    //                      aLen,bLen: Integer):
                    //                      Integer;
                    // IM:I I-487, I-507. Stack: sel(2) + bLen(2) +
                    // aLen(2) + bPtr(4) + aPtr(4) + result(2) = 16.
                    // Pop 14, leave result at new SP+0.
                    0x000A => {
                        let a_ptr = bus.read_long(sp + 10);
                        let b_ptr = bus.read_long(sp + 6);
                        let a_len = bus.read_word(sp + 4) as usize;
                        let b_len = bus.read_word(sp + 2) as usize;
                        let a = bus.read_bytes(a_ptr, a_len);
                        let b = bus.read_bytes(b_ptr, b_len);
                        let result: i16 = match a.cmp(&b) {
                            std::cmp::Ordering::Less => -1,
                            std::cmp::Ordering::Equal => 0,
                            std::cmp::Ordering::Greater => 1,
                        };
                        bus.write_word(sp + 14, result as u16);
                        cpu.write_reg(Register::A7, sp + 14);
                        cpu.write_reg(Register::D0, result as u16 as u32);
                        Ok(())
                    }
                    // FUNCTION IUMagIDString(aPtr,bPtr: Ptr;
                    //                        aLen,bLen: Integer):
                    //                        Integer;
                    // IM:I I-487, I-507. Same stack as IUMagString.
                    // Returns 0 (case-insensitive equal) or 1 (not).
                    0x000C => {
                        let a_ptr = bus.read_long(sp + 10);
                        let b_ptr = bus.read_long(sp + 6);
                        let a_len = bus.read_word(sp + 4) as usize;
                        let b_len = bus.read_word(sp + 2) as usize;
                        let a: Vec<u8> = bus
                            .read_bytes(a_ptr, a_len)
                            .iter()
                            .map(|c| c.to_ascii_lowercase())
                            .collect();
                        let b: Vec<u8> = bus
                            .read_bytes(b_ptr, b_len)
                            .iter()
                            .map(|c| c.to_ascii_lowercase())
                            .collect();
                        let result: u16 = if a == b { 0 } else { 1 };
                        bus.write_word(sp + 14, result);
                        cpu.write_reg(Register::A7, sp + 14);
                        cpu.write_reg(Register::D0, result as u32);
                        Ok(())
                    }
                    // PROCEDURE IUDatePString(dateTime: LongInt;
                    //                         form: DateForm;
                    //                         VAR result: Str255;
                    //                         intlParam: Handle);
                    // IM:I I-487, I-505. Stack: sel(2) +
                    // intlParam(4) + result(4) + form(2) +
                    // dateTime(4) = 16.
                    0x000E => {
                        let result_ptr = bus.read_long(sp + 6);
                        if result_ptr != 0 {
                            bus.write_pstring(result_ptr, b"1/1/04");
                        }
                        cpu.write_reg(Register::A7, sp + 16);
                        cpu.write_reg(Register::D0, 0);
                        Ok(())
                    }
                    // PROCEDURE IUTimePString(dateTime: LongInt;
                    //                         wantSeconds: Boolean;
                    //                         VAR result: Str255;
                    //                         intlParam: Handle);
                    // IM:I I-487, I-505. Stack: sel(2) +
                    // intlParam(4) + result(4) + wantSec(2) +
                    // dateTime(4) = 16.
                    0x0010 => {
                        let result_ptr = bus.read_long(sp + 6);
                        let want_seconds = bus.read_word(sp + 10) != 0;
                        if result_ptr != 0 {
                            let s: &[u8] = if want_seconds {
                                b"12:00:00 AM"
                            } else {
                                b"12:00 AM"
                            };
                            bus.write_pstring(result_ptr, s);
                        }
                        cpu.write_reg(Register::A7, sp + 16);
                        cpu.write_reg(Register::D0, 0);
                        Ok(())
                    }
                    // PROCEDURE IULDateString(VAR dateTime:
                    //                         LongDateTime;
                    //                         longFlag: DateForm;
                    //                         VAR Result: Str255;
                    //                         intlParam: Handle);
                    // IM:VI 14-135. Stack: sel(2) + intlParam(4) +
                    // result(4) + longFlag(2) + dateTime ptr(4) = 16.
                    0x0014 => {
                        let result_ptr = bus.read_long(sp + 6);
                        if result_ptr != 0 {
                            bus.write_pstring(result_ptr, b"1/1/04");
                        }
                        cpu.write_reg(Register::A7, sp + 16);
                        cpu.write_reg(Register::D0, 0);
                        Ok(())
                    }
                    // PROCEDURE IULTimeString(VAR dateTime:
                    //                         LongDateTime;
                    //                         wantSeconds: Boolean;
                    //                         VAR Result: Str255;
                    //                         intlParam: Handle);
                    // IM:VI 14-135. Stack: sel(2) + intlParam(4) +
                    // result(4) + wantSec(2) + dateTime ptr(4) = 16.
                    0x0016 => {
                        let result_ptr = bus.read_long(sp + 6);
                        let want_seconds = bus.read_word(sp + 10) != 0;
                        if result_ptr != 0 {
                            let s: &[u8] = if want_seconds {
                                b"12:00:00 AM"
                            } else {
                                b"12:00 AM"
                            };
                            bus.write_pstring(result_ptr, s);
                        }
                        cpu.write_reg(Register::A7, sp + 16);
                        cpu.write_reg(Register::D0, 0);
                        Ok(())
                    }
                    // PROCEDURE IUClearCache;
                    // IM:VI 14-76. Stack: sel(2) only. No-op (HLE
                    // has no IUtil cache).
                    0x0018 => {
                        cpu.write_reg(Register::A7, sp + 2);
                        cpu.write_reg(Register::D0, 0);
                        Ok(())
                    }
                    // FUNCTION IUMagPString(aPtr,bPtr: Ptr;
                    //                       aLen,bLen: Integer;
                    //                       itl2Handle: Handle):
                    //                       Integer;
                    // IM:VI 14-135. Stack: sel(2) + itl2(4) +
                    // bLen(2) + aLen(2) + bPtr(4) + aPtr(4) +
                    // result(2) = 20. Pop 18, leave result at new
                    // SP+0. itl2Handle ignored (HLE has no itl2).
                    0x001A => {
                        let a_ptr = bus.read_long(sp + 14);
                        let b_ptr = bus.read_long(sp + 10);
                        let a_len = bus.read_word(sp + 8) as usize;
                        let b_len = bus.read_word(sp + 6) as usize;
                        let a = bus.read_bytes(a_ptr, a_len);
                        let b = bus.read_bytes(b_ptr, b_len);
                        let result: i16 = match a.cmp(&b) {
                            std::cmp::Ordering::Less => -1,
                            std::cmp::Ordering::Equal => 0,
                            std::cmp::Ordering::Greater => 1,
                        };
                        bus.write_word(sp + 18, result as u16);
                        cpu.write_reg(Register::A7, sp + 18);
                        cpu.write_reg(Register::D0, result as u16 as u32);
                        Ok(())
                    }
                    // FUNCTION IUMagIDPString(aPtr,bPtr: Ptr;
                    //                         aLen,bLen: Integer;
                    //                         itl2Handle: Handle):
                    //                         Integer;
                    // IM:VI 14-135. Same stack as IUMagPString.
                    0x001C => {
                        let a_ptr = bus.read_long(sp + 14);
                        let b_ptr = bus.read_long(sp + 10);
                        let a_len = bus.read_word(sp + 8) as usize;
                        let b_len = bus.read_word(sp + 6) as usize;
                        let a: Vec<u8> = bus
                            .read_bytes(a_ptr, a_len)
                            .iter()
                            .map(|c| c.to_ascii_lowercase())
                            .collect();
                        let b: Vec<u8> = bus
                            .read_bytes(b_ptr, b_len)
                            .iter()
                            .map(|c| c.to_ascii_lowercase())
                            .collect();
                        let result: u16 = if a == b { 0 } else { 1 };
                        bus.write_word(sp + 18, result);
                        cpu.write_reg(Register::A7, sp + 18);
                        cpu.write_reg(Register::D0, result as u32);
                        Ok(())
                    }
                    // FUNCTION IUScriptOrder(script1, script2:
                    //                        ScriptCode): Integer;
                    // IM:VI 14-135. Stack: sel(2) + script2(2) +
                    // script1(2) + result(2) = 8. Pop 6, leave
                    // result at new SP+0.
                    0x001E => {
                        let script1 = bus.read_word(sp + 4) as i16;
                        let script2 = bus.read_word(sp + 2) as i16;
                        let result: i16 = match script1.cmp(&script2) {
                            std::cmp::Ordering::Less => -1,
                            std::cmp::Ordering::Equal => 0,
                            std::cmp::Ordering::Greater => 1,
                        };
                        bus.write_word(sp + 6, result as u16);
                        cpu.write_reg(Register::A7, sp + 6);
                        cpu.write_reg(Register::D0, result as u16 as u32);
                        Ok(())
                    }
                    // FUNCTION IULangOrder(language1, language2:
                    //                      LangCode): Integer;
                    // IM:VI 14-135. Same stack as IUScriptOrder.
                    0x0020 => {
                        let lang1 = bus.read_word(sp + 4) as i16;
                        let lang2 = bus.read_word(sp + 2) as i16;
                        let result: i16 = match lang1.cmp(&lang2) {
                            std::cmp::Ordering::Less => -1,
                            std::cmp::Ordering::Equal => 0,
                            std::cmp::Ordering::Greater => 1,
                        };
                        bus.write_word(sp + 6, result as u16);
                        cpu.write_reg(Register::A7, sp + 6);
                        cpu.write_reg(Register::D0, result as u16 as u32);
                        Ok(())
                    }
                    // FUNCTION IUTextOrder(aPtr,bPtr: Ptr;
                    //                      aLen,bLen: Integer;
                    //                      aScript,bScript:
                    //                      ScriptCode;
                    //                      aLang,bLang: LangCode):
                    //                      Integer;
                    // IM:VI 14-135. Stack: sel(2) + bLang(2) +
                    // aLang(2) + bScript(2) + aScript(2) + bLen(2)
                    // + aLen(2) + bPtr(4) + aPtr(4) + result(2) =
                    // 24. Pop 22, leave result at new SP+0. All
                    // script/lang args ignored in single-script HLE.
                    0x0022 => {
                        let a_ptr = bus.read_long(sp + 18);
                        let b_ptr = bus.read_long(sp + 14);
                        let a_len = bus.read_word(sp + 12) as usize;
                        let b_len = bus.read_word(sp + 10) as usize;
                        let a = bus.read_bytes(a_ptr, a_len);
                        let b = bus.read_bytes(b_ptr, b_len);
                        let result: i16 = match a.cmp(&b) {
                            std::cmp::Ordering::Less => -1,
                            std::cmp::Ordering::Equal => 0,
                            std::cmp::Ordering::Greater => 1,
                        };
                        bus.write_word(sp + 22, result as u16);
                        cpu.write_reg(Register::A7, sp + 22);
                        cpu.write_reg(Register::D0, result as u16 as u32);
                        Ok(())
                    }
                    // PROCEDURE IUGetIntlTable(script: ScriptCode;
                    //                          tableCode: Integer;
                    //                          VAR itlHandle: Handle;
                    //                          VAR offset: LongInt;
                    //                          VAR length: LongInt);
                    // IM:VI 14-135. Stack: sel(2) + length(4) +
                    // offset(4) + itlHandle(4) + tableCode(2) +
                    // script(2) = 18. No itl2/itl4 tables in HLE
                    // — write NIL/0/0 to all three VAR ptrs so
                    // caller's defensive (handle == NIL) check
                    // sends them down the "table not available"
                    // path.
                    0x0024 => {
                        let length_ptr = bus.read_long(sp + 2);
                        let offset_ptr = bus.read_long(sp + 6);
                        let handle_ptr = bus.read_long(sp + 10);
                        if handle_ptr != 0 {
                            bus.write_long(handle_ptr, 0);
                        }
                        if offset_ptr != 0 {
                            bus.write_long(offset_ptr, 0);
                        }
                        if length_ptr != 0 {
                            bus.write_long(length_ptr, 0);
                        }
                        cpu.write_reg(Register::A7, sp + 18);
                        cpu.write_reg(Register::D0, 0);
                        Ok(())
                    }
                    _ => {
                        // Defensive fallback: pop only the selector
                        // word so a future System addition or buggy
                        // caller doesn't corrupt the rest of the
                        // stack. Documented selectors are $0000..
                        // $0010 (even) per IM:I I-487 plus $0014..
                        // $0024 (even) per IM:VI 14-135. Selector
                        // $0012 is unused — IM:VI's enumeration at
                        // 14-135 jumps from $0010 IUTimePString
                        // straight to $0014 IULDateString.
                        cpu.write_reg(Register::A7, sp + 2);
                        cpu.write_reg(Register::D0, 0);
                        Ok(())
                    }
                }
            }

            // Pack7 ($A9EE) — Binary/Decimal Conversion Package
            // Selector 0: NumToString — converts D0.L to decimal Pascal string at A0.
            // Selector 1: StringToNum — converts Pascal string at A0 to D0.L.
            // PROCEDURE NumToString(theNumber: LONGINT; VAR theString: Str255);
            // PROCEDURE StringToNum(theString: Str255; VAR theNumber: LONGINT);
            // Inside Macintosh Volume I, I-489
            // Pack7 (NumToString/StringToNum) ($A9EE): Selector 0: NumToString (D0→Str255 at A0), Selector 1: StringToNum (Str255 at A0→D0)
            (true, 0x1EE) => {
                let sp = cpu.read_reg(Register::A7);
                let selector = bus.read_word(sp);
                cpu.write_reg(Register::A7, sp + 2);

                match selector {
                    0 => {
                        // NumToString: D0.L = number, A0 = pointer to Str255 result
                        let number = cpu.read_reg(Register::D0) as i32;
                        let a0 = cpu.read_reg(Register::A0);
                        bus.write_pstring(a0, format!("{}", number).as_bytes());
                    }
                    1 => {
                        // StringToNum: A0 = pointer to Pascal string, D0.L = result
                        let a0 = cpu.read_reg(Register::A0);
                        let bytes = bus.read_pstring(a0);
                        let s = String::from_utf8_lossy(&bytes);
                        let num: i32 = s.trim().parse().unwrap_or(0);
                        cpu.write_reg(Register::D0, num as u32);
                    }
                    _ => {
                        eprintln!("[TRAP] Pack7: unknown selector {}", selector);
                    }
                }
                Ok(())
            }

            // Pack12 ($A82E) — Color Picker Package
            // Inside Macintosh Volume V, V-174..V-175.
            // MPW Universal Interfaces 3.4 ColorPicker.h declares:
            //   Fix2SmallFract(Fixed)      THREEWORDINLINE(0x3F3C, 0x0001, 0xA82E)
            //   SmallFract2Fix(SmallFract) THREEWORDINLINE(0x3F3C, 0x0002, 0xA82E)
            //   CMY2RGB(...)               THREEWORDINLINE(0x3F3C, 0x0003, 0xA82E)
            //   RGB2CMY(...)               THREEWORDINLINE(0x3F3C, 0x0004, 0xA82E)
            //   HSL2RGB(...)               THREEWORDINLINE(0x3F3C, 0x0005, 0xA82E)
            //   GetColor(...)              THREEWORDINLINE(0x3F3C, 0x0009, 0xA82E)
            //
            // SmallFract is documented as the low-order word of a Fixed
            // number, so Fix2SmallFract drops the integer part while
            // SmallFract2Fix zero-extends the fractional word into a
            // 16.16 Fixed value with integer part 0.
            (true, 0x02E) => {
                let sp = cpu.read_reg(Register::A7);
                let selector = bus.read_word(sp);
                match selector {
                    // Fix2SmallFract (selector 1)
                    // FUNCTION Fix2SmallFract(f: Fixed): SmallFract;
                    // Stack: [result(2)] [f(4)] [sel(2)] — pop 6, leave 2
                    // Inside Macintosh Volume V, V-175
                    1 => {
                        let f = bus.read_long(sp + 2);
                        let small_fract = (f & 0xFFFF) as u16;
                        bus.write_word(sp + 6, small_fract);
                        cpu.write_reg(Register::A7, sp + 6);
                    }
                    // SmallFract2Fix (selector 2)
                    // FUNCTION SmallFract2Fix(s: SmallFract): Fixed;
                    // Stack: [result(4)] [s(2)] [sel(2)] — pop 4, leave 4
                    // Inside Macintosh Volume V, V-175
                    2 => {
                        let s = bus.read_word(sp + 2) as u32;
                        let fixed = s;
                        bus.write_long(sp + 4, fixed);
                        cpu.write_reg(Register::A7, sp + 4);
                    }
                    // CMY2RGB(3), RGB2CMY(4) — component-wise complements
                    // between the subtractive CMY and additive RGB models.
                    // PROCEDURE XXX(srcColor: XColor; VAR dstColor: YColor);
                    // Stack: [src_ptr(4)] [dst_ptr(4)] [sel(2)] — pop 10
                    // Inside Macintosh Volume V, V-175; Volume VI, 19-10
                    3 | 4 => {
                        let src_ptr = bus.read_long(sp + 6);
                        let dst_ptr = bus.read_long(sp + 2);
                        for i in 0..3u32 {
                            let component = bus.read_word(src_ptr + i * 2);
                            bus.write_word(dst_ptr + i * 2, !component);
                        }
                        cpu.write_reg(Register::A7, sp + 10);
                    }
                    // HSL2RGB(5)
                    // PROCEDURE HSL2RGB(hColor: HSLColor; VAR rColor: RGBColor);
                    // Stack: [src_ptr(4)] [dst_ptr(4)] [sel(2)] — pop 10
                    // Inside Macintosh Volume V, V-175;
                    // Volume VI, 19-10..19-13.
                    5 => {
                        fn hue_to_rgb(p: f64, q: f64, mut t: f64) -> f64 {
                            if t < 0.0 {
                                t += 1.0;
                            } else if t > 1.0 {
                                t -= 1.0;
                            }

                            if t < 1.0 / 6.0 {
                                return p + (q - p) * 6.0 * t;
                            }
                            if t < 1.0 / 2.0 {
                                return q;
                            }
                            if t < 2.0 / 3.0 {
                                return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
                            }
                            p
                        }

                        let src_ptr = bus.read_long(sp + 6);
                        let dst_ptr = bus.read_long(sp + 2);
                        let hue = bus.read_word(src_ptr) as f64 / 65535.0;
                        let saturation = bus.read_word(src_ptr + 2) as f64 / 65535.0;
                        let lightness = bus.read_word(src_ptr + 4) as f64 / 65535.0;
                        let (red, green, blue) = if saturation == 0.0 {
                            (lightness, lightness, lightness)
                        } else {
                            let q = if lightness < 0.5 {
                                lightness * (1.0 + saturation)
                            } else {
                                lightness + saturation - lightness * saturation
                            };
                            let p = 2.0 * lightness - q;
                            (
                                hue_to_rgb(p, q, hue + 1.0 / 3.0),
                                hue_to_rgb(p, q, hue),
                                hue_to_rgb(p, q, hue - 1.0 / 3.0),
                            )
                        };
                        let to_word = |component: f64| -> u16 {
                            (component.clamp(0.0, 1.0) * 65535.0).round() as u16
                        };
                        bus.write_word(dst_ptr, to_word(red));
                        bus.write_word(dst_ptr + 2, to_word(green));
                        bus.write_word(dst_ptr + 4, to_word(blue));
                        cpu.write_reg(Register::A7, sp + 10);
                    }
                    // RGB2HSL(6), HSV2RGB(7), RGB2HSV(8)
                    // are the remaining Color Picker conversions from
                    // IM:V V-175 and IM:VI 19-10..19-11.
                    6 => {
                        let src_ptr = bus.read_long(sp + 6);
                        let dst_ptr = bus.read_long(sp + 2);
                        let r = bus.read_word(src_ptr) as f64 / 65535.0;
                        let g = bus.read_word(src_ptr + 2) as f64 / 65535.0;
                        let b = bus.read_word(src_ptr + 4) as f64 / 65535.0;
                        let max = r.max(g).max(b);
                        let min = r.min(g).min(b);
                        let delta = max - min;
                        let lightness = (max + min) / 2.0;
                        let (hue, saturation) = if delta == 0.0 {
                            (0.0, 0.0)
                        } else {
                            let saturation = if lightness <= 0.5 {
                                delta / (max + min)
                            } else {
                                delta / (2.0 - max - min)
                            };
                            let mut hue = if max == r {
                                (g - b) / delta
                            } else if max == g {
                                2.0 + (b - r) / delta
                            } else {
                                4.0 + (r - g) / delta
                            };
                            if hue < 0.0 {
                                hue += 6.0;
                            }
                            (hue / 6.0, saturation)
                        };
                        let to_word = |component: f64| -> u16 {
                            (component.clamp(0.0, 1.0) * 65535.0).round() as u16
                        };
                        bus.write_word(dst_ptr, to_word(hue));
                        bus.write_word(dst_ptr + 2, to_word(saturation));
                        bus.write_word(dst_ptr + 4, to_word(lightness));
                        cpu.write_reg(Register::A7, sp + 10);
                    }
                    7 => {
                        let src_ptr = bus.read_long(sp + 6);
                        let dst_ptr = bus.read_long(sp + 2);
                        let hue = bus.read_word(src_ptr) as f64 / 65535.0;
                        let saturation = bus.read_word(src_ptr + 2) as f64 / 65535.0;
                        let value = bus.read_word(src_ptr + 4) as f64 / 65535.0;

                        let to_word = |component: f64| -> u16 {
                            (component.clamp(0.0, 1.0) * 65535.0).round() as u16
                        };
                        let (red, green, blue) = if saturation == 0.0 {
                            (value, value, value)
                        } else {
                            let h6 = hue * 6.0;
                            let sector = h6.floor() as i32;
                            let frac = h6 - sector as f64;
                            let p = value * (1.0 - saturation);
                            let q = value * (1.0 - saturation * frac);
                            let t = value * (1.0 - saturation * (1.0 - frac));
                            match sector.rem_euclid(6) {
                                0 => (value, t, p),
                                1 => (q, value, p),
                                2 => (p, value, t),
                                3 => (p, q, value),
                                4 => (t, p, value),
                                _ => (value, p, q),
                            }
                        };

                        bus.write_word(dst_ptr, to_word(red));
                        bus.write_word(dst_ptr + 2, to_word(green));
                        bus.write_word(dst_ptr + 4, to_word(blue));
                        cpu.write_reg(Register::A7, sp + 10);
                    }
                    8 => {
                        let src_ptr = bus.read_long(sp + 6);
                        let dst_ptr = bus.read_long(sp + 2);
                        let r = bus.read_word(src_ptr) as f64 / 65535.0;
                        let g = bus.read_word(src_ptr + 2) as f64 / 65535.0;
                        let b = bus.read_word(src_ptr + 4) as f64 / 65535.0;
                        let max = r.max(g).max(b);
                        let min = r.min(g).min(b);
                        let delta = max - min;
                        let value = max;
                        let saturation = if max == 0.0 { 0.0 } else { delta / max };
                        let hue = if delta == 0.0 {
                            0.0
                        } else {
                            let mut hue = if max == r {
                                (g - b) / delta
                            } else if max == g {
                                2.0 + (b - r) / delta
                            } else {
                                4.0 + (r - g) / delta
                            };
                            if hue < 0.0 {
                                hue += 6.0;
                            }
                            hue / 6.0
                        };
                        let to_word = |component: f64| -> u16 {
                            (component.clamp(0.0, 1.0) * 65535.0).round() as u16
                        };
                        bus.write_word(dst_ptr, to_word(hue));
                        bus.write_word(dst_ptr + 2, to_word(saturation));
                        bus.write_word(dst_ptr + 4, to_word(value));
                        cpu.write_reg(Register::A7, sp + 10);
                    }
                    // GetColor (selector 9)
                    // FUNCTION GetColor(where: Point; prompt: Str255;
                    //   inColor: RGBColor; VAR outColor: RGBColor): BOOLEAN;
                    // Stack: [result(2)] [outColorPtr(4)] [inColorPtr(4)] [prompt(4)]
                    //        [where(4)] [sel(2)] — pop 18, leave 2
                    // Inside Macintosh Volume V, V-174
                    9 => {
                        // Return FALSE (user cancelled)
                        bus.write_word(sp + 18, 0);
                        cpu.write_reg(Register::D0, 0);
                        cpu.write_reg(Register::A7, sp + 18);
                    }
                    _ => {
                        eprintln!("[PACK12] Unknown selector {} — popping 2 bytes", selector);
                        cpu.write_reg(Register::A7, sp + 2);
                    }
                }
                Ok(())
            }

            // ========== AliasDispatch ($A823) ==========

            // AliasDispatch ($A823)
            // Selector-based dispatcher for Alias Manager routines.
            // Selector in D0.
            // Inside Macintosh Volume VI, 9-17; Files 1992, 4-15
            // AliasDispatch ($A823): Selector 0 = FindFolder; other selectors return paramErr.
            (true, 0x023) => {
                let sp = cpu.read_reg(Register::A7);
                let selector = cpu.read_reg(Register::D0) & 0xFFFF;
                match selector {
                    // FindFolder (selector $0000)
                    // FUNCTION FindFolder(vRefNum: INTEGER; folderType: OSType;
                    //   createFolder: BOOLEAN; VAR foundVRefNum: INTEGER;
                    //   VAR foundDirID: LONGINT): OSErr;
                    // Inside Macintosh Volume VI, 9-42 to 9-44
                    // Stack (rightmost on top):
                    //   SP+0:  foundDirID_ptr(4)  SP+4:  foundVRefNum_ptr(4)
                    //   SP+8:  createFolder(2)    SP+10: folderType(4)
                    //   SP+14: vRefNum(2)         SP+16: result(2)
                    0 => {
                        let dirid_ptr = bus.read_long(sp);
                        let vref_ptr = bus.read_long(sp + 4);
                        let _create = bus.read_word(sp + 8) != 0;
                        let folder_type = bus.read_long(sp + 10);
                        let v_ref_num = bus.read_word(sp + 14) as i16;

                        let type_bytes = folder_type.to_be_bytes();
                        let type_str = std::str::from_utf8(&type_bytes).unwrap_or("????");
                        eprintln!(
                            "[ALIAS] FindFolder vRefNum={} type='{}' (${:08X})",
                            v_ref_num, type_str, folder_type
                        );

                        let found_dir_id = match folder_type {
                            t if t == u32::from_be_bytes(*b"pref") => {
                                self.ensure_vfs_directory("System Folder/Preferences")
                            }
                            t if t == u32::from_be_bytes(*b"temp") => {
                                self.ensure_vfs_directory("Temporary Items")
                            }
                            _ => 2,
                        };

                        bus.write_word(vref_ptr, (-1i16) as u16);
                        bus.write_long(dirid_ptr, found_dir_id);
                        // Pop 16 bytes params, leave 2-byte result
                        bus.write_word(sp + 16, 0); // noErr
                        cpu.write_reg(Register::A7, sp + 16);
                    }
                    // NewAlias (selector $0002)
                    // FUNCTION NewAlias(fromFile: FSSpecPtr; target: FSSpecPtr;
                    //   VAR alias: AliasHandle): OSErr;
                    // Stack (rightmost on top):
                    //   SP+0:  alias_ptr(4)   SP+4:  target_ptr(4)
                    //   SP+8:  fromFile_ptr(4)
                    //   SP+12: result(2)
                    // Inside Macintosh Volume VI, 9-57
                    0x0002 => {
                        let alias_ptr = bus.read_long(sp);
                        let target_ptr = bus.read_long(sp + 4);
                        let _from_file_ptr = bus.read_long(sp + 8);

                        let target_name = crate::trap::types::read_fsspec_name(bus, target_ptr);
                        eprintln!("[ALIAS] NewAlias target='{}'", target_name);

                        let alias_data_bytes = self.build_alias_record(bus, target_ptr);
                        if alias_ptr != 0 {
                            let alias_data = bus.alloc(alias_data_bytes.len() as u32);
                            bus.write_bytes(alias_data, &alias_data_bytes);
                            let alias_handle = bus.alloc(4);
                            bus.write_long(alias_handle, alias_data);
                            bus.write_long(alias_ptr, alias_handle);
                            self.ptr_to_handle.insert(alias_data, alias_handle);
                        }

                        bus.write_word(sp + 12, 0); // noErr
                        cpu.write_reg(Register::A7, sp + 12);
                    }
                    // ResolveAliasFile (selector $000C)
                    // FUNCTION ResolveAliasFile(VAR theSpec: FSSpec;
                    //   resolveAliasChains: Boolean;
                    //   VAR targetIsFolder: Boolean;
                    //   VAR wasAliased: Boolean): OSErr;
                    // Macintosh Toolbox Essentials 1992, 7-52
                    // Stack (rightmost on top):
                    //   SP+0:  wasAliased_ptr(4)      SP+4:  targetIsFolder_ptr(4)
                    //   SP+8:  resolveAliasChains(2)  SP+10: theSpec_ptr(4)
                    //   SP+14: result(2)
                    0x000C => {
                        let was_aliased_ptr = bus.read_long(sp);
                        let target_is_folder_ptr = bus.read_long(sp + 4);
                        let _resolve_chains = bus.read_word(sp + 8) != 0;
                        let spec_ptr = bus.read_long(sp + 10);

                        let name = crate::trap::types::read_fsspec_name(bus, spec_ptr);
                        eprintln!("[ALIAS] ResolveAliasFile spec='{}'", name);

                        // Not an alias, not a folder
                        bus.write_byte(was_aliased_ptr, 0);
                        bus.write_byte(target_is_folder_ptr, 0);
                        // Pop 14 bytes params, leave 2-byte result
                        bus.write_word(sp + 14, 0); // noErr
                        cpu.write_reg(Register::A7, sp + 14);
                    }
                    _ => {
                        eprintln!(
                            "[ALIAS] Unimplemented selector {} (${:04X})",
                            selector, selector
                        );
                        return Some(Err(Error::Halted));
                    }
                }
                Ok(())
            }

            // ========== CursorDeviceDispatch ($AADB) ==========
            // Cursor Device Manager dispatcher. The classic glue passes
            // selector $0B in D0; this path clears the Pascal result slot
            // and pops the selector word so the caller sees noErr.
            // CursorDeviceDispatch ($AADB): Returns 0
            (true, 0x2DB) => {
                let sp = cpu.read_reg(Register::A7);
                bus.write_word(sp + 2, 0);
                cpu.write_reg(Register::A7, sp + 2);
                cpu.write_reg(Register::D0, 0);
                Ok(())
            }

            // ========== Movie Toolbox Dispatch ($AAAA) ==========
            // Inside Macintosh: QuickTime (1993), pp. 2-33, 2-82 to 2-84.
            // Public MPW declarations:
            //   pascal OSErr EnterMovies(void);
            //   pascal void ExitMovies(void);
            // Single trap dispatcher for the entire QuickTime Movie
            // Toolbox API; the MPW glue loads the routine selector in
            // D0 before executing `_AAAA` (selector 1 = EnterMovies).
            // The zero-argument client calls exercised by the fixture
            // are stack-neutral.
            //
            // A complete Movie Toolbox emulation is a substantial
            // multi-iteration project (movies, tracks, media handlers,
            // codecs). The selectors below cover documented initialization,
            // movie-file loading, and stateful playback calls that appear
            // before gameplay in Classic Mac demos. Selector values are
            // verified against MPW Universal Headers Movies.h in Docker.
            (true, 0x2AA) => {
                let selector = cpu.read_reg(Register::D0) as u16;
                if super::dispatch::trace_quicktime_enabled() {
                    static QT_LOG_COUNT: std::sync::atomic::AtomicU32 =
                        std::sync::atomic::AtomicU32::new(0);
                    if QT_LOG_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 20 {
                        eprintln!(
                            "[QUICKTIME] MovieToolboxDispatch selector_in_d0=${:04X} sp=${:08X} d1=${:08X} a0=${:08X} a1=${:08X}",
                            selector,
                            cpu.read_reg(Register::A7),
                            cpu.read_reg(Register::D1),
                            cpu.read_reg(Register::A0),
                            cpu.read_reg(Register::A1)
                        );
                    }
                }
                match selector {
                    1 => {
                        // EnterMovies ($AAAA selector $0001)
                        // Initializes Movie Toolbox private state for the current application.
                        // pascal OSErr EnterMovies(void);
                        // Inside Macintosh: QuickTime 1993, pp. 2-82 to 2-83.
                        let sp = cpu.read_reg(Register::A7);
                        bus.write_word(sp, 0);
                        record_movie_error(self, 0);
                        return_noerr(cpu)
                    }
                    2 => {
                        // ExitMovies ($AAAA selector $0002)
                        // Releases Movie Toolbox private state for the current application.
                        // pascal void ExitMovies(void);
                        // Inside Macintosh: QuickTime 1993, pp. 2-83 to 2-84.
                        self.movie_states.clear();
                        self.movie_error = 0;
                        self.movie_sticky_error = 0;
                        return_noerr(cpu)
                    }
                    0x0192 => {
                        // OpenMovieFile ($AAAA selector $0192)
                        // Opens a QuickTime movie file and returns a resource-file refNum.
                        // pascal OSErr OpenMovieFile(const FSSpec *fileSpec,
                        //   short *resRefNum, char perms);
                        // Inside Macintosh: QuickTime 1993, pp. 2-98 to 2-99.
                        let sp = cpu.read_reg(Register::A7);
                        let permission = signed_byte_from_stack_word(bus.read_word(sp));
                        let ref_num_ptr = bus.read_long(sp + 2);
                        let spec_ptr = bus.read_long(sp + 6);
                        let filename = crate::trap::types::read_fsspec_name(bus, spec_ptr);
                        let vref = bus.read_word(spec_ptr) as i16;
                        let dir_id = bus.read_long(spec_ptr + 2);
                        let wants_write = permission == 2 || permission == 3;
                        let Some(vfs_key) = self.vfs_key_for_fsspec(vref, dir_id, &filename) else {
                            bus.write_word(sp + 10, (-43i16) as u16);
                            cpu.write_reg(Register::A7, sp + 10);
                            cpu.write_reg(Register::D0, (-43i16) as u32);
                            return Some(Ok(()));
                        };
                        let normalized_key = Self::normalize_vfs_path(&vfs_key);
                        let rsrc_key = self
                            .vfs_rsrc
                            .keys()
                            .find(|key| {
                                Self::normalize_vfs_path(key).eq_ignore_ascii_case(&normalized_key)
                            })
                            .cloned();
                        let data_key = self
                            .vfs
                            .keys()
                            .find(|key| {
                                Self::normalize_vfs_path(key).eq_ignore_ascii_case(&normalized_key)
                            })
                            .cloned();

                        let refnum = if let Some(vfs_key) = rsrc_key {
                            if let Some(existing) = self.refnum_for_resource_file_name(&vfs_key) {
                                existing
                            } else {
                                self.open_resource_file_from_vfs_key(bus, &vfs_key, wants_write)
                            }
                        } else if let Some(vfs_key) = data_key {
                            if let Some(existing) = self.refnum_for_resource_file_name(&vfs_key) {
                                existing
                            } else {
                                let refnum = self.next_refnum;
                                self.next_refnum += 1;
                                self.register_empty_resource_file(refnum);
                                self.set_resource_file_name(refnum, vfs_key.clone());
                                if wants_write {
                                    self.write_refnums.insert(refnum);
                                }
                                self.open_files.insert(refnum, vfs_key);
                                self.file_positions.insert(refnum, 0);
                                self.set_current_resource_refnum(bus, refnum);
                                refnum
                            }
                        } else {
                            bus.write_word(sp + 10, (-43i16) as u16);
                            cpu.write_reg(Register::A7, sp + 10);
                            cpu.write_reg(Register::D0, (-43i16) as u32);
                            return Some(Ok(()));
                        };

                        if ref_num_ptr != 0 {
                            bus.write_word(ref_num_ptr, refnum);
                        }
                        bus.write_word(sp + 10, 0);
                        cpu.write_reg(Register::A7, sp + 10);
                        cpu.write_reg(Register::D0, 0);
                        Ok(())
                    }
                    0x00F0 => {
                        // NewMovieFromFile ($AAAA selector $00F0)
                        // Creates an in-memory Movie from an open movie file.
                        // pascal OSErr NewMovieFromFile(Movie *theMovie,
                        //   short resRefNum, short *resId, StringPtr resName,
                        //   short newMovieFlags, Boolean *dataRefWasChanged);
                        // Inside Macintosh: QuickTime 1993, pp. 2-88 to 2-90.
                        let sp = cpu.read_reg(Register::A7);
                        let data_ref_changed_ptr = bus.read_long(sp);
                        let new_movie_flags = bus.read_word(sp + 4);
                        let res_name_ptr = bus.read_long(sp + 6);
                        let res_id_ptr = bus.read_long(sp + 10);
                        let res_refnum = bus.read_word(sp + 14);
                        let movie_ptr = bus.read_long(sp + 16);
                        let requested_id = if res_id_ptr != 0 {
                            bus.read_word(res_id_ptr) as i16
                        } else {
                            0
                        };

                        let mut selected_resource: Option<(i16, String, Vec<u8>)> = None;
                        if requested_id != -1 {
                            if let Some(resources) = self.resources.as_ref() {
                                if let Some(file) = resources.files.get(&res_refnum) {
                                    if requested_id != 0
                                        && file.loaded.contains_key(&(*b"moov", requested_id))
                                    {
                                        let name = file
                                            .names_by_id
                                            .get(&(*b"moov", requested_id))
                                            .cloned()
                                            .unwrap_or_default();
                                        let data_ptr = file.loaded[&(*b"moov", requested_id)];
                                        selected_resource = Some((
                                            requested_id,
                                            name,
                                            read_allocated_bytes(bus, data_ptr),
                                        ));
                                    } else if requested_id == 0 {
                                        let mut ids: Vec<i16> = file
                                            .loaded
                                            .keys()
                                            .filter_map(|(res_type, id)| {
                                                (*res_type == *b"moov").then_some(*id)
                                            })
                                            .collect();
                                        ids.sort_unstable();
                                        if let Some(id) = ids.first().copied() {
                                            let name = file
                                                .names_by_id
                                                .get(&(*b"moov", id))
                                                .cloned()
                                                .unwrap_or_default();
                                            if let Some(&data_ptr) =
                                                file.loaded.get(&(*b"moov", id))
                                            {
                                                selected_resource = Some((
                                                    id,
                                                    name,
                                                    read_allocated_bytes(bus, data_ptr),
                                                ));
                                            }
                                        }
                                    }
                                }
                            }
                        }

                        let file_name = self.resource_file_name(res_refnum).map(str::to_owned);
                        let selected_movie = if selected_resource.is_some() {
                            selected_resource
                        } else if requested_id == 0 || requested_id == -1 {
                            file_name.as_ref().and_then(|name| {
                                self.vfs
                                    .get(name)
                                    .cloned()
                                    .map(|data| (-1, String::new(), data))
                            })
                        } else {
                            None
                        };

                        let Some((selected_id, selected_name, movie_data)) = selected_movie else {
                            bus.write_word(sp + 20, QUICKTIME_INVALID_MOVIE as u16);
                            if movie_ptr != 0 {
                                bus.write_long(movie_ptr, 0);
                            }
                            cpu.write_reg(Register::A7, sp + 20);
                            cpu.write_reg(Register::D0, QUICKTIME_INVALID_MOVIE as u32);
                            record_movie_error(self, QUICKTIME_INVALID_MOVIE);
                            return Some(Ok(()));
                        };

                        let (box_rect, duration) = quicktime_movie_metadata(&movie_data);
                        let movie = bus.alloc(16);
                        bus.write_long(movie, u32::from_be_bytes(*b"MooV"));
                        bus.write_word(movie + 4, res_refnum);
                        bus.write_word(movie + 6, selected_id as u16);
                        bus.write_word(movie + 8, new_movie_flags);
                        self.movie_states.insert(
                            movie,
                            MovieState::new(
                                res_refnum,
                                selected_id,
                                new_movie_flags,
                                box_rect,
                                duration,
                            ),
                        );
                        if movie_ptr != 0 {
                            bus.write_long(movie_ptr, movie);
                        }
                        if res_id_ptr != 0 {
                            bus.write_word(res_id_ptr, selected_id as u16);
                        }
                        if res_name_ptr != 0 {
                            bus.write_pstring(res_name_ptr, selected_name.as_bytes());
                        }
                        if data_ref_changed_ptr != 0 {
                            bus.write_byte(data_ref_changed_ptr, 0);
                        }
                        bus.write_word(sp + 20, 0);
                        cpu.write_reg(Register::A7, sp + 20);
                        cpu.write_reg(Register::D0, 0);
                        record_movie_error(self, 0);
                        Ok(())
                    }
                    0x0187 => {
                        // NewMovie ($AAAA selector $0187)
                        // Creates an empty in-memory Movie and returns its handle.
                        // pascal Movie NewMovie(long flags);
                        // Inside Macintosh: QuickTime 1993, pp. 2-92 to 2-93.
                        let sp = cpu.read_reg(Register::A7);
                        let flags = bus.read_long(sp);
                        let movie = bus.alloc(16);
                        bus.write_long(movie, u32::from_be_bytes(*b"MooV"));
                        bus.write_long(movie + 4, flags);
                        bus.write_long(movie + 8, self.current_port);
                        bus.write_long(movie + 12, self.current_gdevice);
                        let mut state = MovieState::new(0, -1, flags as u16, (0, 0, 120, 160), 1);
                        state.gworld_port = self.current_port;
                        state.gworld_gdh = self.current_gdevice;
                        state.active = false;
                        self.movie_states.insert(movie, state);
                        bus.write_long(sp + 4, movie);
                        cpu.write_reg(Register::A7, sp + 4);
                        cpu.write_reg(Register::D0, 0);
                        record_movie_error(self, 0);
                        Ok(())
                    }
                    3 => {
                        // GetMoviesError ($AAAA selector $0003)
                        // Returns and clears the Movie Toolbox current error.
                        // pascal OSErr GetMoviesError(void);
                        // Inside Macintosh: QuickTime 1993, p. 2-85.
                        let sp = cpu.read_reg(Register::A7);
                        let err = self.movie_error;
                        bus.write_word(sp, err as u16);
                        self.movie_error = 0;
                        cpu.write_reg(Register::D0, err as u32);
                        Ok(())
                    }
                    4 => {
                        // GetMoviesStickyError ($AAAA selector $0004)
                        // Returns the first nonzero Movie Toolbox error since it was cleared.
                        // pascal OSErr GetMoviesStickyError(void);
                        // Inside Macintosh: QuickTime 1993, p. 2-85.
                        let sp = cpu.read_reg(Register::A7);
                        bus.write_word(sp, self.movie_sticky_error as u16);
                        cpu.write_reg(Register::D0, self.movie_sticky_error as u32);
                        Ok(())
                    }
                    0x00DE => {
                        // ClearMoviesStickyError ($AAAA selector $00DE)
                        // Clears the Movie Toolbox sticky error.
                        // pascal void ClearMoviesStickyError(void);
                        // Inside Macintosh: QuickTime 1993, p. 2-85.
                        self.movie_sticky_error = 0;
                        cpu.write_reg(Register::D0, 0);
                        Ok(())
                    }
                    0x0005 => {
                        // MoviesTask ($AAAA selector $0005)
                        // Services active movies from the event loop.
                        // pascal void MoviesTask(Movie theMovie, long maxMilliSecToUse);
                        // Inside Macintosh: QuickTime 1993, pp. 2-124 to 2-125.
                        let sp = cpu.read_reg(Register::A7);
                        let movie = bus.read_long(sp + 4);
                        let ok = if movie == 0 {
                            for state in self.movie_states.values_mut() {
                                if state.active && state.rate != 0 {
                                    if state.rate < 0 {
                                        state.current_time = 0;
                                    } else {
                                        state.current_time = state.duration;
                                    }
                                    state.rate = 0;
                                    state.active = false;
                                }
                            }
                            true
                        } else if let Some(state) = self.movie_states.get_mut(&movie) {
                            if state.active && state.rate != 0 {
                                if state.rate < 0 {
                                    state.current_time = 0;
                                } else {
                                    state.current_time = state.duration;
                                }
                                state.rate = 0;
                                state.active = false;
                            }
                            true
                        } else {
                            false
                        };
                        cpu.write_reg(Register::A7, sp + 8);
                        let err = if ok { 0 } else { QUICKTIME_INVALID_MOVIE };
                        cpu.write_reg(Register::D0, err as u32);
                        record_movie_error(self, err);
                        Ok(())
                    }
                    0x0006 => {
                        // PrerollMovie ($AAAA selector $0006)
                        // Prepares movie media for playback at the requested time and rate.
                        // pascal OSErr PrerollMovie(Movie theMovie, TimeValue time, Fixed Rate);
                        // Inside Macintosh: QuickTime 1993, pp. 2-135 to 2-136.
                        let sp = cpu.read_reg(Register::A7);
                        let rate = bus.read_long(sp) as i32;
                        let time = bus.read_long(sp + 4) as i32;
                        let movie = bus.read_long(sp + 8);
                        let err = if let Some(state) = self.movie_states.get_mut(&movie) {
                            state.preferred_rate = rate;
                            state.current_time = time.clamp(0, state.duration);
                            0
                        } else {
                            QUICKTIME_INVALID_MOVIE
                        };
                        bus.write_word(sp + 12, err as u16);
                        cpu.write_reg(Register::A7, sp + 12);
                        cpu.write_reg(Register::D0, err as u32);
                        record_movie_error(self, err);
                        Ok(())
                    }
                    0x0007 => {
                        // LoadMovieIntoRam ($AAAA selector $0007)
                        // Requests a movie segment be loaded into memory for playback.
                        // pascal OSErr LoadMovieIntoRam(Movie theMovie, TimeValue time,
                        //   TimeValue duration, long flags);
                        // Inside Macintosh: QuickTime 1993, p. 2-135.
                        let sp = cpu.read_reg(Register::A7);
                        let _flags = bus.read_long(sp);
                        let _duration = bus.read_long(sp + 4);
                        let _time = bus.read_long(sp + 8);
                        let movie = bus.read_long(sp + 12);
                        let err = if self.movie_states.contains_key(&movie) {
                            0
                        } else {
                            QUICKTIME_INVALID_MOVIE
                        };
                        bus.write_word(sp + 16, err as u16);
                        cpu.write_reg(Register::A7, sp + 16);
                        cpu.write_reg(Register::D0, err as u32);
                        record_movie_error(self, err);
                        Ok(())
                    }
                    0x000B => {
                        // StartMovie ($AAAA selector $000B)
                        // Starts playback from the current movie time at the preferred rate.
                        // pascal void StartMovie(Movie theMovie);
                        // Inside Macintosh: QuickTime 1993, pp. 2-111 to 2-112.
                        let sp = cpu.read_reg(Register::A7);
                        let movie = bus.read_long(sp);
                        let err = if let Some(state) = self.movie_states.get_mut(&movie) {
                            state.active = true;
                            state.rate = state.preferred_rate;
                            0
                        } else {
                            QUICKTIME_INVALID_MOVIE
                        };
                        cpu.write_reg(Register::A7, sp + 4);
                        cpu.write_reg(Register::D0, err as u32);
                        record_movie_error(self, err);
                        Ok(())
                    }
                    0x000C => {
                        // StopMovie ($AAAA selector $000C)
                        // Stops movie playback.
                        // pascal void StopMovie(Movie theMovie);
                        // Inside Macintosh: QuickTime 1993, p. 2-112.
                        let sp = cpu.read_reg(Register::A7);
                        let movie = bus.read_long(sp);
                        let err = if let Some(state) = self.movie_states.get_mut(&movie) {
                            state.rate = 0;
                            state.active = false;
                            0
                        } else {
                            QUICKTIME_INVALID_MOVIE
                        };
                        cpu.write_reg(Register::A7, sp + 4);
                        cpu.write_reg(Register::D0, err as u32);
                        record_movie_error(self, err);
                        Ok(())
                    }
                    0x000D => {
                        // GoToBeginningOfMovie ($AAAA selector $000D)
                        // Repositions a movie to play from its start.
                        // pascal void GoToBeginningOfMovie(Movie theMovie);
                        // Inside Macintosh: QuickTime 1993, p. 2-113.
                        let sp = cpu.read_reg(Register::A7);
                        let movie = bus.read_long(sp);
                        let err = if let Some(state) = self.movie_states.get_mut(&movie) {
                            state.current_time = 0;
                            0
                        } else {
                            QUICKTIME_INVALID_MOVIE
                        };
                        cpu.write_reg(Register::A7, sp + 4);
                        cpu.write_reg(Register::D0, err as u32);
                        record_movie_error(self, err);
                        Ok(())
                    }
                    0x0016 => {
                        // SetMovieGWorld ($AAAA selector $0016)
                        // Sets the graphics world used to display a movie.
                        // pascal void SetMovieGWorld(Movie theMovie, CGrafPtr port,
                        //   GDHandle gdh);
                        // Inside Macintosh: QuickTime 1993, pp. 2-159 to 2-160.
                        let sp = cpu.read_reg(Register::A7);
                        let gdh = bus.read_long(sp);
                        let port = bus.read_long(sp + 4);
                        let movie = bus.read_long(sp + 8);
                        let err = if let Some(state) = self.movie_states.get_mut(&movie) {
                            state.gworld_port = port;
                            state.gworld_gdh = gdh;
                            0
                        } else {
                            QUICKTIME_INVALID_MOVIE
                        };
                        cpu.write_reg(Register::A7, sp + 12);
                        cpu.write_reg(Register::D0, err as u32);
                        record_movie_error(self, err);
                        Ok(())
                    }
                    0x0015 => {
                        // GetMovieGWorld ($AAAA selector $0015)
                        // Returns the graphics world used to display a movie.
                        // pascal void GetMovieGWorld(Movie theMovie, CGrafPtr *port,
                        //   GDHandle *gdh);
                        // Inside Macintosh: QuickTime 1993, p. 2-160.
                        let sp = cpu.read_reg(Register::A7);
                        let gdh_ptr = bus.read_long(sp);
                        let port_ptr = bus.read_long(sp + 4);
                        let movie = bus.read_long(sp + 8);
                        let values = self
                            .movie_states
                            .get(&movie)
                            .map(|state| (state.gworld_port, state.gworld_gdh));
                        let err = if let Some((port, gdh)) = values {
                            if port_ptr != 0 {
                                bus.write_long(port_ptr, port);
                            }
                            if gdh_ptr != 0 {
                                bus.write_long(gdh_ptr, gdh);
                            }
                            0
                        } else {
                            QUICKTIME_INVALID_MOVIE
                        };
                        cpu.write_reg(Register::A7, sp + 12);
                        cpu.write_reg(Register::D0, err as u32);
                        record_movie_error(self, err);
                        Ok(())
                    }
                    0x0023 => {
                        // DisposeMovie ($AAAA selector $0023)
                        // Frees memory and state owned by a movie.
                        // pascal void DisposeMovie(Movie theMovie);
                        // Inside Macintosh: QuickTime 1993, p. 2-96.
                        let sp = cpu.read_reg(Register::A7);
                        let movie = bus.read_long(sp);
                        let err = if self.movie_states.remove(&movie).is_some() {
                            bus.free(movie);
                            0
                        } else {
                            QUICKTIME_INVALID_MOVIE
                        };
                        cpu.write_reg(Register::A7, sp + 4);
                        cpu.write_reg(Register::D0, err as u32);
                        record_movie_error(self, err);
                        Ok(())
                    }
                    0x002B => {
                        // GetMovieDuration ($AAAA selector $002B)
                        // Returns the calculated duration of a movie.
                        // pascal TimeValue GetMovieDuration(Movie theMovie);
                        // Inside Macintosh: QuickTime 1993, p. 2-185.
                        let sp = cpu.read_reg(Register::A7);
                        let movie = bus.read_long(sp);
                        let duration = self
                            .movie_states
                            .get(&movie)
                            .map(|state| state.duration)
                            .unwrap_or(0);
                        let err = if duration == 0 {
                            QUICKTIME_INVALID_MOVIE
                        } else {
                            0
                        };
                        bus.write_long(sp + 4, duration as u32);
                        cpu.write_reg(Register::A7, sp + 4);
                        cpu.write_reg(Register::D0, err as u32);
                        record_movie_error(self, err);
                        Ok(())
                    }
                    0x002C => {
                        // GetMovieRate ($AAAA selector $002C)
                        // Returns the current movie playback rate.
                        // pascal Fixed GetMovieRate(Movie theMovie);
                        // Inside Macintosh: QuickTime 1993, p. 2-185.
                        let sp = cpu.read_reg(Register::A7);
                        let movie = bus.read_long(sp);
                        let rate = self
                            .movie_states
                            .get(&movie)
                            .map(|state| state.rate)
                            .unwrap_or(0);
                        let err = if self.movie_states.contains_key(&movie) {
                            0
                        } else {
                            QUICKTIME_INVALID_MOVIE
                        };
                        bus.write_long(sp + 4, rate as u32);
                        cpu.write_reg(Register::A7, sp + 4);
                        cpu.write_reg(Register::D0, err as u32);
                        record_movie_error(self, err);
                        Ok(())
                    }
                    0x002D => {
                        // SetMovieRate ($AAAA selector $002D)
                        // Sets the movie playback rate.
                        // pascal void SetMovieRate(Movie theMovie, Fixed rate);
                        // Inside Macintosh: QuickTime 1993, p. 2-185.
                        let sp = cpu.read_reg(Register::A7);
                        let rate = bus.read_long(sp) as i32;
                        let movie = bus.read_long(sp + 4);
                        let err = if let Some(state) = self.movie_states.get_mut(&movie) {
                            state.rate = rate;
                            if rate != 0 {
                                state.active = true;
                            } else {
                                state.active = false;
                            }
                            0
                        } else {
                            QUICKTIME_INVALID_MOVIE
                        };
                        cpu.write_reg(Register::A7, sp + 8);
                        cpu.write_reg(Register::D0, err as u32);
                        record_movie_error(self, err);
                        Ok(())
                    }
                    0x00F3 => {
                        // GetMoviePreferredRate ($AAAA selector $00F3)
                        // Returns a movie's preferred playback rate.
                        // MPW Universal Headers Movies.h:
                        //   THREEWORDINLINE(0x303C, 0x00F3, 0xAAAA)
                        // pascal Fixed GetMoviePreferredRate(Movie theMovie);
                        // Inside Macintosh: QuickTime 1993, pp. 2-130 to 2-131.
                        let sp = cpu.read_reg(Register::A7);
                        let movie = bus.read_long(sp);
                        let rate = self
                            .movie_states
                            .get(&movie)
                            .map(|state| state.preferred_rate)
                            .unwrap_or(0);
                        let err = if self.movie_states.contains_key(&movie) {
                            0
                        } else {
                            QUICKTIME_INVALID_MOVIE
                        };
                        bus.write_long(sp + 4, rate as u32);
                        cpu.write_reg(Register::A7, sp + 4);
                        cpu.write_reg(Register::D0, err as u32);
                        record_movie_error(self, err);
                        Ok(())
                    }
                    0x00F4 => {
                        // SetMoviePreferredRate ($AAAA selector $00F4)
                        // Sets a movie's preferred playback rate.
                        // MPW Universal Headers Movies.h:
                        //   THREEWORDINLINE(0x303C, 0x00F4, 0xAAAA)
                        // pascal void SetMoviePreferredRate(Movie theMovie, Fixed rate);
                        // Inside Macintosh: QuickTime 1993, pp. 2-130 to 2-131.
                        let sp = cpu.read_reg(Register::A7);
                        let rate = bus.read_long(sp) as i32;
                        let movie = bus.read_long(sp + 4);
                        let err = if let Some(state) = self.movie_states.get_mut(&movie) {
                            state.preferred_rate = rate;
                            0
                        } else {
                            QUICKTIME_INVALID_MOVIE
                        };
                        cpu.write_reg(Register::A7, sp + 8);
                        cpu.write_reg(Register::D0, err as u32);
                        record_movie_error(self, err);
                        Ok(())
                    }
                    0x002E => {
                        // GetMovieVolume ($AAAA selector $002E)
                        // Returns a movie's current 8.8 fixed-point volume.
                        // pascal short GetMovieVolume(Movie theMovie);
                        // Inside Macintosh: QuickTime 1993, p. 2-182.
                        let sp = cpu.read_reg(Register::A7);
                        let movie = bus.read_long(sp);
                        let volume = self
                            .movie_states
                            .get(&movie)
                            .map(|state| state.volume)
                            .unwrap_or(0);
                        let err = if self.movie_states.contains_key(&movie) {
                            0
                        } else {
                            QUICKTIME_INVALID_MOVIE
                        };
                        bus.write_word(sp + 4, volume as u16);
                        cpu.write_reg(Register::A7, sp + 4);
                        cpu.write_reg(Register::D0, err as u32);
                        record_movie_error(self, err);
                        Ok(())
                    }
                    0x002F => {
                        // SetMovieVolume ($AAAA selector $002F)
                        // Sets a movie's current 8.8 fixed-point volume.
                        // pascal void SetMovieVolume(Movie theMovie, short volume);
                        // Inside Macintosh: QuickTime 1993, p. 2-182.
                        let sp = cpu.read_reg(Register::A7);
                        let volume = bus.read_word(sp) as i16;
                        let movie = bus.read_long(sp + 2);
                        let err = if let Some(state) = self.movie_states.get_mut(&movie) {
                            state.volume = volume;
                            0
                        } else {
                            QUICKTIME_INVALID_MOVIE
                        };
                        cpu.write_reg(Register::A7, sp + 6);
                        cpu.write_reg(Register::D0, err as u32);
                        record_movie_error(self, err);
                        Ok(())
                    }
                    0x00F5 => {
                        // GetMoviePreferredVolume ($AAAA selector $00F5)
                        // Returns a movie's preferred 8.8 fixed-point volume.
                        // MPW Universal Headers Movies.h:
                        //   THREEWORDINLINE(0x303C, 0x00F5, 0xAAAA)
                        // pascal short GetMoviePreferredVolume(Movie theMovie);
                        // Inside Macintosh: QuickTime 1993, pp. 2-132 to 2-133.
                        let sp = cpu.read_reg(Register::A7);
                        let movie = bus.read_long(sp);
                        let volume = self
                            .movie_states
                            .get(&movie)
                            .map(|state| state.volume)
                            .unwrap_or(0);
                        let err = if self.movie_states.contains_key(&movie) {
                            0
                        } else {
                            QUICKTIME_INVALID_MOVIE
                        };
                        bus.write_word(sp + 4, volume as u16);
                        cpu.write_reg(Register::A7, sp + 4);
                        cpu.write_reg(Register::D0, err as u32);
                        record_movie_error(self, err);
                        Ok(())
                    }
                    0x00F6 => {
                        // SetMoviePreferredVolume ($AAAA selector $00F6)
                        // Sets a movie's preferred 8.8 fixed-point volume.
                        // MPW Universal Headers Movies.h:
                        //   THREEWORDINLINE(0x303C, 0x00F6, 0xAAAA)
                        // pascal void SetMoviePreferredVolume(Movie theMovie, short volume);
                        // Inside Macintosh: QuickTime 1993, pp. 2-132 to 2-133.
                        let sp = cpu.read_reg(Register::A7);
                        let volume = bus.read_word(sp) as i16;
                        let movie = bus.read_long(sp + 2);
                        let err = if let Some(state) = self.movie_states.get_mut(&movie) {
                            state.volume = volume;
                            0
                        } else {
                            QUICKTIME_INVALID_MOVIE
                        };
                        cpu.write_reg(Register::A7, sp + 6);
                        cpu.write_reg(Register::D0, err as u32);
                        record_movie_error(self, err);
                        Ok(())
                    }
                    0x00D5 => {
                        // CloseMovieFile ($AAAA selector $00D5)
                        // Closes an open movie file reference number.
                        // pascal OSErr CloseMovieFile(short resRefNum);
                        // Inside Macintosh: QuickTime 1993, p. 2-99.
                        let sp = cpu.read_reg(Register::A7);
                        let refnum = bus.read_word(sp);
                        let err = self.close_movie_file_refnum(bus, refnum);
                        bus.write_word(sp + 2, err as u16);
                        cpu.write_reg(Register::A7, sp + 2);
                        cpu.write_reg(Register::D0, err as u32);
                        record_movie_error(self, err);
                        Ok(())
                    }
                    0x00DD => {
                        // IsMovieDone ($AAAA selector $00DD)
                        // Reports whether a movie has finished playing.
                        // pascal Boolean IsMovieDone(Movie theMovie);
                        // Inside Macintosh: QuickTime 1993, pp. 2-125 to 2-126.
                        let sp = cpu.read_reg(Register::A7);
                        let movie = bus.read_long(sp);
                        let status = self.movie_states.get(&movie).map(|state| {
                            if state.rate < 0 {
                                state.current_time <= 0
                            } else {
                                state.current_time >= state.duration
                            }
                        });
                        let err = if status.is_some() {
                            0
                        } else {
                            QUICKTIME_INVALID_MOVIE
                        };
                        bus.write_word(sp + 4, if status.unwrap_or(true) { 0xFFFF } else { 0 });
                        cpu.write_reg(Register::A7, sp + 4);
                        cpu.write_reg(Register::D0, err as u32);
                        record_movie_error(self, err);
                        Ok(())
                    }
                    0x001F => {
                        // UpdateMovie ($AAAA selector $001F)
                        // Marks a movie for redrawing after an invalidated update area.
                        // pascal OSErr UpdateMovie(Movie theMovie);
                        // Inside Macintosh: QuickTime 1993, pp. 2-126 to 2-127.
                        let sp = cpu.read_reg(Register::A7);
                        let movie = bus.read_long(sp);
                        let err = if self.movie_states.contains_key(&movie) {
                            0
                        } else {
                            QUICKTIME_INVALID_MOVIE
                        };
                        bus.write_word(sp + 4, err as u16);
                        cpu.write_reg(Register::A7, sp + 4);
                        cpu.write_reg(Register::D0, err as u32);
                        record_movie_error(self, err);
                        Ok(())
                    }
                    0x00F9 => {
                        // GetMovieBox ($AAAA selector $00F9)
                        // Returns a movie's boundary rectangle.
                        // pascal void GetMovieBox(Movie theMovie, Rect *boxRect);
                        // Inside Macintosh: QuickTime 1993, pp. 2-161 to 2-162.
                        let sp = cpu.read_reg(Register::A7);
                        let box_ptr = bus.read_long(sp);
                        let movie = bus.read_long(sp + 4);
                        let rect = self.movie_states.get(&movie).map(|state| state.box_rect);
                        let err = if let Some(rect) = rect {
                            write_movie_box(bus, box_ptr, rect);
                            0
                        } else {
                            QUICKTIME_INVALID_MOVIE
                        };
                        cpu.write_reg(Register::A7, sp + 8);
                        cpu.write_reg(Register::D0, err as u32);
                        record_movie_error(self, err);
                        Ok(())
                    }
                    0x00FA => {
                        // SetMovieBox ($AAAA selector $00FA)
                        // Sets a movie's boundary rectangle.
                        // pascal void SetMovieBox(Movie theMovie, const Rect *boxRect);
                        // Inside Macintosh: QuickTime 1993, pp. 2-161 to 2-162.
                        let sp = cpu.read_reg(Register::A7);
                        let box_ptr = bus.read_long(sp);
                        let movie = bus.read_long(sp + 4);
                        let err = if let (Some(rect), Some(state)) = (
                            read_movie_box(bus, box_ptr),
                            self.movie_states.get_mut(&movie),
                        ) {
                            state.box_rect = rect;
                            0
                        } else {
                            QUICKTIME_INVALID_MOVIE
                        };
                        cpu.write_reg(Register::A7, sp + 8);
                        cpu.write_reg(Register::D0, err as u32);
                        record_movie_error(self, err);
                        Ok(())
                    }
                    _ => return_noerr(cpu),
                }
            }

            // SetFractEnable ($A814)
            // Enables or disables fractional character widths for the
            // Font Manager's character-width tables.
            // PROCEDURE SetFractEnable(fractEnable: BOOLEAN); [Not in ROM]
            // Inside Macintosh Volume IV (1986), p. IV-32.
            //
            // Per IM:IV IV-32: "If fractEnable is TRUE, fractional
            // character widths are enabled; if it's FALSE, the Font
            // Manager uses integer widths. To ensure compatibility
            // with existing applications, fractional character widths
            // are disabled by default." The assembly-language note on
            // the same page confirms: "From assembly language, you
            // can change the value of the global variable FractEnable."
            //
            // The FractEnable low-memory global lives at $0BF4 per
            // Macintosh Family Hardware Reference 2nd Ed. (1990)
            // Appendix B and the MPW LowMem.h SystemGlobals table.
            // MPW exposes the global via TWOWORDINLINE accessors:
            //   LMGetFractEnable() = MOVE.B $0BF4, D0  (0x1EB8 0x0BF4)
            //   LMSetFractEnable(v) = MOVE.B v, $0BF4  (0x11DF 0x0BF4)
            //
            // Pascal PROCEDURE protocol (caller perspective):
            //   Stack on entry: SP+0: fractEnable(2) — the BOOLEAN
            //   argument encoded as a 2-byte word with the value byte
            //   in the HIGH byte (MPW Pascal BOOLEAN convention).
            //   The trap pops 2 bytes; no function-result slot is
            //   reserved (this is a PROCEDURE, not a FUNCTION).
            //
            // Byte-write semantic: the real System 7.5 ROM writes the
            // raw Pascal BOOLEAN high byte verbatim to $0BF4 — TRUE
            // becomes the byte value 0x01 (not a normalised 0xFF) and
            // FALSE becomes 0x00. Systemless mirrors this exact byte by
            // reading SP+0 directly (no normalisation). BasiliskII
            // System 7.5.3 ROM follows the same convention, witnessed
            // by `a814_setfractenable_strict`.
            //
            // Regression coverage:
            //   tests::setfractenable_true_writes_one_byte_verbatim_to_fract_enable_global
            //   tests::setfractenable_false_writes_zero_byte_to_fract_enable_global
            //   tests::setfractenable_consumes_two_byte_boolean_argument_and_balances_stack
            // SetFractEnable ($A814): Writes FractEnable low-mem global ($0BF4); per IM:IV IV-32
            (true, 0x014) => {
                let sp = cpu.read_reg(Register::A7);
                // Pascal BOOLEAN at SP+0 (MPW convention: value byte
                // in the high byte of the 2-byte stack slot).
                let fract_enable_byte = bus.read_byte(sp);
                cpu.write_reg(Register::A7, sp + 2);
                bus.write_byte(0x0BF4, fract_enable_byte);
                Ok(())
            }

            // ========== Printing Manager ==========

            // PrGlue ($A8FD)
            // Printing Manager dispatch. 32-bit selector on top of the stack.
            // Inside Macintosh Volume V (1986), p. V-408.
            //
            // Selector format (assembly-language note, IM:V V-408):
            //   bits 31-24 = routine selector
            //   bits 15-8  = parameter byte count (bytes to pop after selector)
            //
            // The second selector byte is not a generic stack-result byte
            // count. For example PrintDefault is selector $20040480 and is a
            // PROCEDURE, while PrOpenDoc is selector $04000C00 and returns a
            // TPPrPort. Results are therefore routine-specific.
            //
            // In emulation, printing is not supported. All routines are no-ops
            // that pop their parameters and write default return values.
            // PrSetError / PrError still model the shared PrintErr state so
            // callers can observe the last Printing Manager result code.
            //
            // Regression coverage:
            //   tests::prglue_selector_param_byte_count_controls_stack_pop
            //   tests::prglue_propendoc_returns_nil_and_consumes_three_pointer_arguments
            //   tests::prglue_prvalidate_returns_false_boolean_result
            //   tests::prglue_prstldialog_returns_true_boolean_result
            //   tests::prglue_prjobdialog_returns_true_boolean_result
            //   tests::prglue_prclosedoc_consumes_tpprport_argument_without_function_result_slot
            //   tests::prglue_prerror_returns_noerr_word_with_zero_result_bits_selector
            //   tests::prglue_prseterror_consumes_ierr_word_argument_without_function_result_slot
            //   tests::prglue_prseterror_updates_prerror_state_roundtrip
            // PrGlue ($A8FD): Selector-based Printing Manager dispatcher per
            // IM:V V-408, with no-op HLE implementations for printing-disabled
            // runtime paths.
            (true, 0x0FD) => {
                let sp = cpu.read_reg(Register::A7);
                let selector = bus.read_long(sp);
                let routine = (selector >> 24) & 0xFF;

                // Extract the parameter size encoded in bits 15-8.
                let param_bytes = (selector >> 8) & 0xFF;

                let total_pop = 4 + param_bytes; // selector + params

                match routine {
                    0x04 => {
                        // PrOpenDoc: returns TPPrPort (4 bytes) — return NIL
                        // Even when the selector's result-size bits are 0
                        // ($04000C00), callers reserve a TPPrPort result
                        // slot per routine signature. Mirror the nil return
                        // in D0 as well so inline shims can observe it.
                        self.printing_error = 0;
                        bus.write_long(sp + total_pop, 0);
                        cpu.write_reg(Register::D0, 0);
                        cpu.write_reg(Register::A7, sp + total_pop);
                    }
                    0x08 => {
                        // PrCloseDoc: consumes one TPPrPort argument and
                        // returns no function result.
                        self.printing_error = 0;
                        cpu.write_reg(Register::A7, sp + total_pop);
                    }
                    0x10 | 0x18 => {
                        // PrOpenPage / PrClosePage: procedures.
                        self.printing_error = 0;
                        cpu.write_reg(Register::A7, sp + total_pop);
                    }
                    0x20 => {
                        // PrintDefault: PROCEDURE PrintDefault(hPrint).
                        // Selector $20040480 has a non-zero second byte, but
                        // that byte is not a result size; writing a stack
                        // result here corrupts MPW compatibility shims whose
                        // LINK frame sits immediately above the selector and
                        // THPrint argument.
                        self.printing_error = 0;
                        cpu.write_reg(Register::D0, 0);
                        cpu.write_reg(Register::A7, sp + total_pop);
                    }
                    0xC8 | 0xD0 => {
                        // PrOpen / PrClose: procedures with no stack
                        // arguments. They consume only the selector long
                        // and do not perturb the shared PrintErr state.
                        cpu.write_reg(Register::A7, sp + total_pop);
                    }
                    0x2A | 0x32 => {
                        // PrStlDialog / PrJobDialog: returns BOOLEAN (2 bytes)
                        // Return TRUE (user clicked OK) so games proceed past print dialogs
                        self.printing_error = 0;
                        bus.write_word(sp + total_pop, 1); // TRUE
                        cpu.write_reg(Register::D0, 1);
                        cpu.write_reg(Register::A7, sp + total_pop);
                    }
                    0x3C | 0x44 => {
                        // PrStlInit / PrJobInit: return TPPrDlg. No native
                        // printer UI is available, so return NIL.
                        self.printing_error = 0;
                        bus.write_long(sp + total_pop, 0);
                        cpu.write_reg(Register::D0, 0);
                        cpu.write_reg(Register::A7, sp + total_pop);
                    }
                    0x4A => {
                        // PrDlgMain: return TRUE so callers take the
                        // confirmed path if they invoked a customized print
                        // dialog.
                        self.printing_error = 0;
                        bus.write_word(sp + total_pop, 1);
                        cpu.write_reg(Register::D0, 1);
                        cpu.write_reg(Register::A7, sp + total_pop);
                    }
                    0x52 => {
                        // PrValidate: returns BOOLEAN (2 bytes)
                        // Return FALSE (record is valid, no changes needed)
                        self.printing_error = 0;
                        bus.write_word(sp + total_pop, 0); // FALSE
                        cpu.write_reg(Register::D0, 0);
                        cpu.write_reg(Register::A7, sp + total_pop);
                    }
                    0x58 | 0x60 | 0x70 | 0x80 | 0x88 | 0xA0 => {
                        // PrJobMerge, PrPicFile, PrGeneral, PrDrvrOpen,
                        // PrDrvrClose, PrCtlCall: procedures.
                        self.printing_error = 0;
                        cpu.write_reg(Register::D0, 0);
                        cpu.write_reg(Register::A7, sp + total_pop);
                    }
                    0x94 => {
                        // PrDrvrDCE: return a DCE Handle. Printing is not
                        // supported, so return NIL.
                        self.printing_error = 0;
                        bus.write_long(sp + total_pop, 0);
                        cpu.write_reg(Register::D0, 0);
                        cpu.write_reg(Register::A7, sp + total_pop);
                    }
                    0x9A => {
                        // PrDrvrVers: return driver version. No printer
                        // driver is present.
                        self.printing_error = 0;
                        bus.write_word(sp + total_pop, 0);
                        cpu.write_reg(Register::D0, 0);
                        cpu.write_reg(Register::A7, sp + total_pop);
                    }
                    0xBA => {
                        // PrError: returns INTEGER (2 bytes). MPW encodes
                        // the selector as 0xBA00_0000 — the per-routine
                        // return-size bits are zero for PrError, but real
                        // ROM returns a 2-byte result regardless because
                        // the dispatcher knows the routine signature by
                        // trap table. Return the stored PrintErr word so
                        // PrSetError can affect later queries.
                        bus.write_word(sp + total_pop, self.printing_error as u16);
                        cpu.write_reg(Register::D0, self.printing_error as u32);
                        cpu.write_reg(Register::A7, sp + total_pop);
                    }
                    0xC0 => {
                        // PrSetError: stores the new printing error code in
                        // the shared PrintErr global and returns no result.
                        self.printing_error = bus.read_word(sp + 4) as i16;
                        cpu.write_reg(Register::D0, 0);
                        cpu.write_reg(Register::A7, sp + total_pop);
                    }
                    _ => {
                        // Unknown printing routines: pop the documented
                        // selector and parameter bytes, but do not infer a
                        // result slot from the second selector byte.
                        self.printing_error = 0;
                        cpu.write_reg(Register::D0, 0);
                        cpu.write_reg(Register::A7, sp + total_pop);
                    }
                }
                Ok(())
            }

            // ========== Script Manager ==========

            // ScriptUtil ($A8B5)
            // Script Manager dispatch. Selector is a LONGINT on top of the stack.
            // Inside Macintosh Volume V, V-288
            //
            // In the emulator we always return Roman script (0) for script queries
            // and noErr for set operations. This is sufficient for English-only games.
            //
            // Regression coverage:
            //   fontscript_returns_roman_script
            //   intlscript_returns_roman_script
            //   keyscript_returns_roman_script
            //   font2script_returns_roman_script
            //   getenvirons_returns_zero
            //   setenvirons_returns_noerr
            //   getscript_returns_zero
            //   setscript_returns_noerr
            //   charbyte_returns_single_byte
            //   chartype_returns_zero
            //   char2pixel_returns_zero
            //   pixel2char_maps_pixel_to_character
            //   transliterate_converts_text
            //   findword_locates_word_boundaries
            //   hilitetext_computes_highlight_ranges
            //   drawjust_draws_justified_text
            //   measurejust_measures_justified_text
            //   styledlinebreak_full_style_run_fits_returns_overflow_and_decrements_width
            //   styledlinebreak_breaks_at_last_space_before_overflow
            //   styledlinebreak_first_long_word_breaks_on_character_boundary
            // ScriptUtil ($A8B5): Dispatches legacy selectors plus System 7 encoded text-utility selectors; returns Roman/noErr/0 fallbacks where Systemless has no script-system state.
            (true, 0x0B5) => {
                let sp = cpu.read_reg(Register::A7);
                // MPW's inline wraps ScriptUtil selectors as
                //   MOVE.L #<encoding>.L, -(SP)
                //   _ScriptUtil
                // where the high word encodes result-size and argument-count
                // metadata. Older Script Manager calls use the low byte as the
                // routine number; System 7 text utilities use full selectors.
                let raw_selector = bus.read_long(sp);
                let selector = (raw_selector & 0xFF) as i32;

                match raw_selector {
                    // TruncString ($8208FFE0): FUNCTION TruncString(width: INTEGER;
                    //   VAR theString: Str255; truncWhere: TruncCode): INTEGER
                    // Returning smNotTruncated leaves the string unchanged.
                    // Stack: selector(4), args(8), result(2). Pop selector + args.
                    // Inside Macintosh Volume VI, 14-59..14-60 and Table C-3.
                    0x8208_FFE0 => {
                        bus.write_word(sp + 12, 0); // smNotTruncated
                        cpu.write_reg(Register::A7, sp + 12);
                        return Some(Ok(()));
                    }
                    // TruncText ($820CFFDE): FUNCTION TruncText(width: INTEGER;
                    //   textPtr: Ptr; VAR length: INTEGER; truncWhere: TruncCode): INTEGER
                    // Returning smNotTruncated leaves the pointed-to text and length unchanged.
                    // Stack: selector(4), args(12), result(2). Pop selector + args.
                    // Inside Macintosh Volume VI, 14-59..14-60 and Table C-3.
                    0x820C_FFDE => {
                        bus.write_word(sp + 16, 0); // smNotTruncated
                        cpu.write_reg(Register::A7, sp + 16);
                        return Some(Ok(()));
                    }
                    // StyledLineBreak ($821CFFFE): FUNCTION StyledLineBreak(textPtr: Ptr;
                    //   textLen, textStart, textEnd, flags: LongInt; VAR textWidth: Fixed;
                    //   VAR textOffset: LongInt): StyledLineBreakCode.
                    // Stack: selector(4), args(28), result(2). Pop selector + args.
                    // Inside Macintosh: Text 1993, pp. 5-79..5-81 and Table D-3.
                    0x821C_FFFE => {
                        return Some(self.handle_scriptutil_styled_line_break(bus, cpu, sp));
                    }
                    _ => {}
                }

                match selector {
                    // FontScript (0): FUNCTION FontScript: INTEGER
                    // Returns script code. Stack: selector(4), result space(2)
                    0 => {
                        bus.write_word(sp + 4, 0); // smRoman = 0
                        cpu.write_reg(Register::A7, sp + 4); // pop selector, result stays
                    }
                    // IntlScript (2): FUNCTION IntlScript: INTEGER
                    2 => {
                        bus.write_word(sp + 4, 0); // smRoman
                        cpu.write_reg(Register::A7, sp + 4);
                    }
                    // KeyScript (4): FUNCTION KeyScript: INTEGER
                    4 => {
                        bus.write_word(sp + 4, 0); // smRoman
                        cpu.write_reg(Register::A7, sp + 4);
                    }
                    // Font2Script (6): FUNCTION Font2Script(fontNum: INTEGER): INTEGER
                    // Stack: selector(4), fontNum(2), result(2)
                    6 => {
                        bus.write_word(sp + 6, 0); // smRoman
                        cpu.write_reg(Register::A7, sp + 6); // pop selector + fontNum
                    }
                    // GetEnvirons (8): FUNCTION GetEnvirons(verb: INTEGER): LongInt
                    // Stack: selector(4), verb(2), result(4)
                    8 => {
                        bus.write_long(sp + 6, 0); // return 0
                        cpu.write_reg(Register::A7, sp + 6); // pop selector + verb
                    }
                    // SetEnvirons (10): FUNCTION SetEnvirons(verb: INTEGER; param: LongInt): OSErr
                    // Stack: selector(4), verb(2), param(4), result(2)
                    10 => {
                        bus.write_word(sp + 10, 0); // noErr
                        cpu.write_reg(Register::A7, sp + 10); // pop selector + verb + param
                    }
                    // GetScript (12): FUNCTION GetScript(script: INTEGER; verb: INTEGER): LongInt
                    // Stack: selector(4), script(2), verb(2), result(4)
                    12 => {
                        bus.write_long(sp + 8, 0); // return 0
                        cpu.write_reg(Register::A7, sp + 8); // pop selector + script + verb
                        cpu.write_reg(Register::D0, 0);
                    }
                    // SetScript (14): FUNCTION SetScript(script: INTEGER; verb: INTEGER; param: LongInt): OSErr
                    // Stack: selector(4), script(2), verb(2), param(4), result(2)
                    14 => {
                        bus.write_word(sp + 12, 0); // noErr
                        cpu.write_reg(Register::A7, sp + 12);
                        cpu.write_reg(Register::D0, 0);
                    }
                    // CharByte (16): FUNCTION CharByte(textBuf: Ptr; textOffset: INTEGER): INTEGER
                    // Returns smSingleByte (0) for all chars in Roman script.
                    // Stack: selector(4), textBuf(4), textOffset(2), result(2)
                    16 => {
                        bus.write_word(sp + 10, 0); // smSingleByte = 0
                        cpu.write_reg(Register::A7, sp + 10);
                    }
                    // CharType (18): FUNCTION CharType(textBuf: Ptr; textOffset: INTEGER): INTEGER
                    // Stack: selector(4), textBuf(4), textOffset(2), result(2)
                    18 => {
                        bus.write_word(sp + 10, 0); // return 0 (left-to-right)
                        cpu.write_reg(Register::A7, sp + 10);
                    }
                    // Char2Pixel (22): FUNCTION Char2Pixel(textBuf: Ptr; textLen: INTEGER;
                    //   slop: INTEGER; offset: INTEGER; direction: INTEGER): INTEGER
                    // Stack: selector(4), textBuf(4), textLen(2), slop(2), offset(2),
                    //        direction(2), result(2)
                    22 => {
                        bus.write_word(sp + 16, 0); // return 0 pixel offset
                        cpu.write_reg(Register::A7, sp + 16);
                    }
                    // Pixel2Char (20): FUNCTION Pixel2Char(textBuf: Ptr; textLen: INTEGER;
                    //   slop: INTEGER; pixelWidth: INTEGER; VAR leadingEdge: BOOLEAN): INTEGER
                    // Pascal pushes args left-to-right (first arg deepest), so the
                    // VAR leadingEdge pointer is the LAST arg pushed and lives at
                    // sp+4 (just past the selector long). Layout post-trap-entry:
                    //   sp+0  selector long
                    //   sp+4  leadingEdge_ptr (last arg, 4 bytes)
                    //   sp+8  pixelWidth (2 bytes)
                    //   sp+10 slop (2 bytes)
                    //   sp+12 textLen (2 bytes)
                    //   sp+14 textBuf (first arg, 4 bytes)
                    //   sp+18 INTEGER result slot
                    // Pop 18 bytes (selector + 14 arg bytes), leave 2-byte result.
                    // Inside Macintosh Volume V, V-310
                    20 => {
                        let leading_edge_ptr = bus.read_long(sp + 4);
                        if leading_edge_ptr != 0 {
                            bus.write_byte(leading_edge_ptr, 0);
                        }
                        bus.write_word(sp + 18, 0); // return offset 0
                        cpu.write_reg(Register::A7, sp + 18);
                    }
                    // Transliterate (24): FUNCTION Transliterate(srcHandle, dstHandle: Handle;
                    //   target: INTEGER; srcMask: LongInt): OSErr
                    // Stack: selector(4), srcHandle(4), dstHandle(4), target(2),
                    //        srcMask(4), result(2). Pop 14 bytes of args, leave result.
                    // Inside Macintosh Volume V, V-312
                    24 => {
                        bus.write_word(sp + 18, 0); // noErr
                        cpu.write_reg(Register::A7, sp + 18);
                    }
                    // FindWord (26): PROCEDURE FindWord(textPtr: Ptr; textLength, offset: INTEGER;
                    //   leadingEdge: BOOLEAN; breaksPtr: Ptr; VAR offsets: OffsetTable)
                    // Pascal pushes args left-to-right (first arg deepest), so the
                    // VAR offsets pointer is the LAST arg pushed and lives at sp+4.
                    // Layout post-trap-entry:
                    //   sp+0  selector long
                    //   sp+4  offsets_ptr (last arg, 4 bytes)
                    //   sp+8  breaksPtr (4 bytes)
                    //   sp+12 leadingEdge (2 bytes)
                    //   sp+14 offset (2 bytes)
                    //   sp+16 textLength (2 bytes)
                    //   sp+18 textPtr (first arg, 4 bytes)
                    // No return value. Pop 22 (selector + 18 arg bytes).
                    // OffsetTable is ARRAY[0..2] OF OffPair = 3*4 = 12 bytes
                    // (Inside Macintosh Volume VI, p. 33514 summary; Text 1993,
                    // p. 10664).
                    // Inside Macintosh Volume V, V-313
                    26 => {
                        let offsets_ptr = bus.read_long(sp + 4);
                        if offsets_ptr != 0 {
                            bus.write_bytes(offsets_ptr, &[0u8; 12]);
                        }
                        cpu.write_reg(Register::A7, sp + 22);
                    }
                    // HiliteText (28): PROCEDURE HiliteText(textPtr: Ptr; textLength,
                    //   firstOffset, secondOffset: INTEGER; VAR offsets: OffsetTable)
                    // Pascal pushes args left-to-right; the VAR offsets pointer is
                    // the LAST arg pushed and lives at sp+4. Layout post-trap-entry:
                    //   sp+0  selector long
                    //   sp+4  offsets_ptr (last arg, 4 bytes)
                    //   sp+8  secondOffset (2 bytes)
                    //   sp+10 firstOffset (2 bytes)
                    //   sp+12 textLength (2 bytes)
                    //   sp+14 textPtr (first arg, 4 bytes)
                    // No return. Pop 18 (selector + 14 arg bytes). OffsetTable is
                    // 12 bytes (Inside Macintosh Volume VI, p. 33514 summary).
                    // Inside Macintosh Volume V, V-314
                    28 => {
                        let offsets_ptr = bus.read_long(sp + 4);
                        if offsets_ptr != 0 {
                            bus.write_bytes(offsets_ptr, &[0u8; 12]);
                        }
                        cpu.write_reg(Register::A7, sp + 18);
                    }
                    // DrawJust (30): PROCEDURE DrawJust(textPtr: Ptr; textLength, slop: INTEGER)
                    // No return, no output. Systemless does not implement justified text drawing
                    // here. Stack: selector(4) + 8 bytes of args; pop 12.
                    // Inside Macintosh Volume V, V-315
                    30 => {
                        cpu.write_reg(Register::A7, sp + 12);
                    }
                    // MeasureJust (32): PROCEDURE MeasureJust(textPtr: Ptr; textLength,
                    //   slop: INTEGER; charLocs: Ptr)
                    // No return. Stack: selector(4) + 12 bytes of args; pop 16.
                    // Inside Macintosh Volume V, V-315
                    32 => {
                        cpu.write_reg(Register::A7, sp + 16);
                    }
                    _ => {
                        let result_bytes = (raw_selector >> 24) & 0x7F;
                        let arg_bytes = (raw_selector >> 16) & 0xFF;
                        if (raw_selector & 0x8000_0000) != 0
                            && matches!(result_bytes, 0 | 1 | 2 | 4)
                        {
                            let result_sp = sp + 4 + arg_bytes;
                            match result_bytes {
                                1 => bus.write_byte(result_sp, 0),
                                2 => bus.write_word(result_sp, 0),
                                4 => bus.write_long(result_sp, 0),
                                _ => {}
                            }
                            eprintln!(
                                "[TRAP] ScriptUtil: unhandled encoded selector ${:08X}; popped {} arg bytes",
                                raw_selector, arg_bytes
                            );
                            cpu.write_reg(Register::A7, result_sp);
                        } else {
                            // Unknown legacy selector — pop the selector and return.
                            eprintln!("[TRAP] ScriptUtil: unhandled selector {}", selector);
                            cpu.write_reg(Register::A7, sp + 4);
                        }
                    }
                }
                Ok(())
            }

            // Pack9 ($A82B) — StackSpace alias
            // Inside Macintosh Volume IV (1986), pp. IV-78 and IV-81;
            // Inside Macintosh: Memory 1992, pp. 2-69 to 2-70.
            // Pack9 maps directly to _StackSpace ($A065), which returns
            // its LongInt result in D0 and consumes no Pascal arguments.
            (true, 0x02B) => return self.dispatch_memory(false, 0x65, cpu, bus),

            // Pack10 ($A82C) — NewEmptyHandle alias
            // Inside Macintosh Volume IV (1986), pp. IV-78 and IV-81;
            // Inside Macintosh: Memory 1992, p. 2-33.
            // Pack10 maps directly to _NewEmptyHandle ($A066), which
            // returns its Handle result in A0 and consumes no Pascal
            // arguments.
            (true, 0x02C) => return self.dispatch_memory(false, 0x66, cpu, bus),

            // Pack11 ($A82D) — Edition Manager
            // Inside Macintosh: Interapplication Communication 1993,
            // pp. 12-60 and 12-103. `_Pack11` is the package trap for
            // the Edition Manager; `InitEditionPack` is selector $0100
            // and takes no Pascal arguments. The public MPW glue passes
            // the selector in D0, matching the Pack13-style package trap
            // convention.
            //
            // Keep the explicit `InitEditionPack` path separate from the
            // generic selector-byte heuristic so the documented bootstrap
            // routine remains obvious to future readers.
            //
            // Regression coverage:
            //   a82d_pack11_initeditionpack_strict
            // Pack11 ($A82D): `_Pack11` Edition Manager. `InitEditionPack` selector $0100 returns noErr and takes no Pascal args; other selectors still use the generic pop fallback.
            (true, 0x02D) => {
                let selector = cpu.read_reg(Register::D0) as u16;
                if selector == 0x0100 {
                    cpu.write_reg(Register::D0, 0);
                } else {
                    let sp = cpu.read_reg(Register::A7);
                    let param_size = ((selector >> 8) & 0xFF) as u32;
                    let total = 2 + if (2..=48).contains(&param_size) {
                        param_size
                    } else {
                        0
                    };
                    cpu.write_reg(Register::A7, sp + total);
                    cpu.write_reg(Register::D0, 0);
                }
                Ok(())
            }

            // Pack13 ($A82F) — Data Access Manager
            // Inside Macintosh: Interapplication Communication 1993,
            // pp. 12-60 and 12-103.
            //
            // The Data Access Manager macros place the routine selector
            // in D0 and call _Pack13. Systemless HLE currently only needs
            // the InitDBPack selector ($0100), so this arm is a D0-driven
            // no-op: return noErr and preserve the caller's Pascal stack.
            //
            // Regression coverage:
            //   src/trap/toolbox.rs::tests::pack13_initdbpack_selector_returns_noerr_and_preserves_stack
            (true, 0x02F) => {
                let _selector = cpu.read_reg(Register::D0) as u16;
                cpu.write_reg(Register::D0, 0);
                Ok(())
            }

            // Pack14 ($A830) — Help Manager
            //
            // Twenty-one-routine selector dispatcher providing balloon
            // help: status query, balloon show/remove, font config,
            // help-resource lookup, and help-message extraction.
            //
            // Selector encoding is `(arg_words << 8) | routine` per
            // IM:VI 11148+ Pack14 trap macro table — the same
            // Apple-Events Pack8 convention, NOT the Pack2/3/6 pure-
            // low-byte convention. The high byte of the selector
            // gives the number of WORDS of args pushed on the stack;
            // arg_bytes = high_byte * 2. Total pop = 2 (selector) +
            // arg_bytes.
            //
            // Pascal calling convention: caller pre-pushes a 2-byte
            // result slot for OSErr / Boolean / Integer FUNCTION
            // returns, then pushes args left-to-right (first source-
            // listed arg deepest, last shallowest at SP+2 just above
            // the selector word at SP+0). Trap pops 2 + arg_bytes,
            // exposing the result slot at the new SP+0. We mirror
            // each result to BOTH the stack slot AND D0 for callers
            // that read either way (matches the Pack6 / IUMagString
            // pattern).
            //
            // HLE compromise: Systemless has no Balloon Help subsystem
            // — no cursor tracking, no balloon WDEF / window, no
            // 'hmnu' / 'hdlg' / 'hrct' / 'hwin' / 'hovr' / 'hfdr'
            // resource walking, no help font cache. Status queries
            // collapse to "help disabled" (HMGetBalloons returns
            // FALSE, HMIsBalloon returns FALSE); show/remove balloon
            // ops return hmHelpDisabled (-850); set ops are no-op
            // noErr; get ops write defensive defaults (NIL handles,
            // 0 fonts, empty Rects, -1 resource IDs) plus the IM-
            // documented error codes for "no resource set" paths
            // (resNotFound for HMGet*ResID, hmHelpManagerNotInited
            // for HMGetHelpMenuHandle); resource extraction routines
            // return resNotFound. Apps that defensively check OSErr
            // before dereffing fall through cleanly; apps that need
            // actual help balloons see the documented "help disabled"
            // path which matches what System 7.5.3 would do if the
            // user toggled Balloon Help off via the Help menu.
            //
            // The selector encoding pinned by the existing stub
            // heuristic was actively wrong: it interpreted the high
            // byte as BYTE count rather than WORD count, and clamped
            // to 2..=48. So $0104 HMSetBalloons (high=$01, true args
            // = 2 bytes) popped only 2 (clamp filtered $01 out),
            // leaving 2 args bytes + result slot stranded; $0B01
            // HMShowBalloon (high=$0B = 11 words = 22 args bytes)
            // popped 11 instead of 22, leaving 11 args bytes
            // stranded. Any real-game caller would crash on RTS.
            //
            // Inside Macintosh: More Macintosh Toolbox 1993, ch. 3,
            // Help Manager, pages 3-1..3-173 + selector summary at
            // page 3-173 (MMTb 11320..11340).
            // Pack14 / Help Manager ($A830): Per-selector Pascal frames per IM:MMTb 1993 ch.3 + selector table 3-173: $0002 HMRemoveBalloon pop 2 D0=hmHelpDisabled, $0003 HMGetBalloons pop 2 D0=0 result=FALSE, $0007 HMIsBalloon pop 2 D0=0 result=FALSE, $0104 HMSetBalloons pop 4 D0=0, $0108 HMSetFont pop 4 D0=0, $0109 HMSetFontSize pop 4 D0=0, $010C HMSetDialogResID pop 4 D0=0, $0200 HMGetHelpMenuHandle pop 6 writes NIL to *mh D0=hmHelpManagerNotInited, $020A HMGetFont pop 6 writes 0 to *font D0=0, $020B HMGetFontSize pop 6 writes 0 to *fontSize D0=0, $020D HMSetMenuResID pop 6 D0=0, $0213 HMGetDialogResID pop 6 writes -1 to *resID D0=resNotFound, $0215 HMGetBalloonWindow pop 6 writes NIL to *window D0=0, $0314 HMGetMenuResID pop 8 writes -1 to *resID D0=resNotFound, $040E HMBalloonRect pop 10 writes Rect(0,0,0,0) D0=0, $040F HMBalloonPict pop 10 writes NIL to *coolPict D0=0, $0410 HMScanTemplateItems pop 10 D0=resNotFound, $0711 HMExtractHelpMsg pop 16 D0=resNotFound, $0B01 HMShowBalloon pop 24 D0=hmHelpDisabled, $0E05 HMShowMenuBalloon pop 30 D0=hmHelpDisabled, $1306 HMGetIndHelpMsg pop 40 D0=resNotFound.
            (true, 0x030) => {
                const HM_HELP_DISABLED: i16 = -850;
                const HM_HELP_MGR_NOT_INITED: i16 = -855;
                const RES_NOT_FOUND: i16 = -192;

                let sp = cpu.read_reg(Register::A7);
                let selector = bus.read_word(sp);

                // Helper: write OSErr/Integer result word to BOTH the
                // stack slot at sp+pop_total AND D0, then advance A7.
                let finish =
                    |bus: &mut MacMemoryBus, cpu: &mut dyn CpuOps, pop_total: u32, result: i16| {
                        bus.write_word(sp + pop_total, result as u16);
                        cpu.write_reg(Register::A7, sp + pop_total);
                        cpu.write_reg(Register::D0, result as i32 as u32);
                    };

                match selector {
                    // FUNCTION HMRemoveBalloon: OSErr;
                    // IM:MMTb 3-105. Pop = 2 (selector only).
                    // No balloon ever up in HLE → noErr per the IM
                    // result table ("No error or the help balloon
                    // was removed").
                    0x0002 => finish(bus, cpu, 2, 0),

                    // FUNCTION HMGetBalloons: Boolean;
                    // IM:MMTb 3-98. Pop = 2.
                    // Help disabled in HLE → FALSE (0).
                    0x0003 => finish(bus, cpu, 2, 0),

                    // FUNCTION HMIsBalloon: Boolean;
                    // IM:MMTb 3-99. Pop = 2.
                    // No balloon up in HLE → FALSE (0).
                    0x0007 => finish(bus, cpu, 2, 0),

                    // FUNCTION HMSetBalloons(flag: Boolean): OSErr;
                    // IM:MMTb 3-107. Pop = 4 (selector + flag).
                    // Accept and ignore — no help to enable/disable.
                    0x0104 => finish(bus, cpu, 4, 0),

                    // FUNCTION HMSetFont(font: Integer): OSErr;
                    // IM:MMTb 3-112. Pop = 4. Accept and ignore.
                    0x0108 => finish(bus, cpu, 4, 0),

                    // FUNCTION HMSetFontSize(fontSize: Integer): OSErr;
                    // IM:MMTb 3-113. Pop = 4. Accept and ignore.
                    0x0109 => finish(bus, cpu, 4, 0),

                    // FUNCTION HMSetDialogResID(resID: Integer): OSErr;
                    // IM:MMTb 3-117. Pop = 4. Accept and ignore.
                    0x010C => finish(bus, cpu, 4, 0),

                    // FUNCTION HMGetHelpMenuHandle(VAR mh: MenuHandle): OSErr;
                    // IM:MMTb 3-109. Pop = 6 (sel + mh ptr).
                    // mh ptr at SP+2. Write NIL to *mh per
                    // hmHelpManagerNotInited contract.
                    0x0200 => {
                        let mh_ptr = bus.read_long(sp + 2);
                        if mh_ptr != 0 {
                            bus.write_long(mh_ptr, 0);
                        }
                        finish(bus, cpu, 6, HM_HELP_MGR_NOT_INITED);
                    }

                    // FUNCTION HMGetFont(VAR font: Integer): OSErr;
                    // IM:MMTb 3-110. Pop = 6. Write 0 (system font)
                    // to *font.
                    0x020A => {
                        let font_ptr = bus.read_long(sp + 2);
                        if font_ptr != 0 {
                            bus.write_word(font_ptr, 0);
                        }
                        finish(bus, cpu, 6, 0);
                    }

                    // FUNCTION HMGetFontSize(VAR fontSize: Integer): OSErr;
                    // IM:MMTb 3-111. Pop = 6. Write 0 (system size)
                    // to *fontSize.
                    0x020B => {
                        let size_ptr = bus.read_long(sp + 2);
                        if size_ptr != 0 {
                            bus.write_word(size_ptr, 0);
                        }
                        finish(bus, cpu, 6, 0);
                    }

                    // FUNCTION HMSetMenuResID(menuID, resID: Integer): OSErr;
                    // IM:MMTb 3-114. Pop = 6. resID at SP+2 (last
                    // arg), menuID at SP+4. Accept and ignore.
                    0x020D => finish(bus, cpu, 6, 0),

                    // FUNCTION HMGetDialogResID(VAR resID: Integer): OSErr;
                    // IM:MMTb 3-118. Pop = 6. Write -1 to *resID
                    // per "no hdlg set" → resNotFound contract.
                    0x0213 => {
                        let res_id_ptr = bus.read_long(sp + 2);
                        if res_id_ptr != 0 {
                            bus.write_word(res_id_ptr, (-1i16) as u16);
                        }
                        finish(bus, cpu, 6, RES_NOT_FOUND);
                    }

                    // FUNCTION HMGetBalloonWindow(VAR window: WindowPtr): OSErr;
                    // IM:MMTb 3-121. Pop = 6. Write NIL to *window
                    // per "no balloon up" contract.
                    0x0215 => {
                        let window_ptr = bus.read_long(sp + 2);
                        if window_ptr != 0 {
                            bus.write_long(window_ptr, 0);
                        }
                        finish(bus, cpu, 6, 0);
                    }

                    // FUNCTION HMGetMenuResID(menuID: Integer;
                    //                         VAR resID: Integer): OSErr;
                    // IM:MMTb 3-115. Pop = 8. resID ptr at SP+2
                    // (last arg), menuID at SP+6. Write -1 to
                    // *resID per "no hmnu set" → resNotFound.
                    0x0314 => {
                        let res_id_ptr = bus.read_long(sp + 2);
                        if res_id_ptr != 0 {
                            bus.write_word(res_id_ptr, (-1i16) as u16);
                        }
                        finish(bus, cpu, 8, RES_NOT_FOUND);
                    }

                    // FUNCTION HMBalloonRect(aHelpMsg: HMMessageRecord;
                    //                        VAR coolRect: Rect): OSErr;
                    // IM:MMTb 3-119. Pop = 10. coolRect ptr at SP+2
                    // (last arg), aHelpMsg ptr at SP+6. Write
                    // Rect(0,0,0,0) — empty, no balloon to size.
                    0x040E => {
                        let rect_ptr = bus.read_long(sp + 2);
                        if rect_ptr != 0 {
                            bus.write_word(rect_ptr, 0);
                            bus.write_word(rect_ptr + 2, 0);
                            bus.write_word(rect_ptr + 4, 0);
                            bus.write_word(rect_ptr + 6, 0);
                        }
                        finish(bus, cpu, 10, 0);
                    }

                    // FUNCTION HMBalloonPict(aHelpMsg: HMMessageRecord;
                    //                        VAR coolPict: PicHandle): OSErr;
                    // IM:MMTb 3-120. Pop = 10. coolPict ptr at SP+2.
                    // Write NIL to *coolPict.
                    0x040F => {
                        let pict_ptr = bus.read_long(sp + 2);
                        if pict_ptr != 0 {
                            bus.write_long(pict_ptr, 0);
                        }
                        finish(bus, cpu, 10, 0);
                    }

                    // FUNCTION HMScanTemplateItems(whichID,
                    //                              whichResFile: Integer;
                    //                              whichType: ResType): OSErr;
                    // IM:MMTb 3-116. Pop = 10. No help resources
                    // ever loaded → resNotFound.
                    0x0410 => finish(bus, cpu, 10, RES_NOT_FOUND),

                    // FUNCTION HMExtractHelpMsg(whichType: ResType;
                    //                           whichResID, whichMsg,
                    //                           whichState: Integer;
                    //                           VAR aHelpMsg:
                    //                           HMMessageRecord): OSErr;
                    // IM:MMTb 3-126. Pop = 16. No help resources →
                    // resNotFound. Don't touch aHelpMsg (caller's
                    // record stays untouched).
                    0x0711 => finish(bus, cpu, 16, RES_NOT_FOUND),

                    // FUNCTION HMShowBalloon(aHelpMsg: HMMessageRecord;
                    //                        tip: Point;
                    //                        alternateRect: RectPtr;
                    //                        tipProc: Ptr;
                    //                        theProc, variant,
                    //                        method: Integer): OSErr;
                    // IM:MMTb 3-100. Pop = 24. Help disabled →
                    // hmHelpDisabled.
                    0x0B01 => finish(bus, cpu, 24, HM_HELP_DISABLED),

                    // FUNCTION HMShowMenuBalloon(itemNum,
                    //                            itemMenuID: Integer;
                    //                            itemFlags,
                    //                            itemReserved: LongInt;
                    //                            tip: Point;
                    //                            alternateRect: RectPtr;
                    //                            tipProc: Ptr;
                    //                            theProc,
                    //                            variant: Integer): OSErr;
                    // IM:MMTb 3-103. Pop = 30. Help disabled →
                    // hmHelpDisabled.
                    0x0E05 => finish(bus, cpu, 30, HM_HELP_DISABLED),

                    // FUNCTION HMGetIndHelpMsg(whichType: ResType;
                    //                          whichResID, whichMsg,
                    //                          whichState: Integer;
                    //                          VAR options: LongInt;
                    //                          VAR tip: Point;
                    //                          VAR altRect: Rect;
                    //                          VAR theProc: Integer;
                    //                          VAR variant: Integer;
                    //                          VAR aHelpMsg:
                    //                          HMMessageRecord;
                    //                          VAR count: Integer): OSErr;
                    // IM:MMTb 3-128. Pop = 40. No help resources →
                    // resNotFound. Don't touch any VAR-out param —
                    // caller's records stay untouched per the IM
                    // contract that resNotFound means "did not
                    // populate anything".
                    0x1306 => finish(bus, cpu, 40, RES_NOT_FOUND),

                    // Unknown selector — pop just the 2-byte selector
                    // and leave the FUNCTION result slot untouched +
                    // D0 = noErr. A future System addition that
                    // assigns a new Pack14 routine should fill in a
                    // new arm above; the unknown-selector path is
                    // intentionally a permissive no-op so the caller
                    // can degrade gracefully.
                    _ => {
                        cpu.write_reg(Register::A7, sp + 2);
                        cpu.write_reg(Register::D0, 0);
                    }
                }
                Ok(())
            }

            // Pack15 ($A831) — Picture Utilities Package
            //
            // Seven-routine selector dispatcher providing image-
            // metadata extraction (color counts, palettes, font
            // names, comment IDs, source rectangles) from PICT and
            // PixMap structures.
            //
            // Selector encoding is `(arg_words << 8) | routine` per
            // IM:VI 18-18 selector summary — same Apple-Events Pack8
            // / Pack14 convention. arg_bytes = high_byte * 2; total
            // pop = 2 (selector) + arg_bytes.
            //
            // Pascal calling convention: caller pre-pushes a 2-byte
            // OSErr result in D0, then args left-to-right (first
            // source-listed arg deepest, last shallowest at SP+2).
            // Pack15's public MPW glue loads the selector into D0
            // (`MOVE.W #selector, D0`) and leaves only the Pascal
            // arguments on the stack. The trap pops args only and
            // returns the OSErr in D0; unlike Pack6 / Pack14, the
            // caller-visible stack does not carry a separate result
            // slot.
            //
            // HLE compromise: Systemless does not parse PICT opcodes
            // for metadata extraction (no quantization, no font-
            // name discovery, no comment-ID enumeration). The
            // routines still return the documented OSErr surface and
            // write defensive defaults to VAR-out parameters:
            // NewPictInfo writes a unique, nonzero PictInfoID to
            // *PictInfoID and registers it as live; RetrievePictInfo /
            // GetPictInfo / GetPixMapInfo zero-fill the 104-byte
            // PictInfo record (uniqueColors=0, depth=0, all
            // counts=0, NIL palette/colorTable/font/comment
            // handles, sourceRect=(0,0,0,0)) when the ID is live;
            // RecordPictInfo / RecordPixMapInfo reject stale IDs
            // with pictInfoIDErr (-11001); DisposPictInfo is
            // idempotent and returns noErr on repeated calls. Apps
            // that walk PictInfo for thumbnail / font discovery see
            // "this picture has no metadata" — which is technically
            // correct for our HLE since we don't model picture
            // introspection. Apps that defensively check OSErr
            // proceed cleanly.
            //
            // The previous heuristic at toolbox.rs:5957..5962
            // interpreted the high byte as BYTE count not WORD
            // count and clamped to (2..=48): RecordPictInfo $0403
            // (high $04 = 4 words = 8 byte args) popped 6 instead
            // of 10; GetPictInfo $0800 (high $08 = 8 words = 16
            // byte args) popped 10 instead of 18. Every selector
            // had wrong pop discipline. A real-game caller would
            // have left 4..8 garbage arg bytes on the stack and
            // crashed on the next RTS.
            //
            // Inside Macintosh Volume VI (1991), ch. 18, Picture
            // Utilities Package, pages 18-1..18-18 + selector
            // summary table at 18-18 (IM:VI 37669..37685).
            // Pack15 / Picture Utilities ($A831): Per-selector Pascal frames per IM:VI 18-18 and Imaging With QuickDraw 1994 pp. 7-53..7-59: selector in D0, no selector word on stack. $0206 DisposPictInfo pop 4 D0=0, $0403 RecordPictInfo pop 8 D0=0, $0404 RecordPixMapInfo pop 8 D0=0, $0505 RetrievePictInfo pop 10 *PictInfo zeroed (104 bytes) D0=0, $0602 NewPictInfo pop 12 *PictInfoID=unique nonzero ID D0=0, $0800 GetPictInfo pop 16 *PictInfo zeroed (104 bytes) D0=0, $0801 GetPixMapInfo pop 16 *PictInfo zeroed (104 bytes) D0=0.
            (true, 0x031) => {
                // Size of PictInfo record per IM:VI 18-5:
                // version(2) + uniqueColors(4) + thePalette(4) +
                // theColorTable(4) + hRes(4) + vRes(4) + depth(2) +
                // sourceRect(8) + 18 LongInts (textCount through
                // reserved2) = 104 bytes.
                const PICT_INFO_SIZE: u32 = 104;

                let sp = cpu.read_reg(Register::A7);
                let selector = cpu.read_reg(Register::D0) as u16;
                let pop_total = ((selector >> 8) as u32) * 2;

                // Helper: advance A7 by the packed-argument bytes and
                // write the OSErr result to D0 only. Pack15's caller
                // stack does not reserve a separate result slot.
                let finish = |cpu: &mut dyn CpuOps, pop_total: u32, result: i16| {
                    cpu.write_reg(Register::A7, sp + pop_total);
                    cpu.write_reg(Register::D0, result as i32 as u32);
                };

                // Helper: zero-fill a PictInfo record at the given
                // ptr. NIL ptr is a graceful no-op.
                let zero_pict_info = |bus: &mut MacMemoryBus, ptr: u32| {
                    if ptr == 0 {
                        return;
                    }
                    for off in 0..PICT_INFO_SIZE {
                        bus.write_byte(ptr + off, 0);
                    }
                };

                match selector {
                    // FUNCTION DisposPictInfo(thePictInfoID:
                    //                         PictInfoID): OSErr;
                    // IM:VI 18-14. Pop = 4. Stack: PictInfoID(4).
                    // The live-ID registry is pruned on the first
                    // dispose, but BasiliskII treats repeated calls
                    // as noErr no-ops.
                    0x0206 => {
                        let pict_info_id = bus.read_long(sp);
                        let _ = self.pict_info_ids.remove(&pict_info_id);
                        finish(cpu, 4, 0);
                    }

                    // FUNCTION RecordPictInfo(thePictInfoID: PictInfoID;
                    //                         thePictHandle: PicHandle): OSErr;
                    // IM:VI 18-12. Pop = 8. Stack: PicHandle(4 last)
                    // + PictInfoID(4 first).
                    // Invalid IDs return pictInfoIDErr.
                    0x0403 => {
                        let pict_info_id = bus.read_long(sp + 4);
                        let result = if self.pict_info_ids.contains(&pict_info_id) {
                            0
                        } else {
                            -11001
                        };
                        finish(cpu, 8, result);
                    }

                    // FUNCTION RecordPixMapInfo(thePictInfoID: PictInfoID;
                    //                           thePixMapHandle: PixMapHandle): OSErr;
                    // IM:VI 18-12. Pop = 8. Same shape as
                    // RecordPictInfo. Invalid IDs return pictInfoIDErr.
                    0x0404 => {
                        let pict_info_id = bus.read_long(sp + 4);
                        let result = if self.pict_info_ids.contains(&pict_info_id) {
                            0
                        } else {
                            -11001
                        };
                        finish(cpu, 8, result);
                    }

                    // FUNCTION RetrievePictInfo(thePictInfoID:
                    //                           PictInfoID;
                    //                           VAR thePictInfo: PictInfo;
                    //                           colorsRequested: Integer): OSErr;
                    // IM:VI 18-13. Pop = 10. Stack: colorsRequested(2
                    // last) + thePictInfo ptr(4) + PictInfoID(4
                    // first). Invalid IDs return pictInfoIDErr; live
                    // IDs zero-fill the PictInfo record.
                    0x0505 => {
                        let pict_info_id = bus.read_long(sp + 6);
                        if self.pict_info_ids.contains(&pict_info_id) {
                            let info_ptr = bus.read_long(sp + 2);
                            zero_pict_info(bus, info_ptr);
                            finish(cpu, 10, 0);
                        } else {
                            finish(cpu, 10, -11001);
                        }
                    }

                    // FUNCTION NewPictInfo(VAR thePictInfoID:
                    //                      PictInfoID; verb: Integer;
                    //                      colorsRequested: Integer;
                    //                      colorPickMethod: Integer;
                    //                      version: Integer): OSErr;
                    // IM:VI 18-11. Pop = 12. Stack: version(2 last)
                    // + colorPickMethod(2) + colorsRequested(2) +
                    // verb(2) + PictInfoID ptr(4 first). Mint a
                    // unique nonzero
                    // PictInfoID and write it to *PictInfoID.
                    0x0602 => {
                        let id_ptr = bus.read_long(sp + 8);
                        if id_ptr != 0 {
                            static PICT_INFO_ID_COUNTER: std::sync::atomic::AtomicU32 =
                                std::sync::atomic::AtomicU32::new(1);
                            let pict_info_id = PICT_INFO_ID_COUNTER
                                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
                            bus.write_long(id_ptr, pict_info_id);
                            self.pict_info_ids.insert(pict_info_id);
                        }
                        finish(cpu, 12, 0);
                    }

                    // FUNCTION GetPictInfo(thePictHandle: PicHandle;
                    //                      VAR thePictInfo: PictInfo;
                    //                      verb: Integer;
                    //                      colorsRequested: Integer;
                    //                      colorPickMethod: Integer;
                    //                      version: Integer): OSErr;
                    // IM:VI 18-9. Pop = 16. Stack: version(2 last)
                    // + colorPickMethod(2) + colorsRequested(2) +
                    // verb(2) + thePictInfo ptr(4) + PicHandle(4
                    // first). Zero-fill the 104-byte PictInfo record.
                    0x0800 => {
                        let info_ptr = bus.read_long(sp + 8);
                        zero_pict_info(bus, info_ptr);
                        finish(cpu, 16, 0);
                    }

                    // FUNCTION GetPixMapInfo(thePixMapHandle:
                    //                        PixMapHandle;
                    //                        VAR thePictInfo: PictInfo;
                    //                        verb: Integer;
                    //                        colorsRequested: Integer;
                    //                        colorPickMethod: Integer;
                    //                        version: Integer): OSErr;
                    // IM:VI 18-10. Pop = 16. Same shape as
                    // GetPictInfo with PixMapHandle in place of
                    // PicHandle. Zero-fill PictInfo record.
                    0x0801 => {
                        let info_ptr = bus.read_long(sp + 8);
                        zero_pict_info(bus, info_ptr);
                        finish(cpu, 16, 0);
                    }

                    // Unknown selector — pop the encoded argument
                    // bytes, leave the FUNCTION result slot
                    // untouched, and return noErr in D0. A future
                    // System addition with a new Pack15 routine
                    // should add a new arm above.
                    _ => {
                        cpu.write_reg(Register::A7, sp + pop_total);
                        cpu.write_reg(Register::D0, 0);
                    }
                }
                Ok(())
            }

            // ========================================================
            // System 7+ Dispatch Managers (no-op family — 10 traps)
            // ========================================================
            //
            // Ten selector-based dispatchers for System-7-and-later
            // subsystems whose underlying machinery Systemless does not
            // model: Component Manager (1991), Object Support Library,
            // Dictionary Manager, Text Services Manager, Docking
            // Manager, Mixed Mode Manager (PowerPC), Code Fragment
            // Manager (PowerPC), Icon Utilities, Thread Manager, and
            // Translation Manager. Each one routes a sub-routine call
            // selected by either a stack-pushed selector word
            // (Component Manager) or a routine number in D0
            // (everything else, Pack8/Pack14 convention).
            //
            // HLE compromise — load-bearing rationale:
            //   1. Apps written for System 7+ uniformly probe presence
            //      via the documented Gestalt selector before calling
            //      the dispatcher. The standard idiom is:
            //          if Gestalt('cfrg', &response) = noErr
            //          and BTst(response, gestaltCFMPresent)
            //          then ... call CodeFragmentManager routines ...
            //          else ... fall back to non-CFM path ...
            //      Systemless's Gestalt arm at src/trap/toolbox.rs:1500+
            //      returns gestaltUndefSelectorErr (-5551) for every
            //      selector listed below, so well-behaved apps see
            //      "absent" and skip the dispatcher entirely.
            //   2. Most of the cluster still follows the stub/no-op
            //      pattern: if a future corpus title bypasses
            //      Gestalt and calls one of those dispatchers
            //      directly, we can enumerate the selectors and
            //      promote that arm to Partial in the Pack14 style
            //      (see Help Manager $A830 at src/trap/toolbox.rs:5666
            //      for the canonical `(arg_words << 8) | routine`
            //      enumeration shape).
            //   3. ThreadDispatch is the exception: the public
            //      ThreadBeginCritical/ThreadEndCritical selectors
            //      are directly observable and are modelled below
            //      with a real critical-section nesting counter
            //      instead of a blanket no-op.
            //
            // Per-trap Gestalt selectors (see arms below for cites):
            //   $A82A ComponentDispatch       gestaltComponentMgr        'cpnt'
            //   $A9F8 MethodDispatch (OSL)    (no documented selector)
            //   $AA53 DictionaryDispatch      gestaltDictionaryMgrAttr   'dict'
            //   $AA54 TextServicesDispatch    gestaltTSMgrVersion        'tsmv'
            //   $AA57 DockingDispatch         (no documented selector)
            //   $AA59 MixedModeDispatch       (no documented selector — PPC bridge)
            //   $AA5A CodeFragmentDispatch    gestaltCFMAttr             'cfrg'
            //   $ABC9 IconDispatch            gestaltIconUtilitiesAttr   'icon'
            //   $ABF2 ThreadDispatch          gestaltThreadMgrAttr       'thds' (partial)
            //   $ABFC TranslationDispatch     gestaltTranslationMgrExists (response bit 0)
            //
            // Future work: pick the highest-impact
            // dispatcher (CodeFragmentDispatch $AA5A is hot for any
            // PowerPC fat binary) and enumerate its selectors with
            // proper pop discipline + per-selector defensive defaults
            // (NIL handles, resNotFound on resource-by-name lookups,
            // etc.) following the Pack14 pattern. Until then, the
            // remaining stubbed dispatchers keep the register-
            // preservation + stack-untouched contract.

            // ComponentDispatch ($A82A) — Component Manager
            // Inside Macintosh: More Macintosh Toolbox (1993),
            // pp. 6-6, 6-29, and 6-98:
            // `Gestalt('cpnt', ...)` gates availability; component
            // call glue uses `INLINE $2F3C, paramSize, callNum,
            // $7000, $A82A`, which pushes a 4-byte selector word
            // [paramSize:callNum] then traps.
            // Selector convention: stack-pushed 4-byte word at SP+0
            // (high word=paramSize bytes, low word=callNum). D0=0
            // means "call my component"; D0!=0 means "Component
            // Manager internal request" with selector in D0.
            // Gestalt: `gestaltComponentMgr = 'cpnt'` returns the CM
            // version (>=3 supports automatic version control,
            // unregister, icon families).
            //
            // HLE behaviour: D0=0 (noErr). For component calls
            // (D0 == 0), read `param_size = read_word(sp + 0)` from
            // the selector long and pop `4 + 4 + param_size` bytes
            // (selector + ComponentInstance + routine args) so the
            // inline glue stays balanced. Internal manager requests
            // (D0 != 0) still return noErr and leave the caller stack
            // untouched. All non-D0 registers are preserved.
            //
            // Regression coverage:
            //   src/trap/toolbox.rs::tests::componentdispatch_*
            // ComponentDispatch ($A82A): MMTB 1993 ch.6 17215. Stack-pushed [paramSize:callNum] selector at SP+0; D0=0 = call component, D0!=0 = CM internal. Gestalt 'cpnt'. HLE: D0=0, registers + stack preserved.
            (true, 0x02A) => {
                let d0 = cpu.read_reg(Register::D0);
                if d0 == 0 {
                    let sp = cpu.read_reg(Register::A7);
                    let param_size = u32::from(bus.read_word(sp));
                    cpu.write_reg(Register::A7, sp + 8 + param_size);
                }
                cpu.write_reg(Register::D0, 0);
                Ok(())
            }

            // MethodDispatch ($A9F8) — Object Support Library
            // Public MPW declaration:
            // `pascal OSErr MethodDispatch(short selector) = {0xA9F8};`
            // Inside Macintosh Volume VI (1991), Object Support
            // Library appendix. The selector arrives in D0 and the
            // selector-0 path is the observed public contract: return
            // noErr, preserve A7, and leave non-D0 registers alone.
            //
            // No documented Gestalt selector — apps that use OSL
            // typically check via NGetTrapAddress($A9F8) returning
            // _Unimplemented vs a real handler.
            //
            // HLE behaviour: D0=0 (noErr), all other registers
            // preserved, stack untouched. OSL is essentially dead
            // since no shipping app uses it directly (only via the
            // System Object Model wrappers).
            //
            // Regression coverage:
            //   methoddispatch_*
            // MethodDispatch (OSL) ($A9F8): IM:VI Object Support Library. D0 selector = method ID, args on stack. No Gestalt selector — apps probe via NGetTrapAddress. HLE: D0=0, registers + stack preserved.
            (true, 0x1F8) => return_noerr(cpu),

            // DictionaryDispatch ($AA53) — Dictionary Manager
            // Inside Macintosh: Text 1993, ch. 8
            // (Gestalt cite Text 1993 25656: "Use Gestalt with the
            // gestaltDictionaryMgrAttr environment selector to obtain
            // a result ... A result of gestaltDictionaryMgrPresent
            // (= 0) means that the Dictionary Manager is present.")
            // Selector convention: D0 = routine number per the
            // Pack8/Pack14 `(arg_words << 8) | routine` encoding.
            // Routines include InitializeDictionary, OpenDictionary,
            // CloseDictionary, FindRecordInDictionary, etc.
            // Gestalt: `gestaltDictionaryMgrAttr = 'dict'`,
            // `gestaltDictionaryMgrPresent = 0`.
            //
            // HLE behaviour: selector 0x0500 (InitializeDictionary)
            // writes noErr to the caller's function-result slot,
            // returns noErr in D0, and pops the 10-byte public call
            // frame (FSSpecPtr + maximumKeyLength + keyAttributes +
            // script); all other selectors preserve registers and
            // remain the no-op safety net. Apps that probe Gestalt
            // first see "absent" and skip the dispatcher entirely.
            //
            // Regression coverage:
            //   src/trap/toolbox.rs::tests::dictionarydispatch_*
            // DictionaryDispatch ($AA53): Text 1993 ch. 8 25656, 26640-26648. D0 selector per (arg_words<<8)|routine. Gestalt 'dict' → gestaltDictionaryMgrPresent=0. HLE: InitializeDictionary writes noErr to the function-result slot, pops 10 bytes, and returns noErr; the remaining selectors stay stack-untouched.
            (true, 0x253) => {
                let selector = cpu.read_reg(Register::D0) & 0xFFFF;
                let result = match selector {
                    0x0500 => {
                        let sp = cpu.read_reg(Register::A7);
                        bus.write_word(sp + 10, 0);
                        cpu.write_reg(Register::A7, sp + 10);
                        cpu.write_reg(Register::D0, 0);
                        Ok(())
                    }
                    _ => return_noerr(cpu),
                };
                result
            }

            // TextServicesDispatch ($AA54) — Text Services Manager
            // Inside Macintosh: Text 1993, ch. 8
            // (Gestalt cite Text 1993 22172: "Use the Gestalt
            // environmental selector gestaltTSMgrVersion to determine
            // whether the Text Services Manager is available.")
            // The TSM routes input-method (e.g. CJKV IME, character
            // palette) calls between an app and an installed text
            // service component. Selector convention: D0 = routine
            // number; selectors include InitTSMAwareApplication,
            // CloseTSMAwareApplication, NewTSMDocument, etc.
            // Gestalt: `gestaltTSMgrVersion = 'tsmv'` returns the
            // TSM version as a 32-bit value.
            //
            // HLE behaviour: D0=0 (noErr), all other registers
            // preserved, stack untouched. No current corpus title
            // (English-only games) needs TSM.
            //
            // Regression coverage:
            //   textservicesdispatch_*
            // TextServicesDispatch (TSM) ($AA54): Text 1993 ch.8 22172. D0 selector. Gestalt 'tsmv' → version word. HLE: D0=0, registers + stack preserved.
            (true, 0x254) => return_noerr(cpu),

            // DockingDispatch ($AA57) — Docking Manager (PowerBook)
            // Inside Macintosh Volume VI (PowerBook docking station
            // protocol). Specific to docking-station-aware PowerBooks
            // (Duo 2x0 series); routes dock/undock notifications and
            // power-state events. Selector convention: D0 = routine
            // number per the standard System 7 dispatcher pattern.
            // No documented Gestalt selector — apps probe via
            // NGetTrapAddress($AA57) returning _Unimplemented when
            // running on non-PowerBook hardware.
            //
            // HLE behaviour: D0=0 (noErr), all other registers
            // preserved, stack untouched. No current corpus title
            // is PowerBook-specific.
            //
            // Regression coverage:
            //   dockingdispatch_*
            // DockingDispatch ($AA57): IM:VI PowerBook docking. D0 selector. No Gestalt selector — probe via NGetTrapAddress. HLE: D0=0, registers + stack preserved.
            (true, 0x257) => {
                cpu.write_reg(Register::D0, 0);
                Ok(())
            }

            // MixedModeDispatch ($AA59) — Mixed Mode Manager
            // Inside Macintosh: PowerPC System Software 1994
            // (PPC SS 1994 line 2774: `_MixedModeMagic` is the
            // mixed-mode A-trap that bridges 68K → PowerPC and
            // PowerPC → 68K calls through Universal Procedure
            // Pointers). Selector convention: A0 = UniversalProcPtr
            // record; D0 = routine number for some MMM-internal
            // services (CallUniversalProc / NewRoutineDescriptor /
            // DisposeRoutineDescriptor).
            // No documented Gestalt selector — MMM presence is
            // implied by gestaltCFMAttr (CFM and MMM ship together).
            //
            // HLE behaviour: D0=0 (noErr), all other registers
            // preserved, stack untouched. Systemless is a 68K-only HLE,
            // so any MMM call (a PPC-side caller transitioning back
            // to 68K) is unreachable in practice.
            //
            // Regression coverage:
            //   src/trap/toolbox.rs::tests::mixedmodedispatch_returns_noerr_and_preserves_stack_pointer
            // MixedModeDispatch ($AA59): PPC SS 1994 ch.7 2774 (`_MixedModeMagic`). A0=UPP record, D0 selector. No Gestalt — implied by 'cfrg'. HLE: D0=0, registers + stack preserved (68K-only HLE, MMM unreachable).
            (true, 0x259) => return_noerr(cpu),

            // CodeFragmentDispatch ($AA5A) — Code Fragment Manager
            // Inside Macintosh: PowerPC System Software 1994
            // (PPC SS 1994 ch.6, Gestalt cite line 1770: "if you
            // need to know whether the Code Fragment Manager is
            // available, you can call the Gestalt function with the
            // selector gestaltCFMAttr"; constant cite line 4736:
            // `#define gestaltCFMAttr 'cfrg'`).
            // The CFM resolves and connects PowerPC code fragments
            // ('cfrg' resources) — the loader for PowerPC native
            // executables and shared libraries (PEF format).
            // Selector convention: D0 = routine number; routines
            // include GetSharedLibrary, GetDiskFragment, FindSymbol,
            // CountSymbols, GetIndSymbol, CloseConnection, etc.
            // Gestalt: `gestaltCFMAttr = 'cfrg'`,
            // `gestaltCFMPresent = 0` (response bit 0).
            //
            // HLE behaviour: D0=0 (noErr), all other registers
            // preserved, stack untouched. Systemless is a 68K-only HLE
            // — apps that probe Gestalt see 'cfrg' undefined and
            // either fall back to the 68K code path or refuse to
            // launch. PPC fat binaries with 68K-fork still execute
            // because the loader picks the 68K fork when CFM is
            // absent.
            //
            // Regression coverage:
            //   src/trap/toolbox.rs::tests::codefragmentdispatch_*
            // CodeFragmentDispatch (CFM) ($AA5A): PPC SS 1994 ch.6 1770. D0 selector. Gestalt 'cfrg' → gestaltCFMPresent=0. HLE: D0=0, registers + stack preserved (68K-only — fat binaries fall back to 68K fork).
            (true, 0x25A) => return_noerr(cpu),

            // IconDispatch ($ABC9) — Icon Utilities
            // Inside Macintosh: More Macintosh Toolbox (1993),
            // pp. 5-18 and 5-71:
            // Icon Utilities availability is gated by
            // `gestaltIconUtilitiesAttr`, and the routines are invoked
            // through `_IconDispatch` with routine selectors.
            // Selector convention: D0 = routine number; routines
            // include PlotIconID, NewIconSuite, AddIconToSuite,
            // GetIconFromSuite, ForEachIconDo, GetIconCacheData,
            // SetIconCacheData, IconIDToRgn, IconSuiteToRgn,
            // IconMethodToRgn, etc. Note that GetIcon ($A9BB),
            // PlotIcon ($A94B), GetCIcon ($AA1E), PlotCIcon ($AA1F),
            // and DisposeCIcon ($AA25) are SEPARATE legacy traps
            // available in System 6 and System 7 — those are NOT
            // routed through IconDispatch.
            // Gestalt: `gestaltIconUtilitiesAttr = 'icon'`.
            //
            // HLE behaviour: D0=0 (noErr), all other registers
            // preserved, and the dispatcher advances A7 by 8 bytes
            // on return. Apps that probe Gestalt see "absent" and
            // fall back to the legacy trap surface for monochrome /
            // color icons (which Systemless implements via $A9BB
            // GetIcon / $AA1E GetCIcon / etc.).
            //
            // Regression coverage:
            //   src/trap/toolbox.rs::tests::icondispatch_*
            // IconDispatch ($ABC9): MMTB 1993 ch.5 14879+15191. D0 selector. Gestalt 'icon' → gestaltIconUtilitiesPresent. HLE: selector $0000 returns noErr and pops the inline selector frame; unsupported selectors return paramErr (-50) and still pop the same frame. Apps fall back to legacy $A9BB/$AA1F monochrome/color icon traps.
            (true, 0x3C9) => {
                let selector = cpu.read_reg(Register::D0) & 0xFFFF;
                match selector {
                    0 => return_noerr_and_pop(cpu, 8),
                    _ => return_error_and_pop(cpu, 8, -50),
                }
            }

            // ThreadDispatch ($ABF2) — Thread Manager critical sections
            // Inside Macintosh: Operating System Utilities 1994,
            // Thread Manager chapter (pp. 69-70):
            //   pascal OSErr ThreadBeginCritical(void);
            //   pascal OSErr ThreadEndCritical(void);
            // The public MPW glue dispatches those no-arg routines
            // through `_ThreadDispatch` selectors $000B and $000C.
            // MPW uses `#pragma parameter __D0` for the selector
            // thunk, so the selector arrives in D0 and the stack is
            // untouched.
            //
            // HLE behaviour: selector $000B increments the dispatcher-
            // wide critical-section nesting counter and returns noErr.
            // Selector $000C decrements the counter when nonzero and
            // returns noErr; when the counter is already zero it
            // returns threadProtocolErr (-619). Unsupported selectors
            // return paramErr (-50). The public `Threads.h` wrappers
            // read the 16-bit OSErr from the caller's zero-arg result
            // slot at SP, so we mirror the low word there as well as
            // in D0.
            //
            // Regression coverage:
            //   src/trap/toolbox.rs::tests::threaddispatch_begin_and_end_critical_roundtrip
            //   src/trap/toolbox.rs::tests::threaddispatch_endcritical_underflow_returns_thread_protocol_err
            //   src/trap/toolbox.rs::tests::threaddispatch_unsupported_selector_returns_param_err
            (true, 0x3F2) => {
                let selector = cpu.read_reg(Register::D0) & 0xFFFF;
                let sp = cpu.read_reg(Register::A7);
                let result = match selector {
                    0x000B => {
                        self.thread_critical_nesting =
                            self.thread_critical_nesting.saturating_add(1);
                        0
                    }
                    0x000C if self.thread_critical_nesting == 0 => -619,
                    0x000C => {
                        self.thread_critical_nesting -= 1;
                        0
                    }
                    _ => -50,
                };
                bus.write_word(sp, result as u16);
                if result == 0 {
                    return_noerr(cpu)
                } else {
                    return_error_and_pop(cpu, 0, result)
                }
            }

            // TranslationDispatch ($ABFC) — Translation Manager
            // Inside Macintosh: More Macintosh Toolbox (1993),
            // pp. 7-12 and 7-66:
            // `Gestalt('xlat', ...)` with response bit
            // `gestaltTranslationMgrExists` gates availability; the
            // manager's routines are invoked through
            // `_TranslationDispatch` routine selectors.
            // The Translation Manager (Mac Easy Open / Macintosh
            // Easy Open) routes file-format translation requests
            // — e.g. MS Word .doc → Mac Word .mcw, JPEG → PICT.
            // Selector convention: D0 = routine number per the
            // selector summary table at MMTB 20828ff. Routines
            // include GetTranslationExtensions, IdentifyFile,
            // TranslateFile, TranslateContents, NewScriptingFile.
            // Gestalt: `gestaltTranslationAttr` (response bit 0
            // `gestaltTranslationMgrExists`).
            //
            // HLE behaviour: the dispatcher returns a non-zero error
            // and advances A7 by 4 bytes. Apps that probe Gestalt
            // see "absent" and either ask the user to convert the
            // file manually or refuse to open foreign formats.
            //
            // Regression coverage:
            //   src/trap/toolbox.rs::tests::translationdispatch_*
            // TranslationDispatch ($ABFC): MMTB 1993 ch.21 20111+20828. D0 selector per MMTB 20828 selector summary. Gestalt → gestaltTranslationMgrExists=0. HLE: D0=0, registers + stack preserved (apps refuse foreign-format opens).
            (true, 0x3FC) => return_error_and_pop(cpu, 4, -50),

            _ => return None,
        })
    }

    /// Shared implementation of UniqueID ($A9C1) and Unique1ID ($A810).
    ///
    /// Pascal signature is identical for both:
    ///   FUNCTION (theType: ResType): INTEGER;
    /// Stack on entry:
    ///   SP       theType (ResType,   4 bytes)
    ///   SP + 4   result  (INTEGER,   2 bytes — caller-allocated)
    /// Pops 4 bytes of args, leaves the 2-byte result slot.
    ///
    /// `current_only = false` ($A9C1) scans every open resource file.
    /// `current_only = true`  ($A810) restricts the used-ID set to the
    /// current resource file only, per IM:IV-16.
    ///
    /// Always starts the candidate scan at 128 to sidestep the IM:I-121
    /// caller-warning about IDs in the system-reserved range 0..127.
    fn handle_unique_id<C: CpuOps>(
        &mut self,
        bus: &mut MacMemoryBus,
        cpu: &mut C,
        current_only: bool,
    ) -> Result<()> {
        let sp = cpu.read_reg(Register::A7);
        let raw_res_type = bus.read_long(sp).to_be_bytes();
        let res_type = super::TrapDispatcher::normalize_ostype(raw_res_type);

        let mut used_ids = std::collections::HashSet::new();
        if let Some(ref resources) = self.resources {
            let mut chain: Vec<u16> = if current_only {
                let refnum = self.current_resource_refnum();
                if resources.files.contains_key(&refnum) {
                    vec![refnum]
                } else {
                    Vec::new()
                }
            } else {
                resources.files.keys().copied().collect()
            };
            chain.sort_unstable();
            for refnum in chain {
                if let Some(file) = resources.files.get(&refnum) {
                    for (t, id) in file.loaded.keys() {
                        if *t == res_type {
                            used_ids.insert(*id);
                        }
                    }
                }
            }
        }

        let mut candidate: i16 = 128;
        while used_ids.contains(&candidate) {
            candidate = candidate.wrapping_add(1);
            if candidate <= 0 {
                candidate = 128;
                break;
            }
        }

        bus.write_word(sp + 4, candidate as u16);
        cpu.write_reg(Register::A7, sp + 4);
        Ok(())
    }

    /// Shared implementation of GetIndType ($A99F) and Get1IndType ($A80F).
    ///
    /// Pascal signature is identical for both:
    ///   PROCEDURE (VAR theType: ResType; index: INTEGER);
    /// Pascal arg push order is left-to-right, so theType (the VAR ptr)
    /// is pushed first (deeper) and index is pushed last (shallower).
    /// Stack on entry:
    ///   SP       index   (INTEGER,    2 bytes)
    ///   SP + 2   typePtr (ResType*,   4 bytes)
    /// Pops 6 bytes of args, no result slot (PROCEDURE).
    ///
    /// `current_only = false` ($A99F) walks `resource_search_order()` —
    /// the current resource file plus every file opened before it.
    /// `current_only = true`  ($A80F) restricts the walk to the current
    /// resource file only, per IM:IV-15.
    ///
    /// Uses BTreeSet so the index→type mapping is deterministic for a
    /// given resource map; HashSet would be undefined-order and would
    /// make tests flaky across rebuilds.
    fn handle_get_ind_type<C: CpuOps>(
        &mut self,
        bus: &mut MacMemoryBus,
        cpu: &mut C,
        current_only: bool,
    ) -> Result<()> {
        let sp = cpu.read_reg(Register::A7);
        let index = bus.read_word(sp) as i16;
        let type_ptr = bus.read_long(sp + 2);

        let result_type = if let Some(ref resources) = self.resources {
            let chain: Vec<u16> = if current_only {
                let refnum = self.current_resource_refnum();
                if resources.files.contains_key(&refnum) {
                    vec![refnum]
                } else {
                    Vec::new()
                }
            } else {
                self.resource_search_order()
            };
            let mut types = std::collections::BTreeSet::new();
            for refnum in chain {
                if let Some(file) = resources.files.get(&refnum) {
                    for (res_type, _) in file.loaded.keys() {
                        types.insert(*res_type);
                    }
                }
            }
            if index >= 1 && (index as usize) <= types.len() {
                types.into_iter().nth((index - 1) as usize)
            } else {
                None
            }
        } else {
            None
        };

        if type_ptr != 0 {
            match result_type {
                Some(t) => bus.write_long(type_ptr, u32::from_be_bytes(t)),
                None => bus.write_long(type_ptr, 0),
            }
        }
        cpu.write_reg(Register::A7, sp + 6);
        Ok(())
    }

    /// Shared implementation of GetIndResource ($A99D) and Get1IndResource ($A80E).
    ///
    /// Pascal signature is identical for both:
    ///   FUNCTION (theType: ResType; index: INTEGER): Handle;
    /// Stack on entry (16-bit aligned):
    ///   SP       index   (INTEGER, 2 bytes)
    ///   SP + 2   theType (ResType,  4 bytes)
    ///   SP + 6   result  (Handle,   4 bytes — caller-allocated)
    /// Pops 6 bytes of args, leaves the 4-byte result slot.
    ///
    /// `current_only = false` ($A99D) walks `resource_search_order()` —
    /// the current resource file plus every file opened before it.
    /// `current_only = true`  ($A80E) restricts the walk to the
    /// current resource file only, per IM:IV-15.
    fn handle_get_ind_resource<C: CpuOps>(
        &mut self,
        bus: &mut MacMemoryBus,
        cpu: &mut C,
        current_only: bool,
    ) -> Result<()> {
        let trap_name = if current_only {
            "Get1IndResource"
        } else {
            "GetIndResource"
        };
        let sp = cpu.read_reg(Register::A7);
        let index = bus.read_word(sp) as i16;
        let raw_res_type = bus.read_long(sp + 2).to_be_bytes();
        let res_type = super::TrapDispatcher::normalize_ostype(raw_res_type);
        let type_str = String::from_utf8_lossy(&res_type);

        if trace_sound_enabled()
            && self
                .resources
                .as_ref()
                .is_some_and(|resources| resources.files.len() > 1)
        {
            eprintln!(
                "[RSRC] {} raw='{}' norm='{}' index={} current={} current_only={}",
                trap_name,
                String::from_utf8_lossy(&raw_res_type),
                type_str,
                index,
                self.current_resource_refnum(),
                current_only,
            );
        }

        // Build the candidate (id, refnum, ptr) list per IM:I I-116 / IV-15.
        // For Get1IndResource ($A80E) the chain collapses to a single
        // entry — the current resource file — so a multi-file scenario
        // never leaks resources from inactive files into the index.
        let candidates: Option<Vec<(i16, u16, u32)>> = self.resources.as_ref().map(|resources| {
            let chain: Vec<u16> = if current_only {
                let refnum = self.current_resource_refnum();
                if resources.files.contains_key(&refnum) {
                    vec![refnum]
                } else {
                    Vec::new()
                }
            } else {
                self.resource_search_order()
            };

            let mut out: Vec<(i16, u16, u32)> = chain
                .into_iter()
                .filter_map(|refnum| resources.files.get(&refnum).map(|file| (refnum, file)))
                .flat_map(|(refnum, file)| {
                    file.loaded
                        .iter()
                        .filter(|((t, _), _)| *t == res_type)
                        .map(move |((_, id), ptr)| (*id, refnum, *ptr))
                        .collect::<Vec<_>>()
                })
                .collect();
            // Stable, deterministic order: by resource id ascending. Real
            // ROM iterates the resource map in map order, but Systemless's
            // HashMap-backed loaded table has no map order to speak of —
            // sorting by id is the documented Get*IndResource contract
            // ("returns handles to all resources of the given type", IM:IV-15)
            // and matches the in-file behaviour of the existing
            // GetIndResource arm prior to this split.
            out.sort_by_key(|&(id, _, _)| id);
            out
        });

        if let Some(candidates) = candidates {
            if index >= 1 && (index as usize) <= candidates.len() {
                let (res_id, _refnum, ptr) = candidates[(index - 1) as usize];
                let handle = self.get_or_create_resource_handle(bus, res_type, res_id, ptr);
                eprintln!(
                    "[TRAP] {}('{}', {}) -> id={} handle=${:08X}",
                    trap_name, type_str, index, res_id, handle
                );
                cpu.write_reg(Register::A0, handle);
                cpu.write_reg(Register::D0, 0);
                bus.write_word(0x0A60, 0); // ResErr = noErr per IM:I I-118
                bus.write_long(sp + 6, handle);
                cpu.write_reg(Register::A7, sp + 6);
                return Ok(());
            }
        }

        eprintln!("[TRAP] {}('{}', {}) -> NULL", trap_name, type_str, index);
        cpu.write_reg(Register::A0, 0);
        cpu.write_reg(Register::D0, -192i32 as u32);
        bus.write_word(0x0A60, (-192i16) as u16); // ResErr = resNotFound per IM:IV-15
        Self::write_point_words(bus, sp + 6, Self::list_no_click_cell());
        cpu.write_reg(Register::A7, sp + 6);
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::super::dispatch::QueuedEvent;
    use super::super::test_helpers::{setup, setup_with_port, MockCpu, TEST_SP};
    use super::{
        quicktime_movie_metadata, AE_ERR_DESC_NOT_FOUND, AE_ERR_NOT_AN_OBJECT_SPEC,
        AE_KEY_EVENT_CLASS_ATTR, AE_KEY_EVENT_ID_ATTR, AE_TYPE_APPLE_EVENT, AE_TYPE_NULL,
        AE_TYPE_TYPE, AE_TYPE_WILDCARD,
    };
    use crate::cpu::{CpuOps, Register};
    use crate::memory::globals::addr;
    use crate::memory::MacMemoryBus;
    use crate::memory::MemoryBus;
    use crate::trap::dispatch::{LoadedResources, ResourceFileMap, TrapDispatcher};
    use crate::trap::extended80::Extended80;
    use std::collections::HashMap;
    use std::ffi::OsString;
    use std::sync::{Mutex, MutexGuard, OnceLock};

    static STANDARD_FILE_ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();

    struct StandardFileEnvGuard {
        previous: Option<OsString>,
        _lock: MutexGuard<'static, ()>,
    }

    fn write_test_fsspec(
        bus: &mut MacMemoryBus,
        spec_ptr: u32,
        vref: i16,
        dir_id: u32,
        name: &[u8],
    ) {
        bus.write_word(spec_ptr, vref as u16);
        bus.write_long(spec_ptr + 2, dir_id);
        let len = name.len().min(63) as u8;
        bus.write_byte(spec_ptr + 6, len);
        bus.write_bytes(spec_ptr + 7, &name[..usize::from(len)]);
    }

    impl Drop for StandardFileEnvGuard {
        fn drop(&mut self) {
            if let Some(previous) = self.previous.take() {
                // SAFETY: The guard holds the module-local environment
                // mutex while mutating this process-wide test variable.
                unsafe { std::env::set_var("SYSTEMLESS_STANDARD_GET_FILE", previous) };
            } else {
                // SAFETY: The guard holds the module-local environment
                // mutex while mutating this process-wide test variable.
                unsafe { std::env::remove_var("SYSTEMLESS_STANDARD_GET_FILE") };
            }
        }
    }

    fn set_standard_file_env(value: &str) -> StandardFileEnvGuard {
        let lock = STANDARD_FILE_ENV_LOCK.get_or_init(|| Mutex::new(()));
        let guard = lock.lock().unwrap();
        let previous = std::env::var_os("SYSTEMLESS_STANDARD_GET_FILE");
        // SAFETY: The guard holds the module-local environment mutex while
        // mutating this process-wide test variable.
        unsafe { std::env::set_var("SYSTEMLESS_STANDARD_GET_FILE", value) };
        StandardFileEnvGuard {
            previous,
            _lock: guard,
        }
    }

    fn clear_standard_file_env() -> StandardFileEnvGuard {
        let lock = STANDARD_FILE_ENV_LOCK.get_or_init(|| Mutex::new(()));
        let guard = lock.lock().unwrap();
        let previous = std::env::var_os("SYSTEMLESS_STANDARD_GET_FILE");
        // SAFETY: The guard holds the module-local environment mutex while
        // mutating this process-wide test variable.
        unsafe { std::env::remove_var("SYSTEMLESS_STANDARD_GET_FILE") };
        StandardFileEnvGuard {
            previous,
            _lock: guard,
        }
    }

    fn read_screen_pixel_1bpp(
        bus: &crate::memory::MacMemoryBus,
        screen_base: u32,
        row_bytes: u32,
        x: i16,
        y: i16,
    ) -> bool {
        let byte_offset = (y as u32) * row_bytes + (x as u32 / 8);
        let bit = 7 - (x as u32 % 8);
        let addr = screen_base + byte_offset;
        (bus.read_byte(addr) & (1 << bit)) != 0
    }

    fn write_pascal_string(bus: &mut crate::memory::MacMemoryBus, addr: u32, text: &str) {
        let bytes = text.as_bytes();
        bus.write_byte(addr, bytes.len() as u8);
        for (idx, byte) in bytes.iter().enumerate() {
            bus.write_byte(addr + 1 + idx as u32, *byte);
        }
    }

    #[test]
    fn methoddispatch_returns_noerr_and_preserves_stack_pointer() {
        // Inside Macintosh Volume VI (OSL appendix):
        // MethodDispatch is the no-op dispatcher on selector 0.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);
        cpu.write_reg(Register::D0, 0x1234_5678);
        cpu.write_reg(Register::D1, 0x89AB_CDEF);

        let result = disp.dispatch_toolbox(true, 0x1F8, &mut cpu, &mut bus);
        assert!(result.is_some(), "MethodDispatch should be handled");
        assert!(result.unwrap().is_ok(), "MethodDispatch should succeed");
        assert_eq!(
            cpu.read_reg(Register::D0),
            0,
            "MethodDispatch should return noErr"
        );
        assert_eq!(
            cpu.read_reg(Register::D1),
            0x89AB_CDEF,
            "MethodDispatch should preserve non-D0 registers"
        );
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_before,
            "MethodDispatch should preserve the caller stack pointer"
        );
    }

    #[test]
    fn codefragmentdispatch_returns_noerr_and_preserves_stack_pointer() {
        // Inside Macintosh: PowerPC System Software 1994, Code Fragment
        // Manager chapter. The selector-0 direct call is the safety-net path.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);
        cpu.write_reg(Register::D0, 0x1234_5678);

        let result = disp.dispatch_toolbox(true, 0x25A, &mut cpu, &mut bus);
        assert!(result.is_some(), "CodeFragmentDispatch should be handled");
        assert!(
            result.unwrap().is_ok(),
            "CodeFragmentDispatch should succeed"
        );
        assert_eq!(
            cpu.read_reg(Register::D0),
            0,
            "CodeFragmentDispatch should return noErr"
        );
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_before,
            "CodeFragmentDispatch should preserve the caller stack pointer"
        );
    }

    #[test]
    fn codefragmentdispatch_preserves_non_d0_registers_and_stack_pointer() {
        let (mut disp, mut cpu, mut bus) = setup();
        cpu.write_reg(Register::D1, 0x2222_3333);
        cpu.write_reg(Register::A0, 0x4444_5555);
        cpu.write_reg(Register::A1, 0x6666_7777);
        let sp_before = cpu.read_reg(Register::A7);

        cpu.write_reg(Register::D0, 0x0000_0000);
        let result = disp.dispatch_toolbox(true, 0x25A, &mut cpu, &mut bus);
        assert!(result.is_some(), "CodeFragmentDispatch should be handled");
        assert!(
            result.unwrap().is_ok(),
            "CodeFragmentDispatch should return"
        );
        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert_eq!(cpu.read_reg(Register::D1), 0x2222_3333);
        assert_eq!(cpu.read_reg(Register::A0), 0x4444_5555);
        assert_eq!(cpu.read_reg(Register::A1), 0x6666_7777);
        assert_eq!(cpu.read_reg(Register::A7), sp_before);
    }

    #[test]
    fn mixedmodedispatch_returns_noerr_and_preserves_stack_pointer() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);
        cpu.write_reg(Register::D0, 0x1234_5678);
        cpu.write_reg(Register::D1, 0x2222_3333);
        cpu.write_reg(Register::A0, 0x4444_5555);
        cpu.write_reg(Register::A1, 0x6666_7777);

        let result = disp.dispatch_toolbox(true, 0x259, &mut cpu, &mut bus);
        assert!(result.is_some(), "MixedModeDispatch should be handled");
        assert!(result.unwrap().is_ok(), "MixedModeDispatch should succeed");
        assert_eq!(
            cpu.read_reg(Register::D0),
            0,
            "MixedModeDispatch should return noErr"
        );
        assert_eq!(cpu.read_reg(Register::D1), 0x2222_3333);
        assert_eq!(cpu.read_reg(Register::A0), 0x4444_5555);
        assert_eq!(cpu.read_reg(Register::A1), 0x6666_7777);
        assert_eq!(cpu.read_reg(Register::A7), sp_before);
    }

    // ========== QuickDraw Text Traps (Toolbox) ==========

    #[test]
    fn spaceextra_sets_current_port_spextra_field() {
        // Inside Macintosh Volume I (1985), p. I-171:
        // SpaceExtra sets the current GrafPort's spExtra field.
        let (mut disp, mut cpu, mut bus) = setup();
        let port = bus.alloc(128);
        disp.current_port = port;
        let extra = 0x0001_8000u32; // 1.5 Fixed
        bus.write_long(port + 76, 0xDEAD_BEEF);
        bus.write_long(TEST_SP, extra);

        let result = disp.dispatch_toolbox(true, 0x08E, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_long(port + 76), extra);
    }

    #[test]
    fn spaceextra_consumes_fixed_argument_and_pops_four_bytes() {
        // Inside Macintosh Volume I (1985), p. I-171:
        // SpaceExtra(extra: Fixed) consumes one 4-byte Fixed argument.
        let (mut disp, mut cpu, mut bus) = setup();
        let port = bus.alloc(128);
        disp.current_port = port;
        let sp_before = cpu.read_reg(Register::A7);
        bus.write_long(sp_before, 0xFFFF_8000); // -0.5 Fixed

        let result = disp.dispatch_toolbox(true, 0x08E, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp_before + 4);
    }

    #[test]
    fn piccomment_consumes_eight_byte_argument_frame() {
        // Inside Macintosh Volume I (1985), p. I-190:
        // PicComment(kind, dataSize, dataHandle) consumes an 8-byte frame.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);
        bus.write_word(sp_before, 0x1234);
        bus.write_word(sp_before + 2, 0x0000);
        bus.write_long(sp_before + 4, 0);

        let result = disp.dispatch_toolbox(true, 0x0F2, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp_before + 8);
    }

    #[test]
    fn stuffhex_decodes_hex_pairs_into_destination_bytes() {
        // Inside Macintosh Volume I (1985), p. I-195:
        // StuffHex stores bits expressed as hexadecimal digits into a target structure.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let src = bus.alloc(32);
        let dst = bus.alloc(16);
        let hex_bytes = *b"0102040810204080";
        let expected = [0x01u8, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80];

        bus.write_byte(src, hex_bytes.len() as u8);
        for (i, byte) in hex_bytes.iter().enumerate() {
            bus.write_byte(src + 1 + i as u32, *byte);
        }
        for i in 0..8u32 {
            bus.write_byte(dst + i, 0xCC);
        }
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, src);
        bus.write_long(sp + 4, dst);

        let result = disp.dispatch_toolbox(true, 0x066, &mut cpu, &mut bus);
        assert!(result.is_some(), "StuffHex should be handled");
        assert!(result.unwrap().is_ok(), "StuffHex should return");
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
        for (i, byte) in expected.iter().enumerate() {
            assert_eq!(bus.read_byte(dst + i as u32), *byte);
        }
    }

    #[test]
    fn stuffhex_consumes_thingptr_and_str255_arguments() {
        // Inside Macintosh Volume I (1985), p. I-195:
        // PROCEDURE StuffHex(thingPtr: Ptr; s: Str255) takes two pointer arguments.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0);
        bus.write_long(sp + 4, 0);
        bus.write_word(sp + 8, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x066, &mut cpu, &mut bus);
        assert!(result.is_some(), "StuffHex should be handled");
        assert!(result.unwrap().is_ok(), "StuffHex should return");
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
        assert_eq!(bus.read_word(sp + 8), 0xBEEF);
    }

    #[test]
    fn ipclistports_zero_request_works_before_ppcinit() {
        // Inside Macintosh: Interapplication Communication (1993),
        // pp. 7-39, 7-41 to 7-42, 7-57.
        let (mut disp, mut cpu, mut bus) = setup();
        let pre_pb = bus.alloc(64);
        let post_pb = bus.alloc(64);
        let port_name = bus.alloc(64);
        let location_name = bus.alloc(64);
        let buffer = bus.alloc(64);
        const IO_RESULT: u32 = 16;
        const START_INDEX: u32 = 40;
        const REQUEST_COUNT: u32 = 42;
        const ACTUAL_COUNT: u32 = 44;
        const PORT_NAME: u32 = 46;
        const LOCATION_NAME: u32 = 50;
        const BUFFER_PTR: u32 = 54;

        bus.write_bytes(port_name, &[0; 64]);
        bus.write_bytes(location_name, &[0; 64]);
        bus.write_bytes(buffer, &[0; 64]);

        bus.write_word(pre_pb + START_INDEX, 0);
        bus.write_word(pre_pb + REQUEST_COUNT, 0);
        bus.write_word(pre_pb + ACTUAL_COUNT, 0x1357);
        bus.write_word(pre_pb + IO_RESULT, 0x2468);
        bus.write_long(pre_pb + PORT_NAME, port_name);
        bus.write_long(pre_pb + LOCATION_NAME, location_name);
        bus.write_long(pre_pb + BUFFER_PTR, buffer);

        cpu.write_reg(Register::A0, pre_pb);
        cpu.write_reg(Register::D0, 0x000A);
        let pre = disp.dispatch_toolbox(false, 0x0DD, &mut cpu, &mut bus);
        assert!(pre.is_some(), "IPCListPorts should be handled");
        assert!(pre.unwrap().is_ok(), "IPCListPorts should return");
        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert_eq!(bus.read_word(pre_pb + IO_RESULT), 0);
        assert_eq!(bus.read_word(pre_pb + ACTUAL_COUNT), 0);
        assert!(!disp.ppc_initialized);

        cpu.write_reg(Register::A0, 0);
        cpu.write_reg(Register::D0, 0);
        let init = disp.dispatch_toolbox(false, 0x0DD, &mut cpu, &mut bus);
        assert!(init.is_some(), "PPCInit should be handled");
        assert!(init.unwrap().is_ok(), "PPCInit should return");
        assert!(disp.ppc_initialized);
        assert_eq!(cpu.read_reg(Register::D0), 0);

        bus.write_word(post_pb + START_INDEX, 0);
        bus.write_word(post_pb + REQUEST_COUNT, 0);
        bus.write_word(post_pb + ACTUAL_COUNT, 0x1357);
        bus.write_word(post_pb + IO_RESULT, 0x2468);
        bus.write_long(post_pb + PORT_NAME, port_name);
        bus.write_long(post_pb + LOCATION_NAME, location_name);
        bus.write_long(post_pb + BUFFER_PTR, buffer);

        cpu.write_reg(Register::A0, post_pb);
        cpu.write_reg(Register::D0, 0x000A);
        let post = disp.dispatch_toolbox(false, 0x0DD, &mut cpu, &mut bus);
        assert!(
            post.is_some(),
            "IPCListPorts should be handled after PPCInit"
        );
        assert!(
            post.unwrap().is_ok(),
            "IPCListPorts should return after PPCInit"
        );
        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert_eq!(bus.read_word(post_pb + IO_RESULT), 0);
        assert_eq!(bus.read_word(post_pb + ACTUAL_COUNT), 0);
    }

    // Pack15 ($A831) — Picture Utilities
    #[test]
    fn pack15_newpictinfo_mints_distinct_nonzero_ids_and_dispospictinfo_returns_noerr() {
        // Inside Macintosh Volume VI (1991), pp. 18-11 and 18-14:
        // Pack15 uses a selector in D0; the stack carries only the
        // Pascal arguments and the caller's function result slot.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let slot1 = bus.alloc(12);
        let slot2 = bus.alloc(12);
        let id1;
        let id2;
        let newpictinfo_first_ok;
        let newpictinfo_second_ok;
        let dispospictinfo_ok;
        let dispospictinfo_double_noerr_ok;

        bus.write_long(slot1, 0x1111_1111);
        bus.write_long(slot1 + 4, 0xDEAD_BEEF);
        bus.write_long(slot1 + 8, 0x2222_2222);
        bus.write_long(slot2, 0x3333_3333);
        bus.write_long(slot2 + 4, 0xFEED_FACE);
        bus.write_long(slot2 + 8, 0x4444_4444);

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x0602);
        bus.write_word(sp, 0);
        bus.write_word(sp + 2, 0);
        bus.write_word(sp + 4, 0);
        bus.write_word(sp + 6, 0);
        bus.write_long(sp + 8, slot1 + 4);
        bus.write_word(sp + 12, 0xBEEF);

        let first = disp.dispatch_toolbox(true, 0x031, &mut cpu, &mut bus);
        assert!(first.is_some(), "NewPictInfo should be handled");
        assert!(first.unwrap().is_ok(), "NewPictInfo should return");
        id1 = bus.read_long(slot1 + 4);
        newpictinfo_first_ok = (id1 != 0)
            && (bus.read_long(slot1) == 0x1111_1111)
            && (bus.read_long(slot1 + 8) == 0x2222_2222)
            && (cpu.read_reg(Register::A7) == sp + 12)
            && (bus.read_word(sp + 12) == 0xBEEF);

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x0602);
        bus.write_word(sp, 0);
        bus.write_word(sp + 2, 0);
        bus.write_word(sp + 4, 0);
        bus.write_word(sp + 6, 0);
        bus.write_long(sp + 8, slot2 + 4);
        bus.write_word(sp + 12, 0xCAFE);

        let second = disp.dispatch_toolbox(true, 0x031, &mut cpu, &mut bus);
        assert!(second.is_some(), "NewPictInfo should be handled");
        assert!(second.unwrap().is_ok(), "NewPictInfo should return");
        id2 = bus.read_long(slot2 + 4);
        newpictinfo_second_ok = (id2 != 0)
            && (id2 != id1)
            && (bus.read_long(slot2) == 0x3333_3333)
            && (bus.read_long(slot2 + 8) == 0x4444_4444)
            && (cpu.read_reg(Register::A7) == sp + 12)
            && (bus.read_word(sp + 12) == 0xCAFE);

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x0206);
        bus.write_long(sp, id1);
        bus.write_word(sp + 4, 0xFACE);

        let third = disp.dispatch_toolbox(true, 0x031, &mut cpu, &mut bus);
        assert!(third.is_some(), "DisposPictInfo should be handled");
        assert!(third.unwrap().is_ok(), "DisposPictInfo should return");
        dispospictinfo_ok = (cpu.read_reg(Register::A7) == sp + 4)
            && (cpu.read_reg(Register::D0) == 0)
            && (bus.read_word(sp + 4) == 0xFACE);

        assert!(
            newpictinfo_first_ok && newpictinfo_second_ok,
            "A831:newpictinfo_mints_distinct_nonzero_ids_and_preserves_stack"
        );
        assert!(
            dispospictinfo_ok,
            "A831:dispospictinfo_returns_noerr_and_preserves_stack"
        );

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x0206);
        bus.write_long(sp, id1);
        bus.write_word(sp + 4, 0xD00D);

        let fourth = disp.dispatch_toolbox(true, 0x031, &mut cpu, &mut bus);
        assert!(fourth.is_some(), "DisposPictInfo should be handled");
        assert!(fourth.unwrap().is_ok(), "DisposPictInfo should return");
        dispospictinfo_double_noerr_ok = (cpu.read_reg(Register::A7) == sp + 4)
            && (cpu.read_reg(Register::D0) == 0)
            && (bus.read_word(sp + 4) == 0xD00D);

        assert!(
            dispospictinfo_double_noerr_ok,
            "A831:dispospictinfo_returns_noerr_on_double_dispose"
        );
    }

    // ColorBit ($A864) — IM:I I-174 says the trap writes `whichBit` to the
    // current grafPort's colrBit field. colrBit is a word-sized INTEGER at
    // GrafPort offset +88 (Imaging With QuickDraw 1994, p. 4-39).
    //
    // Witnesses for the strict bake `a864_colorbit_strict`:
    //   tests::colorbit_writes_whichbit_value_to_current_port_colrbit_field_at_offset_88
    //   tests::colorbit_writes_max_31_value_to_current_port_colrbit_field
    //   tests::colorbit_zero_overwrites_previous_nonzero_colrbit_value
    //   tests::colorbit_consumes_two_byte_whichbit_argument_and_balances_stack
    #[test]
    fn colorbit_writes_whichbit_value_to_current_port_colrbit_field_at_offset_88() {
        let (mut disp, mut cpu, mut bus) = setup_with_port();
        let sp = TEST_SP;
        cpu.write_reg(Register::A7, sp);
        // Pre-poison adjacent fields to detect any over-write past the
        // 2-byte colrBit word at port+88.
        let port_ptr = 0x181000u32;
        bus.write_long(port_ptr + 84, 0x0000001E); // bkColor (whiteColor=30)
        bus.write_word(port_ptr + 88, 0); // colrBit initial value per IM:I I-174
        bus.write_word(port_ptr + 90, 0xC3A5); // patStretch sentinel

        bus.write_word(sp, 5);

        let result = disp.dispatch_toolbox(true, 0x064, &mut cpu, &mut bus);
        assert!(result.is_some(), "ColorBit must be a handled trap");
        assert!(result.unwrap().is_ok());

        assert_eq!(
            bus.read_word(port_ptr + 88),
            5,
            "ColorBit(5) must write 5 to thePort^.colrBit"
        );
        assert_eq!(
            bus.read_word(port_ptr + 90),
            0xC3A5,
            "ColorBit must not over-write past the 2-byte colrBit slot"
        );
        assert_eq!(
            bus.read_long(port_ptr + 84),
            0x0000001E,
            "ColorBit must not corrupt the preceding bkColor field"
        );
        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
    }

    #[test]
    fn colorbit_writes_max_31_value_to_current_port_colrbit_field() {
        // IM:I I-174: "the possible range of values for whichBit is 0 through 31"
        let (mut disp, mut cpu, mut bus) = setup_with_port();
        let sp = TEST_SP;
        cpu.write_reg(Register::A7, sp);
        let port_ptr = 0x181000u32;
        bus.write_word(port_ptr + 88, 0);
        bus.write_word(sp, 31);

        let result = disp.dispatch_toolbox(true, 0x064, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(port_ptr + 88), 31);
        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
    }

    #[test]
    fn colorbit_zero_overwrites_previous_nonzero_colrbit_value() {
        // Defeats a stub that doesn't actually write the field: after a
        // non-zero ColorBit, calling ColorBit(0) must clear it back to 0.
        let (mut disp, mut cpu, mut bus) = setup_with_port();
        let sp = TEST_SP;
        cpu.write_reg(Register::A7, sp);
        let port_ptr = 0x181000u32;

        // First ColorBit(7) — sets colrBit to 7.
        bus.write_word(sp, 7);
        let r1 = disp.dispatch_toolbox(true, 0x064, &mut cpu, &mut bus);
        assert!(r1.unwrap().is_ok());
        assert_eq!(bus.read_word(port_ptr + 88), 7);

        // Reset SP for the second call.
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0);
        let r2 = disp.dispatch_toolbox(true, 0x064, &mut cpu, &mut bus);
        assert!(r2.unwrap().is_ok());
        assert_eq!(bus.read_word(port_ptr + 88), 0);
    }

    #[test]
    fn colorbit_consumes_two_byte_whichbit_argument_and_balances_stack() {
        // Pascal PROCEDURE protocol: caller pushes 2-byte INTEGER whichBit;
        // trap pops 2 bytes; no function-result slot.
        let (mut disp, mut cpu, mut bus) = setup_with_port();
        let sp = TEST_SP;
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 12);
        bus.write_word(sp + 2, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x064, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp + 2,
            "ColorBit must pop exactly the 2-byte INTEGER argument"
        );
        assert_eq!(
            bus.read_word(sp + 2),
            0xBEEF,
            "ColorBit must not over-write past the 2-byte arg slot"
        );
    }

    #[test]
    fn longmul_writes_signed_64bit_product_to_dest_hilong_lolong() {
        // Inside Macintosh Volume I (1985), p. I-472:
        // LongMul writes signed Int64Bit { hiLong, loLong } product output.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let dest = bus.alloc(8);
        let a: i32 = -123_456_789;
        let b: i32 = 42_424;
        let expected = (a as i64) * (b as i64);

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, dest);
        bus.write_long(sp + 4, b as u32);
        bus.write_long(sp + 8, a as u32);

        let result = disp.dispatch_toolbox(true, 0x067, &mut cpu, &mut bus);
        assert!(result.is_some(), "LongMul should be handled");
        assert!(result.unwrap().is_ok(), "LongMul should return");
        assert_eq!(cpu.read_reg(Register::A7), sp + 12);
        assert_eq!(bus.read_long(dest), (expected >> 32) as u32);
        assert_eq!(bus.read_long(dest + 4), expected as u32);
    }

    #[test]
    fn longmul_consumes_a_b_and_dest_arguments() {
        // Inside Macintosh Volume I (1985), p. I-472:
        // PROCEDURE LongMul(a, b: LONGINT; VAR dest: Int64Bit) consumes 12 bytes.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0);
        bus.write_long(sp + 4, 2);
        bus.write_long(sp + 8, 3);
        bus.write_word(sp + 12, 0xCAFE);

        let result = disp.dispatch_toolbox(true, 0x067, &mut cpu, &mut bus);
        assert!(result.is_some(), "LongMul should be handled");
        assert!(result.unwrap().is_ok(), "LongMul should return");
        assert_eq!(cpu.read_reg(Register::A7), sp + 12);
        assert_eq!(bus.read_word(sp + 12), 0xCAFE);
    }

    // ========== Toolbox Event Traps ==========

    // GetNextEvent ($A970) — empty queue
    #[test]
    fn test_get_next_event_empty() {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.sent_open_app_event = true; // suppress synthetic oapp event
        let sp = TEST_SP;
        // SP+0: event_ptr(4), SP+4: eventMask(2), SP+6: result(2)
        let event_ptr = 0x200000u32;
        bus.write_long(sp, event_ptr);
        bus.write_word(sp + 4, 0xFFFF); // eventMask: all
        bus.write_word(sp + 6, 0xBEEF); // result placeholder

        let result = disp.dispatch_toolbox(true, 0x170, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        // No event: result = 0
        assert_eq!(bus.read_word(sp + 6), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 6);
    }

    // GetNextEvent ($A970) — with mouseDown event
    #[test]
    fn test_get_next_event_with_event() {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.sent_open_app_event = true; // suppress synthetic oapp event
        let sp = TEST_SP;
        let event_ptr = 0x200000u32;
        bus.write_long(sp, event_ptr);
        bus.write_word(sp + 4, 0xFFFF); // eventMask: all
        bus.write_word(sp + 6, 0x0000); // result placeholder

        // Push a mouseDown event
        disp.event_queue.push_back(QueuedEvent {
            what: 1,
            message: 0,
            where_v: 10,
            where_h: 20,
            modifiers: 0,
        });

        let result = disp.dispatch_toolbox(true, 0x170, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        // Event found: result = 0xFFFF
        assert_eq!(bus.read_word(sp + 6), 0xFFFF);
        assert_eq!(cpu.read_reg(Register::A7), sp + 6);
        // Event record: what field at event_ptr+0 (word) = 1 (mouseDown)
        assert_eq!(bus.read_word(event_ptr), 1);
    }

    #[test]
    fn get_next_event_delivers_visible_window_update_after_flushevents_drops_queue_entry() {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.sent_open_app_event = true;

        let bounds_rect_ptr = 0x300000u32;
        bus.write_word(bounds_rect_ptr, 40);
        bus.write_word(bounds_rect_ptr + 2, 40);
        bus.write_word(bounds_rect_ptr + 4, 140);
        bus.write_word(bounds_rect_ptr + 6, 220);

        let new_window_sp = TEST_SP - 26;
        cpu.write_reg(Register::A7, new_window_sp);
        for i in 0..30u32 {
            bus.write_byte(new_window_sp + i, 0);
        }
        bus.write_long(new_window_sp + 18, bounds_rect_ptr);
        bus.write_byte(new_window_sp + 12, 0xFF); // visible = TRUE

        let result = disp.dispatch_window(true, 0x113, &mut cpu, &mut bus);
        assert!(result.is_some(), "NewWindow should be handled");
        assert!(result.unwrap().is_ok(), "NewWindow should return");
        let window_ptr = bus.read_long(TEST_SP);
        assert_ne!(window_ptr, 0, "NewWindow should return a window");
        assert!(
            disp.event_queue
                .iter()
                .any(|event| event.what == 6 && event.message == window_ptr),
            "visible NewWindow should queue an updateEvt"
        );

        cpu.write_reg(Register::D0, 0x0000_FFFF);
        let flush = disp.dispatch_event(false, 0x32, &mut cpu, &mut bus);
        assert!(flush.is_some(), "FlushEvents should be handled");
        assert!(flush.unwrap().is_ok(), "FlushEvents should return");
        assert!(
            disp.event_queue.is_empty(),
            "FlushEvents($FFFF, 0) should remove queued events"
        );

        let event_ptr = 0x200000u32;
        let get_next_sp = TEST_SP - 8;
        cpu.write_reg(Register::A7, get_next_sp);
        bus.write_long(get_next_sp, event_ptr);
        bus.write_word(get_next_sp + 4, 0xFFFF);
        bus.write_word(get_next_sp + 6, 0);

        let event = disp.dispatch_toolbox(true, 0x170, &mut cpu, &mut bus);
        assert!(event.is_some(), "GetNextEvent should be handled");
        assert!(event.unwrap().is_ok(), "GetNextEvent should return");
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP - 2);
        assert_eq!(
            bus.read_word(get_next_sp + 6),
            0xFFFF,
            "GetNextEvent should report the pending update event"
        );
        assert_eq!(
            bus.read_word(event_ptr),
            6,
            "event.what should be updateEvt"
        );
        assert_eq!(
            bus.read_long(event_ptr + 2),
            window_ptr,
            "event.message should carry the dirty WindowPtr"
        );

        let second_sp = TEST_SP - 8;
        cpu.write_reg(Register::A7, second_sp);
        bus.write_long(second_sp, event_ptr);
        bus.write_word(second_sp + 4, 0xFFFF);
        bus.write_word(second_sp + 6, 0xBEEF);

        let second = disp.dispatch_toolbox(true, 0x170, &mut cpu, &mut bus);
        assert!(second.is_some(), "second GetNextEvent should be handled");
        assert!(second.unwrap().is_ok(), "second GetNextEvent should return");
        assert_eq!(
            bus.read_word(second_sp + 6),
            0,
            "flushed update recovery should be one-shot until a new update is queued"
        );
    }

    #[test]
    fn slotmanager_sreadinfo_selector_uses_a0_spblock_d0_selector_and_returns_oserr_in_d0() {
        // Inside Macintosh: Devices (1994), pp. 2-61 to 2-62:
        // _SlotManager selector $0010 (SReadInfo) uses A0=SpBlockPtr and
        // D0=selector on entry, and returns OSErr in D0 on exit.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_block_ptr = 0x0031_0000u32;
        let stack_ptr = 0x00F0_6100u32;
        let selector = 0x0010u32; // SReadInfo
        let sm_empty_slot = (-300i32) as u32;

        bus.write_long(sp_block_ptr, 0xA5A5_A5A5);
        bus.write_byte(sp_block_ptr + 49, 0x0A); // spSlot
        cpu.write_reg(Register::A0, sp_block_ptr);
        cpu.write_reg(Register::D0, selector);
        cpu.write_reg(Register::A7, stack_ptr);

        let result = disp.dispatch_toolbox(false, 0x06E, &mut cpu, &mut bus);
        assert!(result.is_some(), "SlotManager should be handled");
        assert!(
            result.unwrap().is_ok(),
            "SlotManager should return normally"
        );
        assert_eq!(
            cpu.read_reg(Register::D0),
            sm_empty_slot,
            "SlotManager should return OSErr in D0"
        );
        assert_eq!(
            cpu.read_reg(Register::A0),
            sp_block_ptr,
            "SlotManager should consume but not rewrite A0 SpBlock pointer"
        );
        assert_eq!(
            cpu.read_reg(Register::A7),
            stack_ptr,
            "SlotManager register calling convention should preserve A7"
        );
    }

    #[test]
    fn slotmanager_sreadinfo_empty_slot_returns_smemptyslot() {
        // Inside Macintosh: Devices (1994), pp. 2-61 to 2-62:
        // SReadInfo result code smEmptySlot (-300) means "No card in this slot."
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_block_ptr = 0x0031_0100u32;
        let sm_empty_slot = (-300i32) as u32;

        for slot in [0x09u8, 0x0Au8] {
            bus.write_long(sp_block_ptr, 0x1122_3344);
            bus.write_byte(sp_block_ptr + 49, slot); // spSlot
            cpu.write_reg(Register::A0, sp_block_ptr);
            cpu.write_reg(Register::D0, 0x0010); // SReadInfo selector

            let result = disp.dispatch_toolbox(false, 0x06E, &mut cpu, &mut bus);
            assert!(result.is_some(), "SlotManager should be handled");
            assert!(
                result.unwrap().is_ok(),
                "SlotManager should return normally"
            );
            assert_eq!(
                cpu.read_reg(Register::D0),
                sm_empty_slot,
                "SReadInfo selector should return smEmptySlot for empty slot {}",
                slot
            );
        }
    }

    #[test]
    fn slotmanager_writes_result_to_spblock_spresult_offset_zero() {
        // Inside Macintosh: Devices (1994), pp. 2-23 to 2-24:
        // SpBlock starts with spResult at offset 0.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_block_ptr = 0x0031_0200u32;
        let sm_empty_slot = (-300i32) as u32;

        bus.write_long(sp_block_ptr, 0xDEAD_BEEF); // spResult (offset 0)
        bus.write_long(sp_block_ptr + 4, 0xBEEF_DEAD); // spsPointer (offset 4)
        cpu.write_reg(Register::A0, sp_block_ptr);
        cpu.write_reg(Register::D0, 0x0010); // SReadInfo selector

        let result = disp.dispatch_toolbox(false, 0x06E, &mut cpu, &mut bus);
        assert!(result.is_some(), "SlotManager should be handled");
        assert!(
            result.unwrap().is_ok(),
            "SlotManager should return normally"
        );
        assert_eq!(
            bus.read_long(sp_block_ptr),
            sm_empty_slot,
            "SlotManager should mirror the result into SpBlock.spResult"
        );
        assert_eq!(
            bus.read_long(sp_block_ptr + 4),
            0xBEEF_DEAD,
            "SlotManager should not clobber adjacent SpBlock fields"
        );
    }

    #[test]
    fn slotmanager_other_selector_leaves_spblock_result_untouched() {
        // Systemless's SlotManager HLE models the documented SReadInfo
        // selector for the empty-slot result and leaves SpBlock state
        // alone for other selectors that still collapse to smEmptySlot.
        // This pins the selector-specific writeback rule in the HLE.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_block_ptr = 0x0031_0300u32;
        let sm_empty_slot = (-300i32) as u32;

        bus.write_long(sp_block_ptr, 0xDEAD_BEEF);
        bus.write_long(sp_block_ptr + 4, 0xBEEF_DEAD);
        cpu.write_reg(Register::A0, sp_block_ptr);
        cpu.write_reg(Register::D0, 0x0000);

        let result = disp.dispatch_toolbox(false, 0x06E, &mut cpu, &mut bus);
        assert!(result.is_some(), "SlotManager should be handled");
        assert!(
            result.unwrap().is_ok(),
            "SlotManager should return normally"
        );
        assert_eq!(
            cpu.read_reg(Register::D0),
            sm_empty_slot,
            "SlotManager should still return smEmptySlot"
        );
        assert_eq!(
            bus.read_long(sp_block_ptr),
            0xDEAD_BEEF,
            "non-SReadInfo selectors should not rewrite SpBlock.spResult"
        );
        assert_eq!(
            bus.read_long(sp_block_ptr + 4),
            0xBEEF_DEAD,
            "non-SReadInfo selectors should not clobber adjacent SpBlock fields"
        );
    }

    // Inside Macintosh Volume I, I-257: events not designated by eventMask are
    // kept in the queue and a null event is returned when no designated event
    // is available.
    #[test]
    fn test_get_next_event_mask_miss_returns_null_and_preserves_queue() {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.sent_open_app_event = true; // suppress synthetic oapp event
        let sp = TEST_SP;
        let event_ptr = 0x200000u32;
        bus.write_long(sp, event_ptr);
        bus.write_word(sp + 4, 0x0008); // keyDownMask (what=3)
        bus.write_word(sp + 6, 0xBEEF);

        disp.event_queue.push_back(QueuedEvent {
            what: 1, // mouseDown (not in keyDownMask)
            message: 0,
            where_v: 44,
            where_h: 88,
            modifiers: 0,
        });

        let first = disp.dispatch_toolbox(true, 0x170, &mut cpu, &mut bus);
        assert!(first.is_some());
        assert!(first.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 6), 0);
        assert_eq!(bus.read_word(event_ptr), 0); // nullEvt
        assert_eq!(disp.event_queue.len(), 1);
        assert_eq!(disp.event_queue.front().map(|event| event.what), Some(1));

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, event_ptr);
        bus.write_word(sp + 4, 0x0002); // mouseDownMask (what=1)
        bus.write_word(sp + 6, 0x0000);

        let second = disp.dispatch_toolbox(true, 0x170, &mut cpu, &mut bus);
        assert!(second.is_some());
        assert!(second.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 6), 0xFFFF);
        assert_eq!(bus.read_word(event_ptr), 1);
        assert!(disp.event_queue.is_empty());
    }

    fn test_region_handle(
        bus: &mut crate::memory::MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
    ) -> u32 {
        let rgn_ptr = bus.alloc(10);
        let rgn_handle = bus.alloc(4);
        bus.write_long(rgn_handle, rgn_ptr);
        bus.write_word(rgn_ptr, 10);
        bus.write_word(rgn_ptr + 2, top as u16);
        bus.write_word(rgn_ptr + 4, left as u16);
        bus.write_word(rgn_ptr + 6, bottom as u16);
        bus.write_word(rgn_ptr + 8, right as u16);
        rgn_handle
    }

    // WaitNextEvent ($A860) — empty queue
    #[test]
    fn test_wait_next_event_empty() {
        let (mut disp, mut cpu, mut bus) = setup();
        // Mark the synthetic kAEOpenApplication as already sent so this tests
        // the normal empty-queue path.
        disp.sent_open_app_event = true;
        let sp = TEST_SP;
        // SP+0: mouseRgn(4), SP+4: sleep(4), SP+8: event_ptr(4), SP+12: eventMask(2), SP+14: result(2)
        bus.write_long(sp, 0); // mouseRgn
        bus.write_long(sp + 4, 60); // sleep
        bus.write_long(sp + 8, 0x200000); // event_ptr
        bus.write_word(sp + 12, 0xFFFF); // eventMask
        bus.write_word(sp + 14, 0xBEEF); // result placeholder

        let result = disp.dispatch_toolbox(true, 0x060, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 14), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 14);
        assert_eq!(disp.pending_wait_sleep_ticks, 60);
        assert_eq!(disp.tick_count, 100);
        assert_eq!(bus.read_long(0x200000 + 6), 100);
    }

    #[test]
    fn test_wait_next_event_zero_mask_takes_null_event_path() {
        let (mut disp, mut cpu, mut bus) = setup();
        assert!(!disp.sent_open_app_event);
        let sp = TEST_SP;
        let event_ptr = 0x200000u32;

        bus.write_word(event_ptr, 0x5555);
        bus.write_long(sp, 0); // mouseRgn
        bus.write_long(sp + 4, 1); // sleep
        bus.write_long(sp + 8, event_ptr);
        bus.write_word(sp + 12, 0); // eventMask: no event types selected
        bus.write_word(sp + 14, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x060, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 14), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 14);
        assert_eq!(bus.read_word(event_ptr), 0);
        assert!(!disp.sent_open_app_event);
        assert_eq!(disp.pending_wait_sleep_ticks, 1);
    }

    #[test]
    fn wait_next_event_mouse_rgn_outside_returns_mouse_moved_os_event() {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.sent_open_app_event = true;
        disp.mouse_pos = (50, 25);
        let sp = TEST_SP;
        let event_ptr = 0x200000u32;
        let mouse_rgn = test_region_handle(&mut bus, 10, 20, 30, 40);

        bus.write_long(sp, mouse_rgn);
        bus.write_long(sp + 4, 60);
        bus.write_long(sp + 8, event_ptr);
        bus.write_word(sp + 12, 0x8000); // osMask, event type 15.
        bus.write_word(sp + 14, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x060, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 14), 0xFFFF);
        assert_eq!(cpu.read_reg(Register::A7), sp + 14);
        assert_eq!(bus.read_word(event_ptr), 15);
        assert_eq!(bus.read_long(event_ptr + 2), 0xFA00_0000);
        assert_eq!(bus.read_word(event_ptr + 10), 50);
        assert_eq!(bus.read_word(event_ptr + 12), 25);
        assert_eq!(disp.pending_wait_sleep_ticks, 0);
    }

    #[test]
    fn wait_next_event_mouse_rgn_inside_takes_null_sleep_path() {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.sent_open_app_event = true;
        disp.mouse_pos = (20, 25);
        let sp = TEST_SP;
        let event_ptr = 0x200000u32;
        let mouse_rgn = test_region_handle(&mut bus, 10, 20, 30, 40);

        bus.write_long(sp, mouse_rgn);
        bus.write_long(sp + 4, 7);
        bus.write_long(sp + 8, event_ptr);
        bus.write_word(sp + 12, 0x8000); // osMask.
        bus.write_word(sp + 14, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x060, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 14), 0);
        assert_eq!(bus.read_word(event_ptr), 0);
        assert_eq!(disp.pending_wait_sleep_ticks, 7);
    }

    #[test]
    fn wait_next_event_empty_mouse_rgn_suppresses_mouse_moved_event() {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.sent_open_app_event = true;
        disp.mouse_pos = (50, 25);
        let sp = TEST_SP;
        let event_ptr = 0x200000u32;
        let empty_mouse_rgn = test_region_handle(&mut bus, 0, 0, 0, 0);

        bus.write_long(sp, empty_mouse_rgn);
        bus.write_long(sp + 4, 9);
        bus.write_long(sp + 8, event_ptr);
        bus.write_word(sp + 12, 0x8000); // osMask.
        bus.write_word(sp + 14, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x060, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 14), 0);
        assert_eq!(bus.read_word(event_ptr), 0);
        assert_eq!(disp.pending_wait_sleep_ticks, 9);
    }

    #[test]
    fn wait_next_event_mouse_rgn_respects_event_mask() {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.sent_open_app_event = true;
        disp.mouse_pos = (50, 25);
        let sp = TEST_SP;
        let event_ptr = 0x200000u32;
        let mouse_rgn = test_region_handle(&mut bus, 10, 20, 30, 40);

        bus.write_long(sp, mouse_rgn);
        bus.write_long(sp + 4, 11);
        bus.write_long(sp + 8, event_ptr);
        bus.write_word(sp + 12, 0x0002); // mouseDownMask, not osMask.
        bus.write_word(sp + 14, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x060, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 14), 0);
        assert_eq!(bus.read_word(event_ptr), 0);
        assert_eq!(disp.pending_wait_sleep_ticks, 11);
    }

    // WaitNextEvent ($A860) — synthesizes kAEOpenApplication on first call
    #[test]
    fn test_wait_next_event_synthesizes_open_app() {
        let (mut disp, mut cpu, mut bus) = setup();
        assert!(!disp.sent_open_app_event);
        let sp = TEST_SP;
        let event_ptr = 0x200000u32;
        bus.write_long(sp, 0); // mouseRgn
        bus.write_long(sp + 4, 0); // sleep
        bus.write_long(sp + 8, event_ptr); // event_ptr
        bus.write_word(sp + 12, 0x0400); // highLevelEventMask only (bit 10)
        bus.write_word(sp + 14, 0x0000); // result placeholder

        let result = disp.dispatch_toolbox(true, 0x060, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        // Should return TRUE (event found)
        assert_eq!(bus.read_word(sp + 14), 0xFFFF);
        // what = kHighLevelEvent (23)
        assert_eq!(bus.read_word(event_ptr), 23);
        // message = kCoreEventClass ('aevt' = 0x61657674)
        assert_eq!(bus.read_long(event_ptr + 2), 0x61657674);
        // where = kAEOpenApplication ('oapp' = 0x6F617070), packed as Point (v, h)
        assert_eq!(bus.read_word(event_ptr + 10), 0x6F61); // where.v
        assert_eq!(bus.read_word(event_ptr + 12), 0x7070); // where.h
                                                           // Flag should be set
        assert!(disp.sent_open_app_event);

        // Second call should NOT return synthetic event
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0);
        bus.write_long(sp + 4, 0);
        bus.write_long(sp + 8, event_ptr);
        bus.write_word(sp + 12, 0x0400);
        bus.write_word(sp + 14, 0x0000);

        let result2 = disp.dispatch_toolbox(true, 0x060, &mut cpu, &mut bus);
        assert!(result2.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 14), 0); // no event
    }

    // EventAvail ($A971) — with event (peeks, does not remove)
    #[test]
    fn test_event_avail_with_event() {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.sent_open_app_event = true; // suppress synthetic oapp event
        let sp = TEST_SP;
        let event_ptr = 0x200000u32;
        // SP+0: event_ptr(4), SP+4: eventMask(2), SP+6: result(2)
        bus.write_long(sp, event_ptr);
        bus.write_word(sp + 4, 0xFFFF); // all events
        bus.write_word(sp + 6, 0x0000);

        disp.event_queue.push_back(QueuedEvent {
            what: 1, // mouseDown
            message: 0,
            where_v: 5,
            where_h: 15,
            modifiers: 0,
        });

        let result = disp.dispatch_toolbox(true, 0x171, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 6), 0xFFFF);
        assert_eq!(cpu.read_reg(Register::A7), sp + 6);
        // Event record should have what=1
        assert_eq!(bus.read_word(event_ptr), 1);
        // Event should still be in the queue (peek, not dequeue)
        assert_eq!(disp.event_queue.len(), 1);
    }

    // EventAvail ($A971) — empty queue
    #[test]
    fn test_event_avail_empty() {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.sent_open_app_event = true; // suppress synthetic oapp event
        let sp = TEST_SP;
        let event_ptr = 0x200000u32;
        bus.write_long(sp, event_ptr);
        bus.write_word(sp + 4, 0xFFFF);
        bus.write_word(sp + 6, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x171, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 6), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 6);
        assert_eq!(bus.read_word(event_ptr), 0);
        assert_ne!(bus.read_word(event_ptr + 14) & 0x0080, 0);
    }

    // Inside Macintosh Volume I, I-257..I-259: EventAvail follows the same
    // eventMask filtering as GetNextEvent and does not dequeue non-designated
    // events.
    #[test]
    fn test_event_avail_mask_miss_returns_null_and_preserves_queue() {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.sent_open_app_event = true; // suppress synthetic oapp event
        let sp = TEST_SP;
        let event_ptr = 0x200000u32;
        bus.write_long(sp, event_ptr);
        bus.write_word(sp + 4, 0x0008); // keyDownMask (what=3)
        bus.write_word(sp + 6, 0xBEEF);

        disp.event_queue.push_back(QueuedEvent {
            what: 1, // mouseDown
            message: 0x1234_5678,
            where_v: 11,
            where_h: 22,
            modifiers: 0,
        });

        let result = disp.dispatch_toolbox(true, 0x171, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 6), 0);
        assert_eq!(bus.read_word(event_ptr), 0); // nullEvt
        assert_eq!(disp.event_queue.len(), 1);
        assert_eq!(disp.event_queue.front().map(|event| event.what), Some(1));
    }

    // Inside Macintosh Volume I, I-259: EventAvail reports an event without
    // removing it, so a following GetNextEvent can return that same event.
    #[test]
    fn test_event_avail_then_get_next_event_returns_same_event() {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.sent_open_app_event = true; // suppress synthetic oapp event
        let sp = TEST_SP;
        let event_ptr = 0x200000u32;
        bus.write_long(sp, event_ptr);
        bus.write_word(sp + 4, 0x0002); // mouseDownMask
        bus.write_word(sp + 6, 0x0000);

        disp.event_queue.push_back(QueuedEvent {
            what: 1,
            message: 0xCAFEBABE,
            where_v: 9,
            where_h: 19,
            modifiers: 0,
        });

        let peek = disp.dispatch_toolbox(true, 0x171, &mut cpu, &mut bus);
        assert!(peek.is_some());
        assert!(peek.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 6), 0xFFFF);
        assert_eq!(bus.read_word(event_ptr), 1);
        assert_eq!(bus.read_long(event_ptr + 2), 0xCAFEBABE);
        assert_eq!(disp.event_queue.len(), 1);

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, event_ptr);
        bus.write_word(sp + 4, 0x0002); // mouseDownMask
        bus.write_word(sp + 6, 0x0000);

        let dequeue = disp.dispatch_toolbox(true, 0x170, &mut cpu, &mut bus);
        assert!(dequeue.is_some());
        assert!(dequeue.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 6), 0xFFFF);
        assert_eq!(bus.read_word(event_ptr), 1);
        assert_eq!(bus.read_long(event_ptr + 2), 0xCAFEBABE);
        assert!(disp.event_queue.is_empty());
    }

    #[test]
    fn test_event_avail_synthesizes_open_app_without_consuming() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let event_ptr = 0x200000u32;
        bus.write_long(sp, event_ptr);
        bus.write_word(sp + 4, 0x0400); // highLevelEventMask
        bus.write_word(sp + 6, 0x0000);

        let result = disp.dispatch_toolbox(true, 0x171, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 6), 0xFFFF);
        assert_eq!(bus.read_word(event_ptr), 23);
        assert_eq!(bus.read_long(event_ptr + 2), 0x61657674);
        assert_eq!(disp.event_queue.len(), 1);
        assert_eq!(disp.event_queue.front().map(|event| event.what), Some(23));
    }

    #[test]
    fn test_get_next_event_synthesizes_open_app() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let event_ptr = 0x200000u32;
        bus.write_long(sp, event_ptr);
        bus.write_word(sp + 4, 0x0400); // highLevelEventMask
        bus.write_word(sp + 6, 0x0000);

        let result = disp.dispatch_toolbox(true, 0x170, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 6), 0xFFFF);
        assert_eq!(bus.read_word(event_ptr), 23);
        assert_eq!(bus.read_long(event_ptr + 2), 0x61657674);
        assert!(disp.event_queue.is_empty());
    }

    // GetKeys ($A976)
    #[test]
    fn test_get_keys_reflects_pressed_keys() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let keys_ptr = 0x200100u32;

        // Press left arrow (0x7B), space (0x31), and the EVO hyperspace
        // controls whose KeyMap word layout matters in-game.
        disp.push_key_down(0x7B, 28);
        disp.push_key_down(0x31, 32);
        disp.push_key_down(0x26, b'j');
        disp.push_key_down(0x7E, 30);

        bus.write_long(sp, keys_ptr);
        let result = disp.dispatch_toolbox(true, 0x176, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);

        assert!(disp.key_is_down(0x7B));
        assert!(disp.key_is_down(0x31));
        assert!(disp.key_is_down(0x26));
        assert!(disp.key_is_down(0x7E));
        assert!(!disp.key_is_down(0x2E), "J must not alias the M key");
        assert_eq!(bus.read_byte(keys_ptr + 4), 0x40, "J key byte");
        assert_eq!(
            bus.read_byte(keys_ptr + 5),
            0,
            "M key byte should stay clear when J is down"
        );
        assert_eq!(bus.read_byte(keys_ptr + 6), 0x02, "space key byte");
        assert_eq!(bus.read_byte(keys_ptr + 15), 0x48, "left/up arrow key byte");
        assert_eq!(
            bus.read_byte(keys_ptr + 14),
            0,
            "unused arrow byte should stay clear"
        );

        // Release left arrow and verify it clears.
        disp.push_key_up(0x7B, 28);
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, keys_ptr);
        let result = disp.dispatch_toolbox(true, 0x176, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert!(!disp.key_is_down(0x7B));
        assert!(disp.key_is_down(0x31));
        assert!(disp.key_is_down(0x26));
        assert!(disp.key_is_down(0x7E));
        assert_eq!(bus.read_byte(keys_ptr + 15), 0x40, "only up remains");

        // J ($26) and M ($2E) differ only by KeyMap byte. This catches the
        // byte-pair swap that makes EV/Marathon direct pollers invert them.
        disp.push_key_up(0x26, b'j');
        disp.push_key_down(0x2E, b'm');
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, keys_ptr);
        let result = disp.dispatch_toolbox(true, 0x176, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert!(!disp.key_is_down(0x26));
        assert!(disp.key_is_down(0x2E));
        assert_eq!(bus.read_byte(keys_ptr + 4), 0, "J byte should be clear");
        assert_eq!(bus.read_byte(keys_ptr + 5), 0x40, "M key byte");
    }

    // GetMouse ($A972)
    #[test]
    fn test_get_mouse() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let pt_ptr = 0x200000u32;
        bus.write_long(sp, pt_ptr);

        disp.mouse_pos = (50, 100);

        let result = disp.dispatch_toolbox(true, 0x172, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        // Point: v at pt_ptr, h at pt_ptr+2
        assert_eq!(bus.read_word(pt_ptr), 50);
        assert_eq!(bus.read_word(pt_ptr + 2), 100);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
    }

    #[test]
    fn get_mouse_returns_current_port_local_coordinates() {
        let (mut disp, mut cpu, mut bus) = setup_with_port();
        let sp = TEST_SP;
        let port = 0x181000u32;
        let pt_ptr = 0x200000u32;
        bus.write_long(sp, pt_ptr);

        // Same geometry as a window/dialog at global (80,120) whose local
        // coordinate system still starts at (0,0).
        bus.write_word(port + 8, (-80i16) as u16);
        bus.write_word(port + 10, (-120i16) as u16);
        bus.write_word(port + 16, 0);
        bus.write_word(port + 18, 0);
        disp.mouse_pos = (95, 145);

        let result = disp.dispatch_toolbox(true, 0x172, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(
            (
                bus.read_word(pt_ptr) as i16,
                bus.read_word(pt_ptr + 2) as i16
            ),
            (15, 25),
            "GetMouse should report the mouse in current-port local coordinates"
        );
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
    }

    // StillDown ($A973) — button pressed
    #[test]
    fn test_still_down_pressed() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_word(sp, 0x0000);

        disp.mouse_button = true;

        let result = disp.dispatch_toolbox(true, 0x173, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp), 0xFFFF);
        // SP unchanged for StillDown
        assert_eq!(cpu.read_reg(Register::A7), sp);
    }

    // StillDown ($A973) — button not pressed
    #[test]
    fn test_still_down_not_pressed() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_word(sp, 0xFFFF);

        disp.mouse_button = false;

        let result = disp.dispatch_toolbox(true, 0x173, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp);
    }

    // Button ($A974) — pressed (reads MBState $0172)
    #[test]
    fn test_button_pressed() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_word(sp, 0x0000);
        bus.write_byte(0x0172, 0x00); // MBState: button down

        let result = disp.dispatch_toolbox(true, 0x174, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp), 0xFFFF);
        assert_eq!(cpu.read_reg(Register::A7), sp);
    }

    // Button ($A974) — not pressed (reads MBState $0172)
    #[test]
    fn test_button_not_pressed() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_word(sp, 0xFFFF);
        bus.write_byte(0x0172, 0x80); // MBState: button up

        let result = disp.dispatch_toolbox(true, 0x174, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp);
    }

    // Button ($A974) also reports a pending mouseDown in the event queue.
    #[test]
    fn test_button_reports_queued_mouse_down() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_word(sp, 0);
        bus.write_byte(0x0172, 0x80); // physical state already released
        disp.push_mouse_down(50, 100);
        disp.push_mouse_up(50, 100);

        let result = disp.dispatch_toolbox(true, 0x174, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(
            bus.read_word(sp),
            0xFFFF,
            "Button should observe a queued mouseDown even after MBState released"
        );
        assert_eq!(cpu.read_reg(Register::A7), sp);
    }

    // Button reads MBState ($0172), which is updated by the runner at
    // each tick advance (VBL analog). After push_mouse_up the internal
    // mouse_button is false, but $0172 retains the pressed state until
    // the next tick — matching real hardware VBL latency.
    #[test]
    fn test_button_reads_mbstate_not_internal_flag() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        // Simulate: runner wrote $0172=0x00 on mouse-down
        bus.write_byte(0x0172, 0x00);
        disp.push_mouse_down(50, 100);
        let _ = disp.dequeue_event(0xFFFF);
        disp.push_mouse_up(50, 100);

        // Internal state is released, but $0172 is still "pressed"
        // (runner hasn't advanced a tick yet).
        assert!(!disp.mouse_button);
        bus.write_word(sp, 0);
        let result = disp.dispatch_toolbox(true, 0x174, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(sp), 0xFFFF); // Button sees $0172 = pressed

        // After the runner advances a tick, it would write $0172 = 0x80.
        // Simulate that:
        bus.write_byte(0x0172, 0x80);
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0xFFFF);
        let result = disp.dispatch_toolbox(true, 0x174, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(sp), 0); // Now Button sees released
    }

    // TickCount ($A975)
    #[test]
    fn test_tick_count() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        // TickCount reads self.tick_count directly (kept in sync with
        // $016A by advance_guest_tick). In production advance_guest_tick
        // updates both; the test sets both here to match.
        bus.write_long(0x016A, 12345);
        disp.tick_count = 12345;

        let result = disp.dispatch_toolbox(true, 0x175, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_long(sp), 12345);
        // SP unchanged
        assert_eq!(cpu.read_reg(Register::A7), sp);
    }

    #[test]
    fn tickcount_returns_self_tick_count_and_does_not_advance_a7() {
        // Per Macintosh Toolbox Essentials 1992 p. 2-112 the Pascal
        // FUNCTION protocol pre-allocates a 4-byte LongInt result
        // slot at SP+0; the trap writes there and the caller pops
        // the result. Specifically the trap must NOT advance A7 —
        // doing so would corrupt the C compiler's expected stack
        // frame on return. This contract test pre-poisons SP+4 with
        // a sentinel and verifies the trap leaves it untouched while
        // writing the result to SP+0.
        //
        // Distinct from `test_tick_count` because that test confirms
        // the value and the SP-non-advance, while this one also
        // proves the trap does not write past the result slot — a
        // regression guard against any future "fix" that uses
        // bus.write_long(sp - 4, ...) or bus.write_long(sp + 4, ...).
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_long(sp, 0xDEAD_BEEF); // pre-poison result slot
        bus.write_long(sp + 4, 0xCAFE_BABE); // pre-poison past-result slot
        bus.write_long(sp.wrapping_sub(4), 0xFEED_FACE); // pre-poison pre-result slot
        disp.tick_count = 0x0000_4321;
        bus.write_long(0x016A, 0x0000_4321);

        let result = disp.dispatch_toolbox(true, 0x175, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        // The trap wrote the LongInt result to SP+0.
        assert_eq!(bus.read_long(sp), 0x0000_4321);
        // SP+4 sentinel preserved — no over-write past the result slot.
        assert_eq!(bus.read_long(sp + 4), 0xCAFE_BABE);
        // SP-4 sentinel preserved — no under-write before the result slot.
        assert_eq!(bus.read_long(sp.wrapping_sub(4)), 0xFEED_FACE);
        // A7 unchanged: Pascal FUNCTION result slot is consumed by caller.
        assert_eq!(cpu.read_reg(Register::A7), sp);
    }

    #[test]
    fn tickcount_returns_monotonically_nondecreasing_across_two_calls() {
        // Per MTE 1992 p. 2-112: "The tick count is incremented during
        // the vertical retrace interrupt"; IM:I I-260 warns to use
        // ">= previous" comparisons. Systemless's HLE reads self.tick_count
        // which is monotonic by construction (advance_guest_tick only
        // increments). This contract test asserts that two consecutive
        // dispatches with self.tick_count incremented between them
        // return monotonic results.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        disp.tick_count = 1_000;
        bus.write_long(0x016A, 1_000);

        let r1 = disp.dispatch_toolbox(true, 0x175, &mut cpu, &mut bus);
        assert!(r1.unwrap().is_ok());
        let t1 = bus.read_long(sp);

        // Advance tick globally (mirroring what advance_guest_tick does
        // between trap dispatches in the runner).
        disp.tick_count += 1;
        bus.write_long(0x016A, disp.tick_count);

        let r2 = disp.dispatch_toolbox(true, 0x175, &mut cpu, &mut bus);
        assert!(r2.unwrap().is_ok());
        let t2 = bus.read_long(sp);

        assert!(t2 >= t1, "TickCount must be monotonic: t1={t1} t2={t2}");
        assert_eq!(t1, 1_000);
        assert_eq!(t2, 1_001);
    }

    // ========== Scrap Manager Traps ==========

    // Inside Macintosh Volume I, I-457..I-459: InfoScrap returns ScrapStuff
    // fields, ZeroScrap increments scrapCount, and PutScrap contributes bytes
    // to scrapSize.
    #[test]
    fn infoscrap_reports_in_memory_scrapstate_and_entry_size() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_long(sp, 0xDEAD_BEEF);
        let zero = disp.dispatch_toolbox(true, 0x1FC, &mut cpu, &mut bus);
        assert!(zero.is_some());
        assert!(zero.unwrap().is_ok());
        assert_eq!(bus.read_long(sp), 0); // noErr
        assert_eq!(cpu.read_reg(Register::A7), sp);

        let source = bus.alloc(3);
        bus.write_bytes(source, b"ABC");
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, source);
        bus.write_long(sp + 4, u32::from_be_bytes(*b"TEXT"));
        bus.write_long(sp + 8, 3);
        bus.write_long(sp + 12, 0xDEAD_BEEF);
        let put = disp.dispatch_toolbox(true, 0x1FE, &mut cpu, &mut bus);
        assert!(put.is_some());
        assert!(put.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 12), 0); // noErr
        assert_eq!(cpu.read_reg(Register::A7), sp + 12);

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0);
        let info = disp.dispatch_toolbox(true, 0x1F9, &mut cpu, &mut bus);
        assert!(info.is_some());
        assert!(info.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp);

        let scrap_info = bus.read_long(sp);
        assert_ne!(scrap_info, 0);
        assert_eq!(bus.read_long(scrap_info), 12); // type(4)+len(4)+padded data(4)
        let scrap_handle = bus.read_long(scrap_info + 4);
        assert_ne!(
            scrap_handle, 0,
            "InfoScrap should expose a live Handle when the scrap is in memory"
        );
        let scrap_ptr = bus.read_long(scrap_handle);
        assert_ne!(
            scrap_ptr, 0,
            "non-empty in-memory scrap should have a non-NIL master-pointer target"
        );
        assert_eq!(bus.get_alloc_size(scrap_ptr), Some(12));
        assert_eq!(bus.read_word(scrap_info + 8), 1); // scrapCount after first ZeroScrap
        assert_eq!(bus.read_word(scrap_info + 10), 1); // positive = in memory (IM:I I-457)
        assert_eq!(bus.read_long(scrap_info + 12), 0); // scrapName = NIL in HLE
    }

    // Inside Macintosh Volume I, I-457: ScrapHandle is a handle to the desk
    // scrap when the scrap is in memory. The serialized bytes are laid out as
    // type(4) + length(4) + data + even-byte padding.
    #[test]
    fn infoscrap_scraphandle_serializes_current_entries() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_long(sp, 0);
        let zero = disp.dispatch_toolbox(true, 0x1FC, &mut cpu, &mut bus);
        assert!(zero.unwrap().is_ok());

        let source = bus.alloc(3);
        bus.write_bytes(source, b"ABC");
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, source);
        bus.write_long(sp + 4, u32::from_be_bytes(*b"TEXT"));
        bus.write_long(sp + 8, 3);
        bus.write_long(sp + 12, 0);
        let put = disp.dispatch_toolbox(true, 0x1FE, &mut cpu, &mut bus);
        assert!(put.unwrap().is_ok());

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0);
        let info = disp.dispatch_toolbox(true, 0x1F9, &mut cpu, &mut bus);
        assert!(info.unwrap().is_ok());

        let scrap_info = bus.read_long(sp);
        let scrap_handle = bus.read_long(scrap_info + 4);
        assert_ne!(scrap_handle, 0);
        let scrap_ptr = bus.read_long(scrap_handle);
        assert_ne!(scrap_ptr, 0);

        let expected = [b'T', b'E', b'X', b'T', 0, 0, 0, 3, b'A', b'B', b'C', 0];
        assert_eq!(
            bus.read_bytes(scrap_ptr, expected.len()),
            expected.as_slice()
        );
        assert_eq!(bus.get_alloc_size(scrap_ptr), Some(expected.len() as u32));
    }

    // Inside Macintosh Volume I, I-458: ZeroScrap clears prior data and changes
    // scrapCount in the InfoScrap record.
    #[test]
    fn zeroscrap_clears_contents_and_changes_scrapcount() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_long(sp, 0);
        let zero1 = disp.dispatch_toolbox(true, 0x1FC, &mut cpu, &mut bus);
        assert!(zero1.unwrap().is_ok());

        let source = bus.alloc(1);
        bus.write_byte(source, b'X');
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, source);
        bus.write_long(sp + 4, u32::from_be_bytes(*b"TEXT"));
        bus.write_long(sp + 8, 1);
        bus.write_long(sp + 12, 0);
        let put = disp.dispatch_toolbox(true, 0x1FE, &mut cpu, &mut bus);
        assert!(put.unwrap().is_ok());

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0);
        let info_before = disp.dispatch_toolbox(true, 0x1F9, &mut cpu, &mut bus);
        assert!(info_before.unwrap().is_ok());
        let scrap_info = bus.read_long(sp);
        assert_eq!(bus.read_long(scrap_info), 10); // 8 + padded(1 -> 2)
        assert_eq!(bus.read_word(scrap_info + 8), 1);

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0xDEAD_BEEF);
        let zero2 = disp.dispatch_toolbox(true, 0x1FC, &mut cpu, &mut bus);
        assert!(zero2.unwrap().is_ok());
        assert_eq!(bus.read_long(sp), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp);

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0);
        let info_after = disp.dispatch_toolbox(true, 0x1F9, &mut cpu, &mut bus);
        assert!(info_after.unwrap().is_ok());
        let scrap_info_after = bus.read_long(sp);
        assert_eq!(bus.read_long(scrap_info_after), 0);
        assert_eq!(bus.read_word(scrap_info_after + 8), 2);
    }

    // Inside Macintosh Volume I, I-459: GetScrap returns byte length on
    // success and reports data offset from start-of-scrap; NIL hDest queries
    // length/offset without copying.
    #[test]
    fn getscrap_with_nil_handle_returns_length_and_data_offset() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_long(sp, 0);
        let zero = disp.dispatch_toolbox(true, 0x1FC, &mut cpu, &mut bus);
        assert!(zero.unwrap().is_ok());

        let text_src = bus.alloc(1);
        bus.write_byte(text_src, b'A');
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, text_src);
        bus.write_long(sp + 4, u32::from_be_bytes(*b"TEXT"));
        bus.write_long(sp + 8, 1);
        bus.write_long(sp + 12, 0);
        let put_text = disp.dispatch_toolbox(true, 0x1FE, &mut cpu, &mut bus);
        assert!(put_text.unwrap().is_ok());

        let pict_src = bus.alloc(3);
        bus.write_bytes(pict_src, b"XYZ");
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, pict_src);
        bus.write_long(sp + 4, u32::from_be_bytes(*b"PICT"));
        bus.write_long(sp + 8, 3);
        bus.write_long(sp + 12, 0);
        let put_pict = disp.dispatch_toolbox(true, 0x1FE, &mut cpu, &mut bus);
        assert!(put_pict.unwrap().is_ok());

        let offset_ptr = bus.alloc(4);
        bus.write_long(offset_ptr, 0xFFFF_FFFF);
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, offset_ptr);
        bus.write_long(sp + 4, u32::from_be_bytes(*b"PICT"));
        bus.write_long(sp + 8, 0); // NIL handle query path
        bus.write_long(sp + 12, 0xDEAD_BEEF);
        let get = disp.dispatch_toolbox(true, 0x1FD, &mut cpu, &mut bus);
        assert!(get.is_some());
        assert!(get.unwrap().is_ok());

        assert_eq!(bus.read_long(sp + 12), 3);
        assert_eq!(bus.read_long(offset_ptr), 18); // TEXT entry(10) + PICT header(8)
        assert_eq!(cpu.read_reg(Register::A7), sp + 12);
    }

    // Inside Macintosh Volume I, I-459: GetScrap returns noTypeErr (-102)
    // when no data of the requested type exists.
    #[test]
    fn getscrap_missing_type_returns_notypeerr() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        let offset_ptr = bus.alloc(4);
        bus.write_long(offset_ptr, 0x1234_5678);
        bus.write_long(sp, offset_ptr);
        bus.write_long(sp + 4, u32::from_be_bytes(*b"TEXT"));
        bus.write_long(sp + 8, 0);
        bus.write_long(sp + 12, 0);

        let get = disp.dispatch_toolbox(true, 0x1FD, &mut cpu, &mut bus);
        assert!(get.is_some());
        assert!(get.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 12) as i32, -102);
        assert_eq!(cpu.read_reg(Register::A7), sp + 12);
    }

    // Inside Macintosh Volume I, I-459 warning: duplicate PutScrap type entries
    // append, and GetScrap returns the first matching type.
    #[test]
    fn getscrap_duplicate_type_returns_first_occurrence() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_long(sp, 0);
        let zero = disp.dispatch_toolbox(true, 0x1FC, &mut cpu, &mut bus);
        assert!(zero.unwrap().is_ok());

        let old_src = bus.alloc(3);
        bus.write_bytes(old_src, b"OLD");
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, old_src);
        bus.write_long(sp + 4, u32::from_be_bytes(*b"TEXT"));
        bus.write_long(sp + 8, 3);
        bus.write_long(sp + 12, 0);
        let put_old = disp.dispatch_toolbox(true, 0x1FE, &mut cpu, &mut bus);
        assert!(put_old.unwrap().is_ok());

        let new_src = bus.alloc(3);
        bus.write_bytes(new_src, b"NEW");
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, new_src);
        bus.write_long(sp + 4, u32::from_be_bytes(*b"TEXT"));
        bus.write_long(sp + 8, 3);
        bus.write_long(sp + 12, 0);
        let put_new = disp.dispatch_toolbox(true, 0x1FE, &mut cpu, &mut bus);
        assert!(put_new.unwrap().is_ok());

        let offset_ptr = bus.alloc(4);
        let h_dest = bus.alloc(4);
        bus.write_long(h_dest, 0);
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, offset_ptr);
        bus.write_long(sp + 4, u32::from_be_bytes(*b"TEXT"));
        bus.write_long(sp + 8, h_dest);
        bus.write_long(sp + 12, 0xDEAD_BEEF);
        let get = disp.dispatch_toolbox(true, 0x1FD, &mut cpu, &mut bus);
        assert!(get.is_some());
        assert!(get.unwrap().is_ok());

        assert_eq!(bus.read_long(sp + 12), 3);
        assert_eq!(bus.read_long(offset_ptr), 8);
        let data_ptr = bus.read_long(h_dest);
        assert_ne!(data_ptr, 0);
        let bytes = [
            bus.read_byte(data_ptr),
            bus.read_byte(data_ptr + 1),
            bus.read_byte(data_ptr + 2),
        ];
        assert_eq!(&bytes, b"OLD");
    }

    // Inside Macintosh Volume I, I-459: given an existing minimum-size
    // handle, GetScrap resizes it to hold the copied bytes and leaves the
    // copied block owned by that same handle.
    #[test]
    fn getscrap_existing_handle_resizes_copy_and_preserves_ownership() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_long(sp, 0);
        let zero = disp.dispatch_toolbox(true, 0x1FC, &mut cpu, &mut bus);
        assert!(zero.unwrap().is_ok());

        let text_src = bus.alloc(3);
        bus.write_bytes(text_src, b"OLD");
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, text_src);
        bus.write_long(sp + 4, u32::from_be_bytes(*b"TEXT"));
        bus.write_long(sp + 8, 3);
        bus.write_long(sp + 12, 0);
        let put = disp.dispatch_toolbox(true, 0x1FE, &mut cpu, &mut bus);
        assert!(put.unwrap().is_ok());

        cpu.write_reg(Register::D0, 1);
        let new_handle = disp.dispatch_memory(false, 0x22, &mut cpu, &mut bus);
        assert!(new_handle.is_some());
        assert!(new_handle.unwrap().is_ok());
        let h_dest = cpu.read_reg(Register::A0);
        let old_ptr = bus.read_long(h_dest);
        assert_ne!(old_ptr, 0);
        bus.write_byte(old_ptr, b'Z');

        let offset_ptr = bus.alloc(4);
        bus.write_long(offset_ptr, 0xFFFF_FFFF);
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, offset_ptr);
        bus.write_long(sp + 4, u32::from_be_bytes(*b"TEXT"));
        bus.write_long(sp + 8, h_dest);
        bus.write_long(sp + 12, 0xDEAD_BEEF);
        let get = disp.dispatch_toolbox(true, 0x1FD, &mut cpu, &mut bus);
        assert!(get.is_some());
        assert!(get.unwrap().is_ok());

        let data_ptr = bus.read_long(h_dest);
        assert_ne!(data_ptr, 0);
        assert_eq!(bus.read_long(sp + 12), 3);
        assert_eq!(bus.read_long(offset_ptr), 8);
        assert_eq!(bus.get_alloc_size(data_ptr), Some(3));
        assert_eq!(bus.read_bytes(data_ptr, 3), b"OLD");

        cpu.write_reg(Register::A0, data_ptr);
        let recover = disp.dispatch_memory(false, 0x28, &mut cpu, &mut bus);
        assert!(recover.is_some());
        assert!(recover.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A0), h_dest);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    // Inside Macintosh Volume I, I-458: UnloadScrap/LoadScrap return noErr on
    // success and the ScrapStuff record reflects the resident/on-disk state.
    #[test]
    fn unloadscrap_and_loadscrap_return_noerr() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_long(sp, 0);
        let zero = disp.dispatch_toolbox(true, 0x1FC, &mut cpu, &mut bus);
        assert!(zero.is_some());
        assert!(zero.unwrap().is_ok());

        let source = bus.alloc(3);
        bus.write_bytes(source, b"ABC");
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, source);
        bus.write_long(sp + 4, u32::from_be_bytes(*b"TEXT"));
        bus.write_long(sp + 8, 3);
        bus.write_long(sp + 12, 0);
        let put = disp.dispatch_toolbox(true, 0x1FE, &mut cpu, &mut bus);
        assert!(put.is_some());
        assert!(put.unwrap().is_ok());

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0);
        let info_before = disp.dispatch_toolbox(true, 0x1F9, &mut cpu, &mut bus);
        assert!(info_before.is_some());
        assert!(info_before.unwrap().is_ok());
        let scrap_info_before = bus.read_long(sp);
        let scrap_handle_before = bus.read_long(scrap_info_before + 4);
        assert_ne!(scrap_handle_before, 0);
        assert_eq!(bus.read_word(scrap_info_before + 10), 1);
        assert_eq!(bus.read_long(scrap_info_before), 12);
        assert_eq!(
            bus.get_alloc_size(bus.read_long(scrap_handle_before)),
            Some(12)
        );

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0xDEAD_BEEF);
        let unload = disp.dispatch_toolbox(true, 0x1FA, &mut cpu, &mut bus);
        assert!(unload.is_some());
        assert!(unload.unwrap().is_ok());
        assert_eq!(bus.read_long(sp), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp);

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0);
        let info_after_unload = disp.dispatch_toolbox(true, 0x1F9, &mut cpu, &mut bus);
        assert!(info_after_unload.is_some());
        assert!(info_after_unload.unwrap().is_ok());
        let scrap_info_after_unload = bus.read_long(sp);
        assert_eq!(bus.read_long(scrap_info_after_unload), 12);
        assert_eq!(bus.read_long(scrap_info_after_unload + 4), 0);
        assert_eq!(bus.read_word(scrap_info_after_unload + 10), 0);

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0xDEAD_BEEF);
        let load = disp.dispatch_toolbox(true, 0x1FB, &mut cpu, &mut bus);
        assert!(load.is_some());
        assert!(load.unwrap().is_ok());
        assert_eq!(bus.read_long(sp), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp);

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0);
        let info_after_load = disp.dispatch_toolbox(true, 0x1F9, &mut cpu, &mut bus);
        assert!(info_after_load.is_some());
        assert!(info_after_load.unwrap().is_ok());
        let scrap_info_after_load = bus.read_long(sp);
        let scrap_handle_after_load = bus.read_long(scrap_info_after_load + 4);
        assert_ne!(scrap_handle_after_load, 0);
        assert_eq!(bus.read_long(scrap_info_after_load), 12);
        assert_eq!(bus.read_word(scrap_info_after_load + 10), 1);
        assert_eq!(
            bus.get_alloc_size(bus.read_long(scrap_handle_after_load)),
            Some(12)
        );
    }

    // Inside Macintosh Volume I, I-458: if the clipboard destination is not
    // writable, UnloadScrap can fail without dropping the resident scrap.
    #[test]
    fn unloadscrap_returns_error_and_keeps_scrap_resident_when_clipboard_unwritable() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        disp.scrap_clipboard_writable = false;

        bus.write_long(sp, 0);
        let zero = disp.dispatch_toolbox(true, 0x1FC, &mut cpu, &mut bus);
        assert!(zero.is_some());
        assert!(zero.unwrap().is_ok());

        let source = bus.alloc(3);
        bus.write_bytes(source, b"ABC");
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, source);
        bus.write_long(sp + 4, u32::from_be_bytes(*b"TEXT"));
        bus.write_long(sp + 8, 3);
        bus.write_long(sp + 12, 0);
        let put = disp.dispatch_toolbox(true, 0x1FE, &mut cpu, &mut bus);
        assert!(put.is_some());
        assert!(put.unwrap().is_ok());

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0);
        let info_before = disp.dispatch_toolbox(true, 0x1F9, &mut cpu, &mut bus);
        assert!(info_before.is_some());
        assert!(info_before.unwrap().is_ok());
        let scrap_info_before = bus.read_long(sp);
        let scrap_handle_before = bus.read_long(scrap_info_before + 4);
        assert_ne!(scrap_handle_before, 0);
        assert_eq!(bus.read_word(scrap_info_before + 10), 1);

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0xDEAD_BEEF);
        let unload = disp.dispatch_toolbox(true, 0x1FA, &mut cpu, &mut bus);
        assert!(unload.is_some());
        assert!(unload.unwrap().is_ok());
        assert_ne!(bus.read_long(sp), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp);

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0);
        let info_after_unload = disp.dispatch_toolbox(true, 0x1F9, &mut cpu, &mut bus);
        assert!(info_after_unload.is_some());
        assert!(info_after_unload.unwrap().is_ok());
        let scrap_info_after_unload = bus.read_long(sp);
        assert_eq!(
            bus.read_long(scrap_info_after_unload + 4),
            scrap_handle_before
        );
        assert_eq!(bus.read_word(scrap_info_after_unload + 10), 1);
        assert_eq!(bus.read_long(scrap_info_after_unload), 12);
    }

    // Inside Macintosh Volume I (1985), p. I-458: LoadScrap is a 0-arg Tool
    // Trap FUNCTION returning LONGINT (OSStatus) via the Pascal function
    // result slot at [SP+0]; the documented "already in memory" success
    // path returns noErr without consuming caller stack bytes. Mirrors B1
    // of the a9fb_loadscrap_strict catalog bake fixture: pre-poisons the
    // 4-byte result slot at SP+0 with a non-zero sentinel and asserts the
    // trap overwrites it with 0 (noErr) while leaving A7 untouched.
    #[test]
    fn loadscrap_writes_noerr_to_pascal_function_result_slot_and_preserves_stack_pointer() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_long(sp, 0xCAFE_F00D);
        bus.write_long(sp + 4, 0xBAAD_F00D);

        let result = disp.dispatch_toolbox(true, 0x1FB, &mut cpu, &mut bus);
        assert!(result.is_some(), "LoadScrap should be handled");
        assert!(result.unwrap().is_ok(), "LoadScrap should return cleanly");

        assert_eq!(
            bus.read_long(sp),
            0,
            "LoadScrap should write noErr to the 4-byte Pascal FUNCTION result slot at [SP+0]"
        );
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp,
            "LoadScrap should leave A7 unchanged (0-arg Tool Trap function)"
        );
        assert_eq!(
            bus.read_long(sp + 4),
            0xBAAD_F00D,
            "LoadScrap should not write past the 4-byte result slot"
        );
    }

    // ========== Printing Manager ==========

    #[test]
    fn prglue_selector_param_byte_count_controls_stack_pop() {
        // Inside Macintosh Volume V (1986), p. V-408:
        // _PrGlue selectors encode parameter-byte count in bits 15-8.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_long(sp, 0xF100_0600); // routine=$F1, result=0, params=6
        bus.write_long(sp + 4, 0xDEAD_BEEF);
        bus.write_word(sp + 8, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
    }

    #[test]
    fn prglue_propendoc_returns_nil_and_consumes_three_pointer_arguments() {
        // Inside Macintosh Volume V (1986), p. V-408:
        // PrOpenDoc selector is $04000C00 and signature takes 12 bytes of
        // arguments (THPrint, TPPrPort, Ptr) and returns TPPrPort.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_long(sp, 0x0400_0C00);
        bus.write_long(sp + 4, 0x1000_2000);
        bus.write_long(sp + 8, 0x2000_3000);
        bus.write_long(sp + 12, 0x3000_4000);
        bus.write_long(sp + 16, 0xFFFF_FFFF); // result slot placeholder

        let result = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 16), 0);
        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 16);
    }

    #[test]
    fn prglue_prvalidate_returns_false_boolean_result() {
        // Inside Macintosh Volume V (1986), p. V-408:
        // PrValidate selector is $52040498 and returns a BOOLEAN.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_long(sp, 0x5204_0498);
        bus.write_long(sp + 4, 0x1234_5678); // hPrint
        bus.write_word(sp + 8, 0xFFFF); // result placeholder

        let result = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 8), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
    }

    #[test]
    fn prglue_prstldialog_returns_true_boolean_result() {
        // Inside Macintosh Volume V (1986), p. V-408:
        // PrStlDialog selector is $2A040484 and returns a BOOLEAN.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_long(sp, 0x2A04_0484);
        bus.write_long(sp + 4, 0x1234_5678); // hPrint
        bus.write_word(sp + 8, 0); // result placeholder

        let result = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 8), 1);
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
    }

    #[test]
    fn prglue_prjobdialog_returns_true_boolean_result() {
        // Inside Macintosh Volume V (1986), p. V-408:
        // PrJobDialog selector is $32040488 and returns a BOOLEAN.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_long(sp, 0x3204_0488);
        bus.write_long(sp + 4, 0x1234_5678); // hPrint
        bus.write_word(sp + 8, 0); // result placeholder

        let result = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 8), 1);
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
    }

    #[test]
    fn prglue_prclosedoc_consumes_tpprport_argument_without_function_result_slot() {
        // Inside Macintosh Volume II (1985), p. II-160; Inside Macintosh
        // Volume V (1986), p. V-408:
        // PrCloseDoc takes one TPPrPort argument and returns no result.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_long(sp, 0x0800_0484);
        bus.write_long(sp + 4, 0x0000_0000); // pPrPort = NIL
        bus.write_long(sp + 8, 0xCAFE_BABE); // sentinel past the argument frame

        let result = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
        assert_eq!(bus.read_long(sp + 8), 0xCAFE_BABE);
    }

    #[test]
    fn prglue_printdefault_consumes_thprint_without_function_result_slot() {
        // Inside Macintosh: Imaging With QuickDraw (1994), p. 9-60:
        // PrintDefault is PROCEDURE PrintDefault(hPrint) with selector
        // $20040480. The selector's second byte is not a stack-result size.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_long(sp, 0x2004_0480);
        bus.write_long(sp + 4, 0x0010_2000); // hPrint
        bus.write_long(sp + 8, 0xCAFE_BABE); // saved-frame sentinel

        let result = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
        assert_eq!(bus.read_long(sp + 8), 0xCAFE_BABE);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    #[test]
    fn prglue_propen_and_prclose_selector_pops_selector_long() {
        // Inside Macintosh Volume II (1985), p. II-151:
        // PrOpen and PrClose are procedures with no stack arguments,
        // but the raw selector dispatch still consumes the selector
        // long pushed for _PrGlue.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_long(sp, 0xC800_0000);
        let open = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
        assert!(open.is_some());
        assert!(open.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0xD000_0000);
        let close = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
        assert!(close.is_some());
        assert!(close.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
    }

    #[test]
    fn prglue_propen_and_prclose_preserve_print_error_state() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_long(sp, 0xC000_0200);
        bus.write_word(sp + 4, 0x1357);
        let set_error = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
        assert!(set_error.is_some());
        assert!(set_error.unwrap().is_ok());

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0xC800_0000);
        let open = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
        assert!(open.is_some());
        assert!(open.unwrap().is_ok());

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0xD000_0000);
        let close = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
        assert!(close.is_some());
        assert!(close.unwrap().is_ok());

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0xBA00_0000);
        bus.write_word(sp + 4, 0);
        let error = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
        assert!(error.is_some());
        assert!(error.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 4), 0x1357);
    }

    #[test]
    fn prglue_prerror_returns_noerr_word_with_zero_result_bits_selector() {
        // Inside Macintosh Volume V (1986), p. V-408:
        // PrError selector is $BA000000 and returns INTEGER from the
        // Printing Manager error state.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_long(sp, 0xBA00_0000);
        bus.write_word(sp + 4, 0xFFFF); // result placeholder

        let result = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 4), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
    }

    #[test]
    fn prglue_prseterror_consumes_ierr_word_without_function_result_slot() {
        // Inside Macintosh Volume V (1986), p. V-408:
        // PrSetError selector is $C0000200 and takes one INTEGER argument.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_long(sp, 0xC000_0200);
        bus.write_word(sp + 4, 0xABCD); // iErr
        bus.write_word(sp + 6, 0xCAFE); // sentinel after arguments

        let result = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 6), 0xCAFE);
        assert_eq!(cpu.read_reg(Register::A7), sp + 6);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    #[test]
    fn prglue_prseterror_updates_prerror_state_roundtrip() {
        // Inside Macintosh Volume II (1985), p. II-161: PrSetError
        // stores the shared PrintErr result code, and PrError returns it.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        // First set a non-zero error and verify PrError reports it.
        bus.write_long(sp, 0xC000_0200);
        bus.write_word(sp + 4, 0x1234);
        let set_nonzero = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
        assert!(set_nonzero.is_some());
        assert!(set_nonzero.unwrap().is_ok());

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0xBA00_0000);
        bus.write_word(sp + 4, 0xFFFF);
        let get_nonzero = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
        assert!(get_nonzero.is_some());
        assert!(get_nonzero.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 4), 0x1234);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);

        // Reset to noErr and verify PrError follows the cleared state.
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0xC000_0200);
        bus.write_word(sp + 4, 0x0000);
        let set_zero = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
        assert!(set_zero.is_some());
        assert!(set_zero.unwrap().is_ok());

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0xBA00_0000);
        bus.write_word(sp + 4, 0xFFFF);
        let get_zero = disp.dispatch_toolbox(true, 0x0FD, &mut cpu, &mut bus);
        assert!(get_zero.is_some());
        assert!(get_zero.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 4), 0x0000);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
    }

    // ========== Utility Traps ==========

    #[test]
    fn bitshift_large_positive_count_zeroes_on_basiliskii() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_word(sp, 36);
        bus.write_long(sp + 2, 1);
        bus.write_long(sp + 6, 0xBEEFBEEF);

        let result = disp.dispatch_toolbox(true, 0x05C, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_long(sp + 6), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 6);
    }

    // Random ($A861) — seed=12345
    #[test]
    fn test_random() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        // globals_ptr is at bus.read_long(A5) = bus.read_long(0x180000) = 0x180004
        // randSeed is at globals_ptr - 126 = 0x180004 - 126 = 0x17FF86
        let seed_addr = 0x180004u32.wrapping_sub(126);
        bus.write_long(seed_addr, 12345);

        let result = disp.dispatch_toolbox(true, 0x061, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        // Park-Miller: new_seed = (12345 * 16807) % 2147483647 = 207482415
        let new_seed = bus.read_long(seed_addr);
        assert_eq!(new_seed, 207482415);
        // Result = low 16 bits of new_seed = 207482415 & 0xFFFF = 36399
        let result_word = bus.read_word(sp);
        assert_eq!(result_word, 207482415u32 as u16); // 36399
                                                      // SP unchanged
        assert_eq!(cpu.read_reg(Register::A7), sp);
    }

    // HiWord ($A86A) / LoWord ($A86B)
    // IM:I I-472 and OS Utils 1994 p.3-18: extract high/low word from LONGINT.
    #[test]
    fn hiword_returns_high_order_word_and_pops_long_argument() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_long(sp, 0x89ABCDEF);
        bus.write_word(sp + 4, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x06A, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 4), 0x89AB);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
    }

    #[test]
    fn loword_returns_low_order_word_and_pops_long_argument() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_long(sp, 0x89ABCDEF);
        bus.write_word(sp + 4, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x06B, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 4), 0xCDEF);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
    }

    // Random ($A861)
    // IM:I I-194 and OS Utils 1994 p.3-37: result depends solely on randSeed,
    // and the pseudorandom sequence is repeatable when randSeed is reset.
    #[test]
    fn random_reseeding_to_same_value_repeats_sequence_head() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let seed_addr = 0x180004u32.wrapping_sub(126);

        bus.write_long(seed_addr, 1);
        let first = disp.dispatch_toolbox(true, 0x061, &mut cpu, &mut bus);
        assert!(first.is_some());
        assert!(first.unwrap().is_ok());
        let first_result = bus.read_word(sp);
        let first_seed = bus.read_long(seed_addr);
        assert_eq!(first_seed, 16807);

        let second = disp.dispatch_toolbox(true, 0x061, &mut cpu, &mut bus);
        assert!(second.is_some());
        assert!(second.unwrap().is_ok());
        let second_result = bus.read_word(sp);
        assert_ne!(second_result, first_result);

        bus.write_long(seed_addr, 1);
        let replay = disp.dispatch_toolbox(true, 0x061, &mut cpu, &mut bus);
        assert!(replay.is_some());
        assert!(replay.unwrap().is_ok());
        assert_eq!(bus.read_word(sp), first_result);
        assert_eq!(bus.read_long(seed_addr), first_seed);
    }

    // IM:I I-194 says Random returns -32767..32767 (not -32768).
    #[test]
    fn random_maps_minus_32768_slot_to_zero() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let seed_addr = 0x180004u32.wrapping_sub(126);

        // Chosen so Park-Miller update produces a low word of 0x8000.
        bus.write_long(seed_addr, 32768);

        let result = disp.dispatch_toolbox(true, 0x061, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_long(seed_addr), 550731776);
        assert_eq!(bus.read_word(sp), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp);
    }

    // NOTE: The former test_pt_to_angle_cardinal_directions test exercised
    // a duplicate PtToAngle stub in toolbox.rs that popped 16 bytes and
    // treated Rect as an inline record. The canonical PtToAngle now lives
    // exclusively in quickdraw.rs (dispatch_quickdraw arm 0x0C3) with the
    // correct 12-byte frame (rect passed by pointer). Its contract tests
    // are in pttoangle_*.

    // GetIndString ($A9E6)
    #[test]
    fn test_get_ind_string() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let str_ptr = 0x200000u32;
        // SP+0: ...(4 bytes), SP+4: str_ptr(4)
        bus.write_long(sp, 0); // first 4 bytes (index, resID, etc.)
        bus.write_long(sp + 4, str_ptr);
        // Write non-zero to str_ptr to verify it gets cleared
        bus.write_byte(str_ptr, 0xFF);

        let result = disp.dispatch_toolbox(true, 0x1E6, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_byte(str_ptr), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
    }

    // Inside Macintosh Volume I (1985), p. I-468: GetIndString reads the
    // indexed string from a STR# resource via GetResource. A successful hit
    // must therefore copy the Pascal-string body and clear stale ResErr.
    #[test]
    fn get_ind_string_present_resource_copies_indexed_string_and_clears_reserr() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let str_ptr = 0x200120u32;
        let data_ptr = bus.alloc(16);

        // STR# layout: count=2, then PString "ONE", then PString "TWO".
        bus.write_word(data_ptr, 2);
        bus.write_byte(data_ptr + 2, 3);
        bus.write_bytes(data_ptr + 3, b"ONE");
        bus.write_byte(data_ptr + 6, 3);
        bus.write_bytes(data_ptr + 7, b"TWO");

        disp.resources = Some(LoadedResources {
            files: HashMap::from([
                (0, ResourceFileMap::default()),
                (
                    2,
                    ResourceFileMap {
                        loaded: HashMap::from([((*b"STR#", 128i16), data_ptr)]),
                        named: HashMap::new(),
                        names_by_id: HashMap::new(),
                        attrs: HashMap::new(),
                        map_attrs: 0,
                    },
                ),
            ]),
            names: HashMap::new(),
            search_order: vec![0, 2],
            current_file: 2,
        });

        bus.write_byte(str_ptr, 0xFF);
        bus.write_word(0x0A60, 0xBEEF);
        bus.write_word(sp, 2); // index
        bus.write_word(sp + 2, 128u16); // STR# id
        bus.write_long(sp + 4, str_ptr);

        let result = disp.dispatch_toolbox(true, 0x1E6, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_pstring(str_ptr), b"TWO");
        assert_eq!(bus.read_word(0x0A60), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
    }

    // ========== Sound Manager ==========

    // SndPlay ($A805)
    #[test]
    fn test_snd_play() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        // SP+0: async(2), SP+2: sndHdl(4), SP+6: chan(4), SP+10: result(2)
        bus.write_word(sp, 0); // async
        bus.write_long(sp + 2, 0); // sndHdl (nil = no sound to play)
        bus.write_long(sp + 6, 0); // chan
        bus.write_word(sp + 10, 0xBEEF); // result placeholder

        let result = disp.dispatch_sound(true, 0x005, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 10), (-204i16) as u16); // resProblem
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
    }

    #[test]
    fn test_snd_play_nil_chan_async_true_reclaims_internal_channel() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let snd_handle = 0x200000u32;
        let snd_ptr = 0x200100u32;

        // Minimal format 2 'snd ' resource:
        //   +0 format=2, +2 refCount=0, +4 numCommands=0.
        bus.write_long(snd_handle, snd_ptr);
        bus.write_word(snd_ptr, 2);
        bus.write_word(snd_ptr + 2, 0);
        bus.write_word(snd_ptr + 4, 0);

        // IM:Sound 1994 p.2-122: if chan is NIL, async is ignored and play
        // is synchronous (must not fail solely because async=TRUE).
        bus.write_word(sp, 0xFFFF); // async = TRUE
        bus.write_long(sp + 2, snd_handle);
        bus.write_long(sp + 6, 0); // chan = NIL
        bus.write_word(sp + 10, 0xBEEF);

        let result = disp.dispatch_sound(true, 0x005, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 10), 0); // noErr
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
        assert!(disp.sound_manager.channels.is_empty());
    }

    #[test]
    fn test_snd_play_unloaded_handle_returns_resproblem() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let snd_handle = 0x200000u32;

        // Handle master pointer is NIL => resource is not loaded.
        // IM:Sound 1994 p.2-122 says SndPlay returns resProblem (-204).
        bus.write_long(snd_handle, 0);
        bus.write_word(sp, 0); // async = FALSE
        bus.write_long(sp + 2, snd_handle);
        bus.write_long(sp + 6, 0);
        bus.write_word(sp + 10, 0xBEEF);

        let result = disp.dispatch_sound(true, 0x005, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 10), (-204i16) as u16); // resProblem
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
        assert!(disp.sound_manager.channels.is_empty());
    }

    #[test]
    fn test_snd_play_invalid_resource_format_returns_badformat() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let snd_handle = 0x200000u32;
        let snd_ptr = 0x200100u32;

        bus.write_long(snd_handle, snd_ptr);
        bus.write_word(snd_ptr, 3); // unsupported format (valid values: 1 or 2)
        bus.write_word(sp, 0); // async = FALSE
        bus.write_long(sp + 2, snd_handle);
        bus.write_long(sp + 6, 0);
        bus.write_word(sp + 10, 0xBEEF);

        let result = disp.dispatch_sound(true, 0x005, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        // IM:Sound 1994 p.2-123 result codes: malformed/corrupt resource -> badFormat.
        assert_eq!(bus.read_word(sp + 10), (-206i16) as u16); // badFormat
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
        assert!(disp.sound_manager.channels.is_empty());
    }

    // SysBeep ($A9C8)
    #[test]
    fn test_sys_beep() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_word(sp, 30); // duration param

        let result = disp.dispatch_sound(true, 0x1C8, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
    }

    // SystemClick ($A9B3)
    #[test]
    fn systemclick_consumes_eventrecord_and_windowptr_arguments() {
        // IM:I 1985, p. I-441: SystemClick(theEvent: EventRecord; theWindow: WindowPtr)
        // IM:I 1985, p. I-251: EventRecord layout totals 16 bytes.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        // 16-byte EventRecord by value + 4-byte WindowPtr.
        for i in 0..16u32 {
            bus.write_byte(sp + i, (0x40u8).wrapping_add(i as u8));
        }
        bus.write_long(sp + 16, 0x00AB_CDEF);
        bus.write_word(sp + 20, 0xBEEF); // sentinel after argument frame

        let result = disp.dispatch_toolbox(true, 0x1B3, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 20);
        assert_eq!(
            bus.read_word(sp + 20),
            0xBEEF,
            "SystemClick must pop exactly 20 bytes and not overwrite trailing stack memory"
        );
    }

    #[test]
    fn systemclick_repeated_calls_preserve_stack_and_sentinel() {
        // Repeating the direct trap call catches cumulative stack drift
        // that a single call could miss if the ABI were off by a byte.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        for i in 0..16u32 {
            bus.write_byte(sp + i, (0x50u8).wrapping_add(i as u8));
        }
        bus.write_long(sp + 16, 0x1020_3040);
        bus.write_word(sp + 20, 0xC0DE);

        for _ in 0..5 {
            cpu.write_reg(Register::A7, sp);
            let result = disp.dispatch_toolbox(true, 0x1B3, &mut cpu, &mut bus);
            assert!(result.is_some());
            assert!(result.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), sp + 20);
            assert_eq!(bus.read_word(sp + 20), 0xC0DE);
        }
    }

    // SystemTask ($A9B4)
    #[test]
    fn systemtask_procedure_call_preserves_stack_pointer() {
        // IM:I 1985, p. I-440: PROCEDURE SystemTask;
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);

        let result = disp.dispatch_toolbox(true, 0x1B4, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_before,
            "SystemTask is a no-argument procedure and must preserve A7"
        );
    }

    // SystemTask ($A9B4) — mirrors B1 of a9b4_a9c2_systemtask_systemedit_strict
    // (5-call composition catches per-call drift a single sandwich might mask).
    #[test]
    fn systemtask_five_call_composition_preserves_stack_pointer() {
        // IM:I 1985, p. I-440: PROCEDURE SystemTask;
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);

        for _ in 0..5 {
            let result = disp.dispatch_toolbox(true, 0x1B4, &mut cpu, &mut bus);
            assert!(result.is_some());
            assert!(result.unwrap().is_ok());
        }
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_before,
            "SystemTask must preserve A7 across a 5-call composition"
        );
    }

    // OpenDeskAcc ($A9B6)
    #[test]
    fn opendeskacc_consumes_name_pointer_and_returns_zero_refnum_in_result_slot() {
        // IM:I 1985, p. I-440: FUNCTION OpenDeskAcc(theAcc: Str255): INTEGER;
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let name_ptr = 0x240000u32;
        bus.write_pstring(name_ptr, b"Calculator");
        bus.write_long(sp, name_ptr);
        bus.write_word(sp + 4, 0xBEEF); // INTEGER function-result slot
        bus.write_word(sp + 6, 0xCAFE); // trailing sentinel

        let result = disp.dispatch_toolbox(true, 0x1B6, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
        assert_eq!(
            bus.read_word(sp + 4),
            0,
            "OpenDeskAcc HLE path writes 0 into the INTEGER result slot"
        );
        assert_eq!(
            bus.read_word(sp + 6),
            0xCAFE,
            "OpenDeskAcc must pop only the 4-byte name pointer argument"
        );
    }

    // OpenDeskAcc ($A9B6) — mirrors B1 of a9b6_a9b7_opendeskacc_closedeskacc_strict
    // (5-call composition: net stack effect after each Pascal FUNCTION
    // call's epilogue is zero, so A7 returns to its pre-composition value).
    #[test]
    fn opendeskacc_five_call_composition_preserves_stack_pointer() {
        // IM:I 1985, p. I-440: FUNCTION OpenDeskAcc(theAcc: Str255): INTEGER;
        let (mut disp, mut cpu, mut bus) = setup();
        let name_ptr = 0x240000u32;
        bus.write_pstring(name_ptr, b"NoSuchDA_A9B6!");

        // Each call: caller pushes 2-byte result placeholder + 4-byte name
        // ptr, dispatches, trap pops the 4-byte arg + writes the 2-byte
        // result slot, then caller pops the 2-byte result slot.
        let sp_before = cpu.read_reg(Register::A7);
        for _ in 0..5 {
            let call_sp = cpu.read_reg(Register::A7);
            bus.write_long(call_sp.wrapping_sub(4), name_ptr);
            bus.write_word(call_sp.wrapping_sub(6), 0xBEEF);
            cpu.write_reg(Register::A7, call_sp.wrapping_sub(6));

            let result = disp.dispatch_toolbox(true, 0x1B6, &mut cpu, &mut bus);
            assert!(result.is_some());
            assert!(result.unwrap().is_ok());

            // Caller's epilogue pops the 2-byte INTEGER result slot.
            let post_sp = cpu.read_reg(Register::A7);
            cpu.write_reg(Register::A7, post_sp.wrapping_add(2));
        }
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_before,
            "OpenDeskAcc Pascal FUNCTION calling convention must preserve A7 across a 5-call composition"
        );
    }

    // CloseDeskAcc ($A9B7) — mirrors B2 of a9b6_a9b7_opendeskacc_closedeskacc_strict
    #[test]
    fn closedeskacc_consumes_refnum_arg_and_writes_no_result() {
        // IM:I 1985, p. I-440: PROCEDURE CloseDeskAcc(refNum: INTEGER);
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_word(sp, 0); // refNum=0 (clearly invalid — no-op path)
        bus.write_word(sp + 2, 0xCAFE); // trailing sentinel

        let result = disp.dispatch_toolbox(true, 0x1B7, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
        assert_eq!(
            bus.read_word(sp + 2),
            0xCAFE,
            "CloseDeskAcc must pop exactly 2 bytes and not overwrite trailing stack memory"
        );
    }

    // CloseDeskAcc ($A9B7) — mirrors B2 of a9b6_a9b7_opendeskacc_closedeskacc_strict
    // (5-call composition: each call pops 2 bytes so A7 advances by 10).
    #[test]
    fn closedeskacc_five_call_composition_advances_stack_by_ten() {
        // IM:I 1985, p. I-440: PROCEDURE CloseDeskAcc(refNum: INTEGER);
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);

        // Caller pre-pushes 5 × 2-byte refNum=0 arguments.
        for i in 0..5u32 {
            bus.write_word(sp_before.wrapping_sub(10 - 2 * i), 0);
        }
        cpu.write_reg(Register::A7, sp_before.wrapping_sub(10));

        for _ in 0..5 {
            let result = disp.dispatch_toolbox(true, 0x1B7, &mut cpu, &mut bus);
            assert!(result.is_some());
            assert!(result.unwrap().is_ok());
        }
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_before,
            "CloseDeskAcc must pop exactly 2 bytes per call (Pascal PROCEDURE)"
        );
    }

    // SystemMenu ($A9B5) — mirrors B1 of a9b5_systemmenu_strict (single call).
    // Per IM:I 1985, p. I-441: PROCEDURE SystemMenu(menuResult: LONGINT) pops
    // a 4-byte LONGINT argument and writes no result slot.
    #[test]
    fn systemmenu_procedure_call_pops_four_bytes_from_stack() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_long(sp, 0); // menuResult=0 (no DA matches menuID=0)
        bus.write_word(sp + 4, 0xCAFE); // trailing sentinel

        let result = disp.dispatch_toolbox(true, 0x1B5, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
        assert_eq!(
            bus.read_word(sp + 4),
            0xCAFE,
            "SystemMenu must pop exactly 4 bytes and not overwrite trailing stack memory"
        );
    }

    // SystemMenu ($A9B5) — mirrors B1 of a9b5_systemmenu_strict (5-call composition).
    // Each call pops 4 bytes so A7 advances by 20 after 5 dispatches.
    #[test]
    fn systemmenu_five_call_composition_advances_stack_by_twenty() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);

        // Caller pre-pushes 5 × 4-byte LONGINT menuResult=0 arguments.
        for i in 0..5u32 {
            bus.write_long(sp_before.wrapping_sub(20 - 4 * i), 0);
        }
        cpu.write_reg(Register::A7, sp_before.wrapping_sub(20));

        for _ in 0..5 {
            let result = disp.dispatch_toolbox(true, 0x1B5, &mut cpu, &mut bus);
            assert!(result.is_some());
            assert!(result.unwrap().is_ok());
        }
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_before,
            "SystemMenu must pop exactly 4 bytes per call (Pascal PROCEDURE pop-LONGINT)"
        );
    }

    // SystemEdit ($A9C2)
    #[test]
    fn systemedit_consumes_editcmd_and_returns_false_boolean_result() {
        // IM:I 1985, p. I-441:
        // FUNCTION SystemEdit(editCmd: INTEGER): BOOLEAN;
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_word(sp, 3); // copyCmd
        bus.write_word(sp + 2, 0xFFFF); // BOOLEAN result slot sentinel
        bus.write_word(sp + 4, 0xCAFE); // trailing sentinel

        let result = disp.dispatch_toolbox(true, 0x1C2, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
        assert_eq!(
            bus.read_word(sp + 2),
            0,
            "SystemEdit should return FALSE when no desk accessory handles edit commands"
        );
        assert_eq!(
            bus.read_word(sp + 4),
            0xCAFE,
            "SystemEdit must pop only the 2-byte editCmd argument"
        );
    }

    // SystemEdit ($A9C2) — mirrors B2 of a9b4_a9c2_systemtask_systemedit_strict
    // (FALSE return + 2-byte arg pop for every standard editCmd per IM:I I-441).
    #[test]
    fn systemedit_returns_false_for_every_standard_editcmd() {
        // IM:I 1985, p. I-441 table:
        //   0  undoCmd
        //   2  cutCmd
        //   3  copyCmd
        //   4  pasteCmd
        //   5  clearCmd
        // (1 is a historic gap.)
        for edit_cmd in [0u16, 2, 3, 4, 5] {
            let (mut disp, mut cpu, mut bus) = setup();
            let sp = TEST_SP;
            bus.write_word(sp, edit_cmd);
            bus.write_word(sp + 2, 0xFFFF); // BOOLEAN result slot sentinel
            bus.write_word(sp + 4, 0xCAFE); // trailing sentinel

            let result = disp.dispatch_toolbox(true, 0x1C2, &mut cpu, &mut bus);
            assert!(result.is_some());
            assert!(result.unwrap().is_ok());
            assert_eq!(
                cpu.read_reg(Register::A7),
                sp + 2,
                "SystemEdit(editCmd={edit_cmd}) must pop exactly 2 bytes"
            );
            assert_eq!(
                bus.read_word(sp + 2),
                0,
                "SystemEdit(editCmd={edit_cmd}) must return FALSE in the no-DA path"
            );
            assert_eq!(
                bus.read_word(sp + 4),
                0xCAFE,
                "SystemEdit(editCmd={edit_cmd}) must not overwrite trailing stack"
            );
        }
    }

    fn seed_synthetic_kchr(bus: &mut super::MacMemoryBus, trans_data: u32) {
        // Minimal KCHR layout:
        //   byte 0 = version
        //   bytes 1..=256 = table-selection index
        //   table 0 and table 1 = 128-byte character tables
        //
        // Modifier byte 0 selects table 0; modifier byte 1 selects table 1.
        bus.write_byte(trans_data, 0);
        for i in 0..256u32 {
            bus.write_byte(trans_data + 1 + i, 0);
        }
        bus.write_byte(trans_data + 1, 0);
        bus.write_byte(trans_data + 2, 1);

        let table0 = trans_data + 1 + 256;
        let table1 = table0 + 128;
        bus.write_byte(table0 + 2, b'Q');
        bus.write_byte(table0 + 3, b'W');
        bus.write_byte(table0 + 4, b'E');
        bus.write_byte(table0 + 5, b'R');
        bus.write_byte(table0 + 6, b'T');
        bus.write_byte(table1 + 2, b'Z');
        bus.write_byte(table1 + 3, b'X');
        bus.write_byte(table1 + 4, b'C');
        bus.write_byte(table1 + 5, b'V');
        bus.write_byte(table1 + 6, b'B');
    }

    // KeyTrans ($A9C3)
    #[test]
    fn keytrans_consumes_state_keycode_transdata_arguments_and_writes_long_result_slot() {
        // Inside Macintosh: Macintosh Toolbox Essentials (1992), p. 2-110:
        // FUNCTION KeyTranslate(transData: Ptr; keycode: Integer;
        //                       VAR state: LongInt): LongInt;
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let state_ptr = 0x230000u32;
        let trans_data = 0x240000u32;
        seed_synthetic_kchr(&mut bus, trans_data);
        bus.write_long(state_ptr, 0x1234_5678);
        bus.write_long(sp, state_ptr);
        bus.write_word(sp + 4, 0x0002); // virtual keycode 2
        bus.write_long(sp + 6, trans_data);
        bus.write_long(sp + 10, 0xDEAD_BEEF); // result slot sentinel
        bus.write_word(sp + 14, 0xCAFE); // trailing sentinel

        let result = disp.dispatch_toolbox(true, 0x1C3, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
        assert_eq!(
            bus.read_long(sp + 10),
            0x0000_0051,
            "KeyTrans should write the translated character at the result slot"
        );
        assert_eq!(
            bus.read_word(sp + 14),
            0xCAFE,
            "KeyTrans must pop exactly state/keycode/transData arguments (10 bytes)"
        );
        assert_eq!(
            bus.read_long(state_ptr),
            0,
            "KeyTrans should clear pending state on the nominal path"
        );
    }

    #[test]
    fn keytrans_single_character_result_uses_charcode2_low_byte() {
        // Inside Macintosh: Macintosh Toolbox Essentials (1992), p. 2-111:
        // when one character is returned, it is in Character code 2
        // (low byte) of the 32-bit result.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let state_ptr = 0x230100u32;
        let trans_data = 0x240100u32;
        seed_synthetic_kchr(&mut bus, trans_data);
        bus.write_long(state_ptr, 0);
        bus.write_long(sp, state_ptr);
        bus.write_word(sp + 4, 0x0102); // modifier byte 1, keycode 2
        bus.write_long(sp + 6, trans_data);
        bus.write_long(sp + 10, 0);

        let result = disp.dispatch_toolbox(true, 0x1C3, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        let translated = bus.read_long(sp + 10);
        assert_eq!(
            translated & 0x0000_00FF,
            0x5A,
            "single-character output should occupy Character code 2 (low byte)"
        );
        assert_eq!(
            translated & 0x00FF_0000,
            0,
            "Character code 1 byte should be zero when only one character is returned"
        );
    }

    #[test]
    fn keytrans_non_deadkey_path_clears_state_for_followup_calls() {
        // Inside Macintosh: Text (1993), C-19..C-20: state carries dead-key
        // context; nominal non-dead-key translation should leave no pending
        // state for the next call.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let state_ptr = 0x230200u32;
        let trans_data = 0x240200u32;
        seed_synthetic_kchr(&mut bus, trans_data);
        bus.write_long(state_ptr, 0xFFFF_0001); // stale nonzero value
        bus.write_long(sp, state_ptr);
        bus.write_word(sp + 4, 0x0003); // virtual keycode 3
        bus.write_long(sp + 6, trans_data);
        bus.write_long(sp + 10, 0);

        let result = disp.dispatch_toolbox(true, 0x1C3, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(
            bus.read_long(state_ptr),
            0,
            "KeyTrans nominal HLE path should clear pending dead-key state"
        );
        assert_eq!(
            bus.read_long(sp + 10),
            0x0000_0057,
            "KeyTrans should still return the translated character"
        );
    }

    // GetAppParms ($A9F5)
    #[test]
    fn getappparms_returns_curapname_curaprefnum_and_appparmhandle() {
        // IM:II 1985, p. II-58: GetAppParms returns CurApName, CurApRefNum,
        // and AppParmHandle through its three VAR output parameters.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let ap_param_out = 0x210000u32;
        let ap_refnum_out = 0x210100u32;
        let ap_name_out = 0x210200u32;

        bus.write_pstring(addr::CUR_APNAME, b"Marathon");
        bus.write_word(addr::CUR_APREF_NUM, (-6i16) as u16);
        bus.write_long(addr::APP_PARM_HANDLE, 0x00AB_CDEF);

        bus.write_long(ap_param_out, 0xDEAD_BEEF);
        bus.write_word(ap_refnum_out, 0xBEEF);
        bus.write_pstring(ap_name_out, b"XXXX");

        bus.write_long(sp, ap_param_out);
        bus.write_long(sp + 4, ap_refnum_out);
        bus.write_long(sp + 8, ap_name_out);

        let result = disp.dispatch_toolbox(true, 0x1F5, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 12);

        assert_eq!(bus.read_pstring(ap_name_out), b"Marathon".to_vec());
        assert_eq!(bus.read_word(ap_refnum_out), (-6i16) as u16);
        assert_eq!(bus.read_long(ap_param_out), 0x00AB_CDEF);
    }

    #[test]
    fn getappparms_consumes_three_var_pointer_arguments() {
        // IM:II 1985, p. II-58 signature:
        // GetAppParms(VAR Str255, VAR INTEGER, VAR Handle) -> 3 pointers.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_long(sp, 0);
        bus.write_long(sp + 4, 0);
        bus.write_long(sp + 8, 0);

        let result = disp.dispatch_toolbox(true, 0x1F5, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 12);
    }

    #[test]
    fn unloadseg_consumes_routineaddr_pointer_argument() {
        // IM:II 1985, p. II-58:
        // PROCEDURE UnloadSeg(routineAddr: Ptr);
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_long(sp, 0x00AB_CDEF);
        bus.write_word(sp + 4, 0xBEEF); // sentinel after pointer argument

        let result = disp.dispatch_toolbox(true, 0x1F1, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
        assert_eq!(bus.read_word(sp + 4), 0xBEEF);
    }

    #[test]
    fn unloadseg_noop_preserves_registered_segment_cache() {
        // Systemless's HLE keeps segment_map resident; UnloadSeg currently
        // contracts to pointer-pop + no mutation of registered segments.
        let (mut disp, mut cpu, mut bus) = setup();
        let seg_map = HashMap::from([(1i16, 0x0022_0000u32), (9i16, 0x0033_0000u32)]);
        disp.register_segments(seg_map.clone());

        let sp = TEST_SP;
        bus.write_long(sp, 0x0022_1000);
        let result = disp.dispatch_toolbox(true, 0x1F1, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
        assert_eq!(disp.segment_map, seg_map);
    }

    // ExitToShell ($A9F4)
    #[test]
    fn exittoshell_terminates_application_and_returns_halted_error() {
        // IM:II 1985, p. II-58: ExitToShell terminates the current app
        // and returns to the shell.
        let (mut disp, mut cpu, mut bus) = setup();

        let result = disp.dispatch_toolbox(true, 0x1F4, &mut cpu, &mut bus);
        assert!(result.is_some());
        let err = result.unwrap().unwrap_err();
        assert!(
            matches!(err, crate::Error::Halted),
            "ExitToShell should return Error::Halted"
        );
    }

    #[test]
    fn exittoshell_procedure_signature_consumes_no_stack_arguments() {
        // IM:II 1985, p. II-58: PROCEDURE ExitToShell; (no arguments).
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);

        let result = disp.dispatch_toolbox(true, 0x1F4, &mut cpu, &mut bus);
        assert!(result.is_some());
        let err = result.unwrap().unwrap_err();
        assert!(matches!(err, crate::Error::Halted));
        assert_eq!(cpu.read_reg(Register::A7), sp_before);
    }

    #[test]
    fn exittoshell_halted_path_preserves_d0_and_a7() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);
        cpu.write_reg(Register::D0, 0x1234_5678);

        let result = disp.dispatch_toolbox(true, 0x1F4, &mut cpu, &mut bus);
        assert!(result.is_some(), "ExitToShell should be handled");
        assert!(
            matches!(result.unwrap().unwrap_err(), crate::Error::Halted),
            "ExitToShell should return Error::Halted"
        );
        assert_eq!(cpu.read_reg(Register::D0), 0x1234_5678);
        assert_eq!(cpu.read_reg(Register::A7), sp_before);
    }

    #[test]
    fn launchapplication_launchcontinue_clear_records_target_app_path_and_halts() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);
        let launch_pb = bus.alloc(64);
        let app_spec = bus.alloc(32);
        let target_dir_id = disp.ensure_vfs_directory("LaunchTargets");

        cpu.write_reg(Register::A0, launch_pb);
        cpu.write_reg(Register::D0, 0x1234_5678);

        for offset in 0..64u32 {
            bus.write_byte(launch_pb + offset, 0);
        }
        bus.write_word(launch_pb + 6, 0x4C43); // extendedBlock
        bus.write_long(launch_pb + 8, 32); // extendedBlockLen
        bus.write_word(launch_pb + 12, 0);
        bus.write_word(launch_pb + 14, 0);
        bus.write_long(launch_pb + 16, app_spec);

        for offset in 0..32u32 {
            bus.write_byte(app_spec + offset, 0);
        }
        bus.write_word(app_spec, 0);
        bus.write_long(app_spec + 2, target_dir_id);
        write_pascal_string(&mut bus, app_spec + 6, "NoSuchApp");

        let result = disp.dispatch_toolbox(true, 0x1F2, &mut cpu, &mut bus);
        assert!(result.is_some(), "LaunchApplication should be handled");
        assert!(
            matches!(result.unwrap().unwrap_err(), crate::Error::Halted),
            "LaunchApplication should return Error::Halted"
        );
        assert_eq!(cpu.read_reg(Register::A0), launch_pb);
        assert_eq!(cpu.read_reg(Register::D0) as i32, -43);
        assert_eq!(cpu.read_reg(Register::A7), sp_before);
        assert_eq!(bus.read_long(launch_pb + 20), 0);
        assert_eq!(bus.read_long(launch_pb + 24), 0);
        assert_eq!(bus.read_long(launch_pb + 28), 0);
        assert_eq!(bus.read_long(launch_pb + 32), 0);
        assert_eq!(bus.read_long(launch_pb + 36), 0);
        assert_eq!(
            disp.launched_app_path.as_deref(),
            Some("LaunchTargets/NoSuchApp")
        );
        assert_eq!(disp.default_dir_id, target_dir_id);
        assert_ne!(disp.app_wd_refnum, 0);
    }

    #[test]
    fn launchapplication_launchcontinue_set_records_target_app_path_and_returns() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);
        let launch_pb = bus.alloc(64);
        let app_spec = bus.alloc(32);
        let target_dir_id = disp.ensure_vfs_directory("LaunchTargets");

        cpu.write_reg(Register::A0, launch_pb);
        cpu.write_reg(Register::D0, 0x1234_5678);

        for offset in 0..64u32 {
            bus.write_byte(launch_pb + offset, 0);
        }
        bus.write_word(launch_pb + 6, 0x4C43); // extendedBlock
        bus.write_long(launch_pb + 8, 32); // extendedBlockLen
        bus.write_word(launch_pb + 12, 0);
        bus.write_word(launch_pb + 14, 0x4000); // launchContinue
        bus.write_long(launch_pb + 16, app_spec);

        for offset in 0..32u32 {
            bus.write_byte(app_spec + offset, 0);
        }
        bus.write_word(app_spec, 0);
        bus.write_long(app_spec + 2, target_dir_id);
        write_pascal_string(&mut bus, app_spec + 6, "NoSuchApp");

        let result = disp.dispatch_toolbox(true, 0x1F2, &mut cpu, &mut bus);
        assert!(result.is_some(), "LaunchApplication should be handled");
        assert!(
            result.unwrap().is_ok(),
            "LaunchApplication should return when launchContinue is set"
        );
        assert_eq!(cpu.read_reg(Register::A0), launch_pb);
        assert_eq!(cpu.read_reg(Register::D0) as i32, -43);
        assert_eq!(cpu.read_reg(Register::A7), sp_before);
        assert_eq!(bus.read_long(launch_pb + 20), 0);
        assert_eq!(bus.read_long(launch_pb + 24), 0);
        assert_eq!(bus.read_long(launch_pb + 28), 0);
        assert_eq!(bus.read_long(launch_pb + 32), 0);
        assert_eq!(bus.read_long(launch_pb + 36), 0);
        assert_eq!(
            disp.launched_app_path.as_deref(),
            Some("LaunchTargets/NoSuchApp")
        );
        assert_eq!(disp.default_dir_id, target_dir_id);
        assert_ne!(disp.app_wd_refnum, 0);
    }

    #[test]
    fn launchapplication_existing_foreground_target_queues_runner_launch() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);
        let launch_pb = bus.alloc(64);
        let app_spec = bus.alloc(32);
        let target_dir_id = disp.ensure_vfs_directory("LaunchTargets");
        disp.vfs
            .insert("LaunchTargets/Register Helper".to_string(), Vec::new());

        cpu.write_reg(Register::A0, launch_pb);
        cpu.write_reg(Register::D0, 0x1234_5678);

        for offset in 0..64u32 {
            bus.write_byte(launch_pb + offset, 0);
        }
        bus.write_word(launch_pb + 6, 0x4C43); // extendedBlock
        bus.write_long(launch_pb + 8, 32); // extendedBlockLen
        bus.write_word(launch_pb + 12, 0);
        bus.write_word(launch_pb + 14, 0x4000); // launchContinue
        bus.write_long(launch_pb + 16, app_spec);

        for offset in 0..32u32 {
            bus.write_byte(app_spec + offset, 0);
        }
        bus.write_word(app_spec, 0);
        bus.write_long(app_spec + 2, target_dir_id);
        write_pascal_string(&mut bus, app_spec + 6, "Register Helper");

        let result = disp.dispatch_toolbox(true, 0x1F2, &mut cpu, &mut bus);
        assert!(result.is_some(), "LaunchApplication should be handled");
        assert!(
            result.unwrap().is_ok(),
            "LaunchApplication should return while queued foreground app waits for Event Manager yield"
        );
        assert_eq!(cpu.read_reg(Register::A0), launch_pb);
        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp_before);
        assert_eq!(
            disp.launched_app_path.as_deref(),
            Some("LaunchTargets/Register Helper")
        );
        assert!(
            disp.take_pending_launch_application(false).is_none(),
            "launchContinue target should not start until an Event Manager yield"
        );
        assert_eq!(
            disp.take_pending_launch_application(true).as_deref(),
            Some("LaunchTargets/Register Helper")
        );
    }

    #[test]
    fn chain_records_cmdline_path_and_curpageoption_before_halt() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);
        let cmd_line = bus.alloc(8);
        let app_name = bus.alloc(32);
        let target_dir_id = disp.ensure_vfs_directory("ChainTargets");

        disp.default_dir_id = target_dir_id;
        cpu.write_reg(Register::A0, cmd_line);
        cpu.write_reg(Register::D0, 0x1234_5678);

        for offset in 0..8u32 {
            bus.write_byte(cmd_line + offset, 0);
        }
        bus.write_long(cmd_line, app_name);
        bus.write_word(cmd_line + 4, 0x0001);

        write_pascal_string(&mut bus, app_name, "NoSuchApp");

        let result = disp.dispatch_toolbox(true, 0x1F3, &mut cpu, &mut bus);
        assert!(result.is_some(), "Chain should be handled");
        assert!(
            matches!(result.unwrap().unwrap_err(), crate::Error::Halted),
            "Chain should return Error::Halted"
        );
        assert_eq!(cpu.read_reg(Register::A0), cmd_line);
        assert_eq!(cpu.read_reg(Register::D0), 0x1234_5678);
        assert_eq!(cpu.read_reg(Register::A7), sp_before);
        assert_eq!(bus.read_word(0x0936), 0x0001);
        assert_eq!(
            disp.launched_app_path.as_deref(),
            Some("ChainTargets/NoSuchApp")
        );
        assert_eq!(disp.default_dir_id, target_dir_id);
        assert_ne!(disp.app_wd_refnum, 0);
    }

    // _SCSIDispatch ($A815)
    #[test]
    fn scsidispatch_selector_zero_returns_noerr_and_pops_selector_word() {
        // Inside Macintosh Volume IV (1986), pp. IV-287 to IV-300:
        // selector 0 (SCSIReset) is a word-selector dispatch entry.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);
        cpu.write_reg(Register::D0, 0x1234_5678);
        bus.write_word(sp_before + 2, 0xFFFF);
        bus.write_word(sp_before, 0);

        let result = disp.dispatch_toolbox(true, 0x015, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp_before + 2);
        assert_eq!(bus.read_word(sp_before + 2), 0);
        assert_eq!(cpu.read_reg(Register::D0), 0x1234_5678);
    }

    #[test]
    fn scsidispatch_selector_two_returns_noerr_and_pops_two_byte_argument_frame() {
        // Inside Macintosh Volume IV (1986), pp. IV-287 to IV-300:
        // selector 2 (SCSISelect) consumes its selector word plus one
        // 2-byte argument before the OSErr result slot.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);
        cpu.write_reg(Register::D0, 0x89AB_CDEF);
        bus.write_word(sp_before + 4, 0xFFFF);
        bus.write_word(sp_before, 2);
        bus.write_word(sp_before + 2, 0x1357);

        let result = disp.dispatch_toolbox(true, 0x015, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp_before + 4);
        assert_eq!(bus.read_word(sp_before + 4), 0);
        assert_eq!(cpu.read_reg(Register::D0), 0x89AB_CDEF);
    }

    #[test]
    fn scsidispatch_selector_three_returns_noerr_and_pops_six_byte_argument_frame() {
        // Inside Macintosh Volume IV (1986), pp. IV-287 to IV-300:
        // selector 3 (SCSICmd) consumes three 2-byte arguments.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);
        cpu.write_reg(Register::D0, 0x0BAD_F00D);
        bus.write_word(sp_before + 8, 0xFFFF);
        bus.write_word(sp_before, 3);
        bus.write_word(sp_before + 2, 0x1111);
        bus.write_word(sp_before + 4, 0x2222);
        bus.write_word(sp_before + 6, 0x3333);

        let result = disp.dispatch_toolbox(true, 0x015, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp_before + 8);
        assert_eq!(bus.read_word(sp_before + 8), 0);
        assert_eq!(cpu.read_reg(Register::D0), 0x0BAD_F00D);
    }

    // Debugger ($A9FF)
    #[test]
    fn debugger_trap_is_parameterless_and_preserves_stack_pointer() {
        // Universal Interfaces Types.h declares Debugger() as a
        // parameterless one-word inline trap.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);

        let result = disp.dispatch_toolbox(true, 0x1FF, &mut cpu, &mut bus);
        assert!(result.is_some(), "Debugger should be handled");
        assert!(result.unwrap().is_ok(), "Debugger should return");
        assert_eq!(cpu.read_reg(Register::A7), sp_before);
    }

    #[test]
    fn debugger_without_installed_debugger_returns_to_caller() {
        // On a stock System 7 setup without MacsBug installed,
        // Debugger returns immediately to the caller.
        let (mut disp, mut cpu, mut bus) = setup();
        cpu.write_reg(Register::D0, 0x1234_5678);
        cpu.write_reg(Register::A0, 0x00AA_5500);

        let result = disp.dispatch_toolbox(true, 0x1FF, &mut cpu, &mut bus);
        assert!(result.is_some(), "Debugger should be handled");
        assert!(result.unwrap().is_ok(), "Debugger should return to caller");
        assert_eq!(cpu.read_reg(Register::D0), 0x1234_5678);
        assert_eq!(cpu.read_reg(Register::A0), 0x00AA_5500);
    }

    // ShutDwnPower ($A895)
    #[test]
    fn test_shut_dwn_power() {
        let (mut disp, mut cpu, mut bus) = setup();

        let result = disp.dispatch_toolbox(true, 0x095, &mut cpu, &mut bus);
        assert!(result.is_some());
        let err = result.unwrap().unwrap_err();
        assert!(
            matches!(err, crate::Error::Halted),
            "ShutDwnPower should return Error::Halted"
        );
    }

    // ShutDwnInstall ($A895 selector 3)
    // Inside Macintosh Volume V, V-589.
    // Universal Headers <ShutDown.h> declares the call as
    //   THREEWORDINLINE(0x3F3C, 0x0003, 0xA895)
    // — the compiler emits MOVE.W #3,-(A7) immediately before $A895, so
    // SP at trap entry holds selector(2) + flags(2) + proc(4) = 8 bytes.
    #[test]
    fn shutdwninstall_pops_eight_byte_argument_frame_and_returns() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_at_trap = TEST_SP;
        bus.write_word(sp_at_trap, 3); // selector
        bus.write_word(sp_at_trap + 2, 0x0001); // flags = sdOnPowerOff
        bus.write_long(sp_at_trap + 4, 0xCAFE_BABE); // proc (never invoked)
        cpu.write_reg(Register::A7, sp_at_trap);

        let result = disp.dispatch_toolbox(true, 0x095, &mut cpu, &mut bus);
        let inner = result.expect("_Shutdown must be a handled trap");
        assert!(
            inner.is_ok(),
            "ShutDwnInstall (selector 3) must return Ok, got {:?}",
            inner
        );
        assert_eq!(
            cpu.read_reg(Register::D0),
            0,
            "sdInstall must report noErr in D0"
        );
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_at_trap + 8,
            "sdInstall must pop selector(2) + flags(2) + proc(4) = 8 bytes"
        );
    }

    // ShutDwnRemove ($A895 selector 4)
    // Inside Macintosh Volume V, V-590.
    // Universal Headers <ShutDown.h> declares the call as
    //   THREEWORDINLINE(0x3F3C, 0x0004, 0xA895)
    // — compiler emits MOVE.W #4,-(A7) immediately before $A895, so SP at
    // trap entry holds selector(2) + proc(4) = 6 bytes.
    #[test]
    fn shutdwnremove_pops_six_byte_argument_frame_and_returns() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_at_trap = TEST_SP;
        bus.write_word(sp_at_trap, 4); // selector
        bus.write_long(sp_at_trap + 2, 0xCAFE_BABE); // proc (never invoked)
        cpu.write_reg(Register::A7, sp_at_trap);

        let result = disp.dispatch_toolbox(true, 0x095, &mut cpu, &mut bus);
        let inner = result.expect("_Shutdown must be a handled trap");
        assert!(
            inner.is_ok(),
            "ShutDwnRemove (selector 4) must return Ok, got {:?}",
            inner
        );
        assert_eq!(
            cpu.read_reg(Register::D0),
            0,
            "sdRemove must report noErr in D0"
        );
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_at_trap + 6,
            "sdRemove must pop selector(2) + proc(4) = 6 bytes"
        );
    }

    // SetFractEnable ($A814)
    //
    // Per Inside Macintosh Volume IV (1986), p. IV-32, SetFractEnable
    // writes the BOOLEAN argument byte verbatim into the FractEnable
    // low-memory global at $0BF4. MPW Pascal BOOLEAN convention places
    // the value byte in the HIGH byte of the 2-byte stack slot, so the
    // trap reads byte at SP+0 (not SP+1) and writes that byte to $0BF4
    // unchanged — TRUE → 0x01, FALSE → 0x00 (NOT a normalised 0xFF for
    // TRUE). These tests pre-poison $0BF4 with a distinct sentinel and
    // assert (a) exact byte parity with the input high-byte, (b) the
    // trap pops exactly 2 bytes (Pascal PROCEDURE protocol — no
    // function-result slot), and (c) memory adjacent to $0BF4 is
    // preserved (regression guard against any future "fix" that writes
    // a wider word or normalises the byte).
    #[test]
    fn setfractenable_true_writes_one_byte_verbatim_to_fract_enable_global() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        // Pre-poison $0BF4 with a sentinel and the adjacent bytes with
        // distinct values to catch any over-write.
        bus.write_byte(0x0BF4, 0xA5);
        bus.write_byte(0x0BF3, 0x77);
        bus.write_byte(0x0BF5, 0x88);
        // Push BOOLEAN TRUE: value byte 0x01 in the HIGH byte of the
        // 2-byte stack slot.
        bus.write_byte(sp, 0x01);
        bus.write_byte(sp + 1, 0x00);

        let result = disp.dispatch_toolbox(true, 0x014, &mut cpu, &mut bus);
        assert!(result.is_some(), "SetFractEnable must be a handled trap");
        assert!(result.unwrap().is_ok());

        // FractEnable byte at $0BF4 holds 0x01 — verbatim BOOLEAN high byte.
        assert_eq!(bus.read_byte(0x0BF4), 0x01);
        // Adjacent bytes preserved — no over-write past the single byte.
        assert_eq!(bus.read_byte(0x0BF3), 0x77);
        assert_eq!(bus.read_byte(0x0BF5), 0x88);
        // A7 advanced by exactly 2 — Pascal PROCEDURE pops the BOOLEAN arg.
        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
    }

    #[test]
    fn setfractenable_false_writes_zero_byte_to_fract_enable_global() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        // Pre-poison with a non-zero value so we can detect that the
        // trap actually wrote 0x00 (rather than no-op).
        bus.write_byte(0x0BF4, 0xC3);
        bus.write_byte(sp, 0x00);
        bus.write_byte(sp + 1, 0x00);

        let result = disp.dispatch_toolbox(true, 0x014, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        // FractEnable byte at $0BF4 cleared to 0x00.
        assert_eq!(bus.read_byte(0x0BF4), 0x00);
        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
    }

    #[test]
    fn setfractenable_consumes_two_byte_boolean_argument_and_balances_stack() {
        // Pascal PROCEDURE protocol: caller pushes one 2-byte BOOLEAN,
        // trap pops 2 bytes, no function-result slot. Net externally
        // observed A7 movement is +2 (the trap's pop). This test asserts
        // the exact pop count regardless of the BOOLEAN value to defeat
        // any future change that pops a different number of bytes
        // (e.g. 4 for a hypothetical LONGINT widening).
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_byte(sp, 0x01);
        bus.write_byte(sp + 1, 0x00);

        let result = disp.dispatch_toolbox(true, 0x014, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp + 2,
            "SetFractEnable must pop exactly the 2-byte BOOLEAN argument"
        );
    }

    // Delay ($A03B) — OS trap
    #[test]
    fn test_delay() {
        let (mut disp, mut cpu, mut bus) = setup();

        // A0 = numTicks (0 from setup), Ticks at $016A = 100 (from setup)
        // finalTicks = 100 + 0 = 100
        let result = disp.dispatch_toolbox(false, 0x3B, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::D0), 100); // finalTicks returned in D0
    }

    // ========== Sound Manager (extended) ==========

    // SoundDispatch ($A800)
    // Selector encoding: bits 31-24 = param_bytes/2, bits 23-16 = routine
    // Sound 1994, 2-256
    #[test]
    fn test_sound_dispatch() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        // Selector goes in D0, not on the stack.
        // 0x00080008: param_bytes=0, routine=$08 (unknown → stub)
        cpu.write_reg(Register::D0, 0x00080008);

        let result = disp.dispatch_sound(true, 0x000, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        // No params to pop, SP unchanged
        assert_eq!(cpu.read_reg(Register::A7), sp);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    // SndNewChannel ($A807)
    #[test]
    fn test_snd_new_channel() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let chan_ptr_ptr = 0x300000u32;
        // Pascal stack (right-to-left push):
        //   SP+0:  userRoutine (4, ProcPtr)
        //   SP+4:  init (4, LongInt)
        //   SP+8:  synth (2, Integer)
        //   SP+10: chan ptr (4, VAR SndChannelPtr)
        //   SP+14: result (2, OSErr)
        // Sound 1994, 2-195
        bus.write_long(sp, 0); // userRoutine
        bus.write_long(sp + 4, 0); // init
        bus.write_word(sp + 8, 0); // synth
        bus.write_long(sp + 10, chan_ptr_ptr); // chan_ptr_ptr
        bus.write_word(sp + 14, 0xBEEF); // result placeholder

        let result = disp.dispatch_sound(true, 0x007, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 14), 0); // noErr
        assert_eq!(cpu.read_reg(Register::A7), sp + 14);
        // Verify a channel was allocated (chan_ptr_ptr should point to non-zero)
        let chan_ptr = bus.read_long(chan_ptr_ptr);
        assert_ne!(chan_ptr, 0, "SndNewChannel should allocate a channel");
    }

    #[test]
    fn test_snd_new_channel_writes_callback_procptr() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let chan_ptr_ptr = 0x300000u32;
        let user_routine = 0x00AB_CDEFu32;
        // Per IM:Sound 1994 p.2-195, SndNewChannel installs the caller's
        // callback procedure for callBackCmd processing.
        bus.write_long(sp, user_routine); // userRoutine
        bus.write_long(sp + 4, 0); // init
        bus.write_word(sp + 8, 0); // synth
        bus.write_long(sp + 10, chan_ptr_ptr); // chan VAR pointer
        bus.write_word(sp + 14, 0xBEEF); // result placeholder

        let result = disp.dispatch_sound(true, 0x007, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        let chan_ptr = bus.read_long(chan_ptr_ptr);
        assert_ne!(chan_ptr, 0);
        // Sound channel callback field is at offset +8 in SndChannel.
        assert_eq!(bus.read_long(chan_ptr + 8), user_routine);
        assert_eq!(disp.sound_manager.channels.len(), 1);
        assert_eq!(disp.sound_manager.channels[0].callback_addr, user_routine);
    }

    // SndDisposeChannel ($A801)
    #[test]
    fn test_snd_dispose_channel() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        // SP+0: quiet(2), SP+2: chan(4), SP+6: result(2)
        bus.write_word(sp, 0);
        bus.write_long(sp + 2, 0);
        bus.write_word(sp + 6, 0xBEEF);

        let result = disp.dispatch_sound(true, 0x001, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 6), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 6);
    }

    #[test]
    fn test_snd_dispose_channel_removes_allocated_channel() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let chan_ptr_ptr = 0x300000u32;
        // Allocate channel first (IM:Sound 1994 p.2-195).
        bus.write_long(sp, 0); // userRoutine
        bus.write_long(sp + 4, 0); // init
        bus.write_word(sp + 8, 0); // synth
        bus.write_long(sp + 10, chan_ptr_ptr); // chan VAR pointer
        bus.write_word(sp + 14, 0xBEEF); // result placeholder
        let result = disp.dispatch_sound(true, 0x007, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        let chan_ptr = bus.read_long(chan_ptr_ptr);
        assert_ne!(chan_ptr, 0);
        assert_eq!(disp.sound_manager.channels.len(), 1);

        // Dispose the channel (IM:Sound 1994 p.2-196).
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 1); // quietNow = TRUE
        bus.write_long(sp + 2, chan_ptr);
        bus.write_word(sp + 6, 0xBEEF);
        let result = disp.dispatch_sound(true, 0x001, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 6), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 6);
        assert!(disp.sound_manager.channels.is_empty());
    }

    // SndDoCommand ($A803)
    #[test]
    fn test_snd_do_command() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        // SP+0: noWait(2), SP+2: cmd(4, ptr to SndCommand), SP+6: chan(4), SP+10: result(2)
        // Sound 1994, 2-130
        let cmd_addr = 0x200100u32;
        bus.write_word(cmd_addr, 0); // cmd = nullCmd
        bus.write_word(cmd_addr + 2, 0); // param1
        bus.write_long(cmd_addr + 4, 0); // param2
        bus.write_word(sp, 0); // noWait
        bus.write_long(sp + 2, cmd_addr); // cmd ptr
        bus.write_long(sp + 6, 0); // chan
        bus.write_word(sp + 10, 0xBEEF); // result placeholder

        let result = disp.dispatch_sound(true, 0x003, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 10), (-205i16) as u16);
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
    }

    #[test]
    fn test_snd_do_command_callback_cmd_queues_pending_callback() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let chan_ptr_ptr = 0x300000u32;
        let user_routine = 0x00AB_CDEFu32;
        let cmd_addr = 0x200100u32;

        // Install channel callback via SndNewChannel (IM:Sound 1994 p.2-195).
        bus.write_long(sp, user_routine); // userRoutine
        bus.write_long(sp + 4, 0); // init
        bus.write_word(sp + 8, 0); // synth
        bus.write_long(sp + 10, chan_ptr_ptr); // chan VAR pointer
        bus.write_word(sp + 14, 0xBEEF);
        let result = disp.dispatch_sound(true, 0x007, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        let chan_ptr = bus.read_long(chan_ptr_ptr);
        assert_ne!(chan_ptr, 0);

        // callBackCmd should schedule the channel callback procedure.
        // IM:Sound notes to issue callBackCmd with SndDoCommand (not
        // SndDoImmediate) so queue ordering is preserved.
        cpu.write_reg(Register::A7, sp);
        bus.write_word(cmd_addr, crate::sound::cmd::CALLBACK);
        bus.write_word(cmd_addr + 2, 7);
        bus.write_long(cmd_addr + 4, 0x1111_2222);
        bus.write_word(sp, 0); // noWait
        bus.write_long(sp + 2, cmd_addr);
        bus.write_long(sp + 6, chan_ptr);
        bus.write_word(sp + 10, 0xBEEF);

        let result = disp.dispatch_sound(true, 0x003, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 10), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
        assert_eq!(disp.sound_manager.pending_sound_callbacks.len(), 1);
        match &disp.sound_manager.pending_sound_callbacks[0] {
            crate::sound::PendingSoundCallback::Command {
                callback_addr,
                chan_ptr: queued_chan_ptr,
                cmd,
            } => {
                assert_eq!(*callback_addr, user_routine);
                assert_eq!(*queued_chan_ptr, chan_ptr);
                assert_eq!(cmd.cmd, crate::sound::cmd::CALLBACK);
                assert_eq!(cmd.param1, 7);
                assert_eq!(cmd.param2, 0x1111_2222);
            }
            other => panic!("expected command callback, got {other:?}"),
        }
    }

    // SndDoImmediate ($A804)
    #[test]
    fn test_snd_do_immediate() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        // SP+0: cmd(4, ptr to SndCommand), SP+4: chan(4), SP+8: result(2)
        // Sound 1994, 2-131
        let cmd_addr = 0x200100u32;
        bus.write_word(cmd_addr, 0); // cmd = nullCmd
        bus.write_word(cmd_addr + 2, 0); // param1
        bus.write_long(cmd_addr + 4, 0); // param2
        bus.write_long(sp, cmd_addr); // cmd ptr
        bus.write_long(sp + 4, 0); // chan
        bus.write_word(sp + 8, 0xBEEF); // result placeholder

        let result = disp.dispatch_sound(true, 0x004, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 8), (-205i16) as u16);
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
    }

    #[test]
    fn test_snd_do_immediate_get_rate_writes_unity_fixed() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let chan_ptr_ptr = 0x300000u32;
        let cmd_addr = 0x200100u32;
        let rate_out_addr = 0x200200u32;

        // Allocate a channel first so getRateCmd queries a real channel.
        bus.write_long(sp, 0); // userRoutine
        bus.write_long(sp + 4, 0); // init
        bus.write_word(sp + 8, 0); // synth
        bus.write_long(sp + 10, chan_ptr_ptr); // chan VAR pointer
        bus.write_word(sp + 14, 0xBEEF);
        let result = disp.dispatch_sound(true, 0x007, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        let chan_ptr = bus.read_long(chan_ptr_ptr);
        assert_ne!(chan_ptr, 0);

        // IM:Sound documents getRateCmd writes the channel rate as a Fixed
        // through param2 when SndDoImmediate returns noErr.
        cpu.write_reg(Register::A7, sp);
        bus.write_word(cmd_addr, crate::sound::cmd::GET_RATE);
        bus.write_word(cmd_addr + 2, 0);
        bus.write_long(cmd_addr + 4, rate_out_addr);
        bus.write_long(rate_out_addr, 0xDEAD_BEEFu32);
        bus.write_long(sp, cmd_addr);
        bus.write_long(sp + 4, chan_ptr);
        bus.write_word(sp + 8, 0xBEEF);

        let result = disp.dispatch_sound(true, 0x004, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 8), 0); // noErr
        assert_eq!(bus.read_long(rate_out_addr), 0x0001_0000); // unity rate
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
    }

    // ========== Resource Manager (extended) ==========

    // OpenRFPerm ($A9C4) — file not found
    #[test]
    fn test_open_rf_perm_not_found() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        // SP+0: perm(2), SP+2: vref(2), SP+4: name_ptr(4), SP+8: result(2)
        let name_ptr = 0x200000u32;
        bus.write_word(sp, 1); // perm = fsRdPerm
        bus.write_word(sp + 2, 0); // vRefNum
        bus.write_long(sp + 4, name_ptr);
        bus.write_word(sp + 8, 0x0000); // result placeholder

        // Write Pascal string filename at name_ptr
        bus.write_pstring(name_ptr, b"NoSuchFile");

        let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        // result = -1 as u16 = 0xFFFF
        assert_eq!(bus.read_word(sp + 8), (-1i16) as u16);
        // D0 mirrors the FUNCTION result slot (-1 on failure).
        assert_eq!(cpu.read_reg(Register::D0), (-1i32) as u32);
        // ResErr at $0A60 = -43
        assert_eq!(bus.read_word(0x0A60), (-43i16) as u16);
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
    }

    // IM:IV IV-17 and MTb 1993 1-64..1-66: successful OpenRFPerm returns a
    // file refnum and makes the newly opened file current.
    #[test]
    fn open_rf_perm_present_file_returns_refnum_and_sets_current_file() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let name_ptr = 0x200100u32;

        disp.vfs_rsrc.insert("Shapes".to_string(), vec![]);

        bus.write_word(sp, 1); // fsRdPerm
        bus.write_word(sp + 2, 0); // vRefNum
        bus.write_long(sp + 4, name_ptr);
        bus.write_word(sp + 8, 0xBEEF); // result placeholder
        bus.write_pstring(name_ptr, b"Shapes");

        let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        let refnum = bus.read_word(sp + 8);
        assert_ne!(refnum as i16, -1);
        assert_eq!(cpu.read_reg(Register::D0), refnum as u32);
        assert_eq!(bus.read_word(0x0A60), 0);
        assert_eq!(disp.current_resource_refnum(), refnum);
        assert_eq!(disp.resource_file_name(refnum), Some("Shapes"));
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
    }

    // MTb 1993 p. 1-65: if already open, OpenRFPerm returns the same refnum
    // and does not make that file current.
    #[test]
    fn open_rf_perm_reopen_returns_existing_refnum_without_switching_current_file() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let name_ptr = 0x200200u32;

        disp.vfs_rsrc.insert("Shapes".to_string(), vec![]);
        disp.vfs_rsrc.insert("Sounds".to_string(), vec![]);

        // First open: Shapes.
        bus.write_word(sp, 1);
        bus.write_word(sp + 2, 0);
        bus.write_long(sp + 4, name_ptr);
        bus.write_word(sp + 8, 0xBEEF);
        bus.write_pstring(name_ptr, b"Shapes");
        let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        let shapes_ref = bus.read_word(sp + 8);
        assert_ne!(shapes_ref as i16, -1);
        assert_eq!(disp.current_resource_refnum(), shapes_ref);

        // Second open: Sounds.
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 1);
        bus.write_word(sp + 2, 0);
        bus.write_long(sp + 4, name_ptr);
        bus.write_word(sp + 8, 0xBEEF);
        bus.write_pstring(name_ptr, b"Sounds");
        let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        let sounds_ref = bus.read_word(sp + 8);
        assert_ne!(sounds_ref as i16, -1);
        assert_ne!(sounds_ref, shapes_ref);
        assert_eq!(disp.current_resource_refnum(), sounds_ref);
        assert_eq!(cpu.read_reg(Register::D0), sounds_ref as u32);

        // Re-open Shapes: refnum re-used, current file unchanged.
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 1);
        bus.write_word(sp + 2, 0);
        bus.write_long(sp + 4, name_ptr);
        bus.write_word(sp + 8, 0xBEEF);
        bus.write_pstring(name_ptr, b"Shapes");
        let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 8), shapes_ref);
        assert_eq!(cpu.read_reg(Register::D0), shapes_ref as u32);
        assert_eq!(disp.current_resource_refnum(), sounds_ref);
        assert_eq!(bus.read_word(0x0A60), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
    }

    #[test]
    fn open_rf_perm_read_after_write_reuses_existing_read_only_refnum() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let name_ptr = 0x200240u32;

        disp.vfs_rsrc.insert("Shapes".to_string(), vec![]);

        bus.write_word(sp, 0);
        bus.write_byte(sp, 3); // fsRdWrPerm
        bus.write_word(sp + 2, 0);
        bus.write_long(sp + 4, name_ptr);
        bus.write_word(sp + 8, 0xBEEF);
        bus.write_pstring(name_ptr, b"Shapes");
        let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        let write_ref = bus.read_word(sp + 8);
        assert!(disp.write_refnums.contains(&write_ref));

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 1); // fsRdPerm
        bus.write_word(sp + 2, 0);
        bus.write_long(sp + 4, name_ptr);
        bus.write_word(sp + 8, 0xBEEF);
        bus.write_pstring(name_ptr, b"Shapes");
        let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        let read_ref = bus.read_word(sp + 8);
        assert_ne!(read_ref, write_ref);
        assert!(!disp.write_refnums.contains(&read_ref));

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 1); // fsRdPerm
        bus.write_word(sp + 2, 0);
        bus.write_long(sp + 4, name_ptr);
        bus.write_word(sp + 8, 0xBEEF);
        bus.write_pstring(name_ptr, b"Shapes");
        let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 8), read_ref);
        assert_eq!(cpu.read_reg(Register::D0), read_ref as u32);
        assert_eq!(bus.read_word(0x0A60), 0);
        assert_eq!(
            disp.resources
                .as_ref()
                .unwrap()
                .names
                .values()
                .filter(|name| name.as_str() == "Shapes")
                .count(),
            2,
            "one write and one read-only access path should be enough"
        );
    }

    // OpenRFPerm should mirror the current refnum in D0 for both the first
    // open and the already-open reuse path.
    #[test]
    fn open_rf_perm_mirrors_refnum_in_d0_on_success_and_reopen() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let name_ptr = 0x200480u32;

        disp.vfs_rsrc.insert("Shapes".to_string(), vec![]);
        disp.vfs_rsrc.insert("Sounds".to_string(), vec![]);

        bus.write_word(sp, 1);
        bus.write_word(sp + 2, 0);
        bus.write_long(sp + 4, name_ptr);
        bus.write_word(sp + 8, 0xBEEF);
        bus.write_pstring(name_ptr, b"Shapes");
        let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        let shapes_ref = bus.read_word(sp + 8);
        assert_ne!(shapes_ref as i16, -1);
        assert_eq!(cpu.read_reg(Register::D0), shapes_ref as u32);
        assert_eq!(disp.current_resource_refnum(), shapes_ref);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 1);
        bus.write_word(sp + 2, 0);
        bus.write_long(sp + 4, name_ptr);
        bus.write_word(sp + 8, 0xBEEF);
        bus.write_pstring(name_ptr, b"Sounds");
        let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        let sounds_ref = bus.read_word(sp + 8);
        assert_ne!(sounds_ref as i16, -1);
        assert_eq!(cpu.read_reg(Register::D0), sounds_ref as u32);
        assert_eq!(disp.current_resource_refnum(), sounds_ref);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 1);
        bus.write_word(sp + 2, 0);
        bus.write_long(sp + 4, name_ptr);
        bus.write_word(sp + 8, 0xBEEF);
        bus.write_pstring(name_ptr, b"Shapes");
        let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 8), shapes_ref);
        assert_eq!(cpu.read_reg(Register::D0), shapes_ref as u32);
        assert_eq!(disp.current_resource_refnum(), sounds_ref);
        assert_eq!(bus.read_word(0x0A60), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
    }

    // MTb 1993 p. 1-65: if already open, OpenRFPerm returns the same refnum
    // and does not make that file current, even when a different file is
    // current at the time of the reopen.
    #[test]
    fn open_rf_perm_reopen_keeps_current_file_when_a_different_file_is_current() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let name_ptr = 0x200280u32;

        disp.vfs_rsrc.insert("Shapes".to_string(), vec![]);
        disp.vfs_rsrc.insert("Sounds".to_string(), vec![]);

        // Open Shapes.
        bus.write_word(sp, 1); // fsRdPerm
        bus.write_word(sp + 2, 0);
        bus.write_long(sp + 4, name_ptr);
        bus.write_word(sp + 8, 0xBEEF);
        bus.write_pstring(name_ptr, b"Shapes");
        let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        let shapes_ref = bus.read_word(sp + 8);
        assert_ne!(shapes_ref as i16, -1);
        assert_eq!(disp.current_resource_refnum(), shapes_ref);

        // Open Sounds so we can later make Shapes current again.
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 1);
        bus.write_word(sp + 2, 0);
        bus.write_long(sp + 4, name_ptr);
        bus.write_word(sp + 8, 0xBEEF);
        bus.write_pstring(name_ptr, b"Sounds");
        let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        let sounds_ref = bus.read_word(sp + 8);
        assert_ne!(sounds_ref as i16, -1);
        assert_ne!(sounds_ref, shapes_ref);
        assert_eq!(disp.current_resource_refnum(), sounds_ref);

        // Make Shapes current again, then reopen Sounds. The reopen must
        // return the original refnum and leave Shapes current.
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, shapes_ref);
        let result = disp.dispatch_toolbox(true, 0x198, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(disp.current_resource_refnum(), shapes_ref);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 1);
        bus.write_word(sp + 2, 0);
        bus.write_long(sp + 4, name_ptr);
        bus.write_word(sp + 8, 0xBEEF);
        bus.write_pstring(name_ptr, b"Sounds");
        let result = disp.dispatch_toolbox(true, 0x1C4, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 8), sounds_ref);
        assert_eq!(disp.current_resource_refnum(), shapes_ref);
        assert_eq!(bus.read_word(0x0A60), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
    }

    // Inside Macintosh Volume I (1985), p. I-115: OpenResFile opens the
    // named resource file, returns a refnum, and makes it current.
    #[test]
    fn open_res_file_present_returns_refnum_and_sets_current_file() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let name_ptr = 0x200250u32;

        disp.vfs_rsrc.insert("Shapes".to_string(), vec![]);

        bus.write_long(sp, name_ptr);
        bus.write_word(sp + 4, 0xBEEF);
        bus.write_pstring(name_ptr, b"Shapes");

        let result = disp.dispatch_toolbox(true, 0x197, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        let refnum = bus.read_word(sp + 4);
        assert_ne!(refnum as i16, -1);
        assert_eq!(cpu.read_reg(Register::D0), refnum as u32);
        assert_eq!(bus.read_word(0x0A60), 0);
        assert_eq!(disp.current_resource_refnum(), refnum);
        assert_eq!(disp.resource_file_name(refnum), Some("Shapes"));
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
    }

    // Inside Macintosh Volume I (1985), p. I-115: reopening an already-open
    // resource file returns its refnum but does not make it current.
    #[test]
    fn open_res_file_reopen_returns_existing_refnum_without_switching_current_file() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let name_ptr = 0x200260u32;

        disp.vfs_rsrc.insert("Shapes".to_string(), vec![]);
        disp.vfs_rsrc.insert("Sounds".to_string(), vec![]);

        bus.write_long(sp, name_ptr);
        bus.write_word(sp + 4, 0xBEEF);
        bus.write_pstring(name_ptr, b"Shapes");
        let result = disp.dispatch_toolbox(true, 0x197, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        let shapes_ref = bus.read_word(sp + 4);
        assert_ne!(shapes_ref as i16, -1);
        assert_eq!(cpu.read_reg(Register::D0), shapes_ref as u32);
        assert_eq!(disp.current_resource_refnum(), shapes_ref);

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, name_ptr);
        bus.write_word(sp + 4, 0xBEEF);
        bus.write_pstring(name_ptr, b"Sounds");
        let result = disp.dispatch_toolbox(true, 0x197, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        let sounds_ref = bus.read_word(sp + 4);
        assert_ne!(sounds_ref as i16, -1);
        assert_ne!(sounds_ref, shapes_ref);
        assert_eq!(cpu.read_reg(Register::D0), sounds_ref as u32);
        assert_eq!(disp.current_resource_refnum(), sounds_ref);

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, name_ptr);
        bus.write_word(sp + 4, 0xBEEF);
        bus.write_pstring(name_ptr, b"Shapes");
        let result = disp.dispatch_toolbox(true, 0x197, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 4), shapes_ref);
        assert_eq!(cpu.read_reg(Register::D0), shapes_ref as u32);
        assert_eq!(disp.current_resource_refnum(), sounds_ref);
        assert_eq!(bus.read_word(0x0A60), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
    }

    #[test]
    fn open_res_file_canonical_reopen_reuses_existing_refnum_without_switching_current_file() {
        // IM:I I-115: already-open OpenResFile returns the existing refnum
        // and does not make that file current. This holds even when the
        // second open spells the same file via a more explicit path that
        // resolves to the same VFS resource fork.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let name_ptr = 0x200265u32;

        disp.vfs_rsrc.insert("Folder/Shapes".to_string(), vec![]);
        disp.vfs_rsrc.insert("Sounds".to_string(), vec![]);

        bus.write_long(sp, name_ptr);
        bus.write_word(sp + 4, 0xBEEF);
        bus.write_pstring(name_ptr, b"Shapes");
        let result = disp.dispatch_toolbox(true, 0x197, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        let shapes_ref = bus.read_word(sp + 4);
        assert_ne!(shapes_ref as i16, -1);
        assert_eq!(disp.current_resource_refnum(), shapes_ref);
        assert_eq!(disp.resource_file_name(shapes_ref), Some("Folder/Shapes"));

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, name_ptr);
        bus.write_word(sp + 4, 0xBEEF);
        bus.write_pstring(name_ptr, b"Sounds");
        let result = disp.dispatch_toolbox(true, 0x197, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        let sounds_ref = bus.read_word(sp + 4);
        assert_ne!(sounds_ref as i16, -1);
        assert_ne!(sounds_ref, shapes_ref);
        assert_eq!(disp.current_resource_refnum(), sounds_ref);

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, name_ptr);
        bus.write_word(sp + 4, 0xBEEF);
        bus.write_pstring(name_ptr, b"Unix:Folder:Shapes");
        let result = disp.dispatch_toolbox(true, 0x197, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 4), shapes_ref);
        assert_eq!(disp.current_resource_refnum(), sounds_ref);
        assert_eq!(bus.read_word(0x0A60), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
    }

    // Inside Macintosh Volume I (1985), p. I-115: on failure OpenResFile
    // returns -1 and ResError reports the underlying file-system error.
    #[test]
    fn open_res_file_missing_returns_minus_one_and_fnferr() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let name_ptr = 0x200270u32;

        bus.write_long(sp, name_ptr);
        bus.write_word(sp + 4, 0xBEEF);
        bus.write_pstring(name_ptr, b"NoSuchOpenResFile");

        let result = disp.dispatch_toolbox(true, 0x197, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 4), (-1i16) as u16);
        assert_eq!(cpu.read_reg(Register::D0), (-1i32) as u32);
        assert_eq!(bus.read_word(0x0A60), (-43i16) as u16);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
    }

    // MTb 1993 pp. 1-62..1-64: HOpenResFile opens the requested resource
    // fork, returns the refnum, and makes it current.
    #[test]
    fn hopenresfile_success_returns_refnum_and_sets_current_resource_file() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let name_ptr = 0x2002f0u32;

        disp.vfs_rsrc.insert("Shapes".to_string(), vec![]);

        bus.write_word(sp, 1); // fsRdPerm
        bus.write_long(sp + 2, name_ptr);
        bus.write_long(sp + 6, 0); // dirID
        bus.write_word(sp + 10, 0); // vRefNum
        bus.write_word(sp + 12, 0xBEEF); // result
        bus.write_pstring(name_ptr, b"Shapes");

        let result = disp.dispatch_toolbox(true, 0x01A, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        let refnum = bus.read_word(sp + 12);
        assert_ne!(refnum as i16, -1);
        assert_eq!(bus.read_word(0x0A60), 0);
        assert_eq!(disp.current_resource_refnum(), refnum);
        assert_eq!(disp.resource_file_name(refnum), Some("Shapes"));
        assert_eq!(cpu.read_reg(Register::A7), sp + 12);
    }

    // MTb 1993 p. 1-63: HOpenResFile returns -1 and ResError reports the file
    // error when it can't open the requested resource fork.
    #[test]
    fn hopenresfile_missing_file_returns_minus_one_and_fnferr() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let name_ptr = 0x200300u32;

        bus.write_word(sp, 1); // fsRdPerm
        bus.write_long(sp + 2, name_ptr);
        bus.write_long(sp + 6, 0); // dirID
        bus.write_word(sp + 10, 0); // vRefNum
        bus.write_word(sp + 12, 0xBEEF); // result
        bus.write_pstring(name_ptr, b"NoSuchHOpenFile");

        let result = disp.dispatch_toolbox(true, 0x01A, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 12), (-1i16) as u16);
        assert_eq!(bus.read_word(0x0A60), (-43i16) as u16);
        assert_eq!(cpu.read_reg(Register::A7), sp + 12);
    }

    // MTb 1993 p. 1-63: already-open HOpenResFile returns the existing refnum
    // and does not make that file current.
    #[test]
    fn hopenresfile_already_open_returns_same_refnum_without_switching_current_file() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let name_ptr = 0x200400u32;

        disp.vfs_rsrc.insert("Shapes".to_string(), vec![]);
        disp.vfs_rsrc.insert("Sounds".to_string(), vec![]);

        // First open: Shapes.
        bus.write_word(sp, 1);
        bus.write_long(sp + 2, name_ptr);
        bus.write_long(sp + 6, 0);
        bus.write_word(sp + 10, 0);
        bus.write_word(sp + 12, 0xBEEF);
        bus.write_pstring(name_ptr, b"Shapes");
        let result = disp.dispatch_toolbox(true, 0x01A, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        let shapes_ref = bus.read_word(sp + 12);
        assert_ne!(shapes_ref as i16, -1);
        assert_eq!(bus.read_word(0x0A60), 0);
        assert_eq!(disp.current_resource_refnum(), shapes_ref);

        // Second open: Sounds.
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 1);
        bus.write_long(sp + 2, name_ptr);
        bus.write_long(sp + 6, 0);
        bus.write_word(sp + 10, 0);
        bus.write_word(sp + 12, 0xBEEF);
        bus.write_pstring(name_ptr, b"Sounds");
        let result = disp.dispatch_toolbox(true, 0x01A, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        let sounds_ref = bus.read_word(sp + 12);
        assert_ne!(sounds_ref as i16, -1);
        assert_ne!(sounds_ref, shapes_ref);
        assert_eq!(disp.current_resource_refnum(), sounds_ref);

        // Re-open Shapes: refnum re-used, current file unchanged.
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 1);
        bus.write_long(sp + 2, name_ptr);
        bus.write_long(sp + 6, 0);
        bus.write_word(sp + 10, 0);
        bus.write_word(sp + 12, 0xBEEF);
        bus.write_pstring(name_ptr, b"Shapes");
        let result = disp.dispatch_toolbox(true, 0x01A, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 12), shapes_ref);
        assert_eq!(disp.current_resource_refnum(), sounds_ref);
        assert_eq!(bus.read_word(0x0A60), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 12);
    }

    #[test]
    fn hopenresfile_canonical_reopen_reuses_existing_refnum_without_switching_current_file() {
        // MTb 1993 p. 1-63: already-open HOpenResFile returns the existing
        // refnum and does not make that file current, even when the reopen
        // uses a canonicalized path spelling for the same fork.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let name_ptr = 0x200410u32;

        disp.vfs_rsrc.insert("Folder/Shapes".to_string(), vec![]);
        disp.vfs_rsrc.insert("Sounds".to_string(), vec![]);

        bus.write_word(sp, 1);
        bus.write_long(sp + 2, name_ptr);
        bus.write_long(sp + 6, 0);
        bus.write_word(sp + 10, 0);
        bus.write_word(sp + 12, 0xBEEF);
        bus.write_pstring(name_ptr, b"Shapes");
        let result = disp.dispatch_toolbox(true, 0x01A, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        let shapes_ref = bus.read_word(sp + 12);
        assert_ne!(shapes_ref as i16, -1);
        assert_eq!(disp.current_resource_refnum(), shapes_ref);
        assert_eq!(disp.resource_file_name(shapes_ref), Some("Folder/Shapes"));

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 1);
        bus.write_long(sp + 2, name_ptr);
        bus.write_long(sp + 6, 0);
        bus.write_word(sp + 10, 0);
        bus.write_word(sp + 12, 0xBEEF);
        bus.write_pstring(name_ptr, b"Sounds");
        let result = disp.dispatch_toolbox(true, 0x01A, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        let sounds_ref = bus.read_word(sp + 12);
        assert_ne!(sounds_ref as i16, -1);
        assert_ne!(sounds_ref, shapes_ref);
        assert_eq!(disp.current_resource_refnum(), sounds_ref);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 1);
        bus.write_long(sp + 2, name_ptr);
        bus.write_long(sp + 6, 0);
        bus.write_word(sp + 10, 0);
        bus.write_word(sp + 12, 0xBEEF);
        bus.write_pstring(name_ptr, b"Unix:Folder:Shapes");
        let result = disp.dispatch_toolbox(true, 0x01A, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 12), shapes_ref);
        assert_eq!(disp.current_resource_refnum(), sounds_ref);
        assert_eq!(bus.read_word(0x0A60), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 12);
    }

    // CloseResFile ($A99A)
    #[test]
    fn test_close_res_file() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_word(sp, 2); // refNum

        let result = disp.dispatch_toolbox(true, 0x19A, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
    }

    #[test]
    fn closeresfile_releases_loaded_resource_memory_and_handles() {
        let (mut disp, mut cpu, mut bus) = setup();
        let refnum = 2;
        let data_ptr = bus.alloc(8);
        bus.write_bytes(data_ptr, &[0xAB; 8]);
        let handle = bus.alloc(4);
        bus.write_long(handle, data_ptr);

        disp.resources = Some(crate::trap::dispatch::LoadedResources {
            files: std::collections::HashMap::from([
                (0, crate::trap::dispatch::ResourceFileMap::default()),
                (
                    refnum,
                    crate::trap::dispatch::ResourceFileMap {
                        loaded: std::collections::HashMap::from([((*b"PICT", 23002), data_ptr)]),
                        named: std::collections::HashMap::new(),
                        names_by_id: std::collections::HashMap::new(),
                        attrs: std::collections::HashMap::new(),
                        map_attrs: 0,
                    },
                ),
            ]),
            names: std::collections::HashMap::from([(refnum, "BladeData".to_string())]),
            search_order: vec![0, refnum],
            current_file: refnum,
        });
        disp.loaded_handles
            .insert(handle, (data_ptr, *b"PICT", 23002));
        disp.resource_handle_files.insert(handle, refnum);
        disp.ptr_to_handle.insert(data_ptr, handle);
        bus.write_word(0x0A5A, refnum);

        let sp = TEST_SP;
        bus.write_word(sp, refnum);

        let result = disp.dispatch_toolbox(true, 0x19A, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
        assert_eq!(disp.current_resource_refnum(), 0);
        assert_eq!(bus.get_alloc_size(data_ptr), None);
        assert_eq!(bus.get_alloc_size(handle), None);
        assert!(!disp.loaded_handles.contains_key(&handle));
        assert!(!disp.resource_handle_files.contains_key(&handle));
        assert_eq!(disp.ptr_to_handle.get(&data_ptr), None);
        assert!(!disp.resources.as_ref().unwrap().files.contains_key(&refnum));
        assert_eq!(bus.read_word(0x0A60), 0);
    }

    #[test]
    fn test_close_res_file_invalid_refnum_sets_basiliskii_resfnotfound() {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.resources = Some(crate::trap::dispatch::LoadedResources {
            files: std::collections::HashMap::from([
                (0, crate::trap::dispatch::ResourceFileMap::default()),
                (2, crate::trap::dispatch::ResourceFileMap::default()),
            ]),
            names: std::collections::HashMap::new(),
            search_order: vec![0, 2],
            current_file: 2,
        });
        bus.write_word(0x0A5A, 2);
        bus.write_word(0x0A60, 0);

        let sp = TEST_SP;
        bus.write_word(sp, (-2i16) as u16);

        let result = disp.dispatch_toolbox(true, 0x19A, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
        assert_eq!(disp.current_resource_refnum(), 2);
        assert_eq!(bus.read_word(0x0A5A), 2);
        assert_eq!(bus.read_word(0x0A60), (-193i16) as u16);
    }

    // UseResFile ($A998)
    #[test]
    fn test_use_res_file() {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.resources = Some(crate::trap::dispatch::LoadedResources {
            files: std::collections::HashMap::from([
                (0, crate::trap::dispatch::ResourceFileMap::default()),
                (2, crate::trap::dispatch::ResourceFileMap::default()),
            ]),
            names: std::collections::HashMap::new(),
            search_order: vec![0, 2],
            current_file: 0,
        });
        let sp = TEST_SP;
        bus.write_word(sp, 2); // refNum

        let result = disp.dispatch_toolbox(true, 0x198, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
        assert_eq!(disp.current_resource_refnum(), 2);
        assert_eq!(bus.read_word(0x0A5A), 2);
    }

    #[test]
    fn test_use_res_file_invalid_refnum_preserves_current_and_sets_resfnotfound() {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.resources = Some(crate::trap::dispatch::LoadedResources {
            files: std::collections::HashMap::from([
                (0, crate::trap::dispatch::ResourceFileMap::default()),
                (2, crate::trap::dispatch::ResourceFileMap::default()),
            ]),
            names: std::collections::HashMap::new(),
            search_order: vec![0, 2],
            current_file: 2,
        });
        bus.write_word(0x0A5A, 2);
        bus.write_word(0x0A60, 0);

        let sp = TEST_SP;
        bus.write_word(sp, (-2i16) as u16);

        let result = disp.dispatch_toolbox(true, 0x198, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
        assert_eq!(disp.current_resource_refnum(), 2);
        assert_eq!(bus.read_word(0x0A5A), 2);
        assert_eq!(bus.read_word(0x0A60), (-193i16) as u16);
    }

    #[test]
    fn uniqueid_scans_all_open_files_while_unique1id_uses_current_file_only() {
        // Inside Macintosh Volume I (1985), p. I-121:
        // UniqueID searches all open resource files for the type.
        // Inside Macintosh Volume IV (1986), p. IV-16:
        // Unique1ID applies the same rule to the current file only.
        let (mut disp, mut cpu, mut bus) = setup();

        disp.install_test_resource_in_file(&mut bus, 0, *b"STR ", 128, &[0x11]);
        disp.install_test_resource_in_file(&mut bus, 2, *b"STR ", 129, &[0x22]);
        if let Some(resources) = disp.resources.as_mut() {
            resources.current_file = 0;
        }

        let sp = TEST_SP;
        bus.write_long(sp, u32::from_be_bytes(*b"STR "));
        bus.write_word(sp + 4, 0xBEEF);
        let uniqueid = disp.dispatch_toolbox(true, 0x1C1, &mut cpu, &mut bus);
        assert!(uniqueid.is_some());
        assert!(uniqueid.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 4), 130);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, u32::from_be_bytes(*b"STR "));
        bus.write_word(sp + 4, 0xBEEF);
        let unique1id = disp.dispatch_toolbox(true, 0x010, &mut cpu, &mut bus);
        assert!(unique1id.is_some());
        assert!(unique1id.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 4), 129);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
    }

    #[test]
    fn uniqueid_family_returns_128_when_requested_type_is_absent() {
        // Inside Macintosh Volume I (1985), p. I-121 and Volume IV (1986),
        // p. IV-16: both routines return an unused ID for the requested type.
        // In Systemless's HLE, candidate scans begin at 128.
        let (mut disp, mut cpu, mut bus) = setup();

        disp.install_test_resource_in_file(&mut bus, 0, *b"MENU", 200, &[0x33]);
        disp.install_test_resource_in_file(&mut bus, 3, *b"MENU", 201, &[0x44]);
        if let Some(resources) = disp.resources.as_mut() {
            resources.current_file = 0;
        }

        let sp = TEST_SP;
        bus.write_long(sp, u32::from_be_bytes(*b"STR "));
        bus.write_word(sp + 4, 0xBEEF);
        let uniqueid = disp.dispatch_toolbox(true, 0x1C1, &mut cpu, &mut bus);
        assert!(uniqueid.is_some());
        assert!(uniqueid.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 4), 128);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, u32::from_be_bytes(*b"STR "));
        bus.write_word(sp + 4, 0xBEEF);
        let unique1id = disp.dispatch_toolbox(true, 0x010, &mut cpu, &mut bus);
        assert!(unique1id.is_some());
        assert!(unique1id.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 4), 128);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
    }

    // Inside Macintosh Volume I (1985), p. I-124: RmvResource removes the
    // current-file map reference but does not dispose handle memory.
    #[test]
    fn rmveresource_removes_current_file_reference_without_disposing_handle_data() {
        let (mut disp, mut cpu, mut bus) = setup();
        let data_ptr = bus.alloc(6);
        bus.write_bytes(data_ptr, &[0x10, 0x20, 0x30, 0x40, 0x50, 0x60]);
        let handle = bus.alloc(4);
        bus.write_long(handle, data_ptr);

        disp.resources = Some(LoadedResources {
            files: HashMap::from([(
                0,
                ResourceFileMap {
                    loaded: HashMap::from([((*b"TEST", 7), data_ptr)]),
                    named: HashMap::from([((*b"TEST", "Sample".to_string()), (7, data_ptr))]),
                    names_by_id: HashMap::new(),
                    attrs: HashMap::from([((*b"TEST", 7), 0u8)]),
                    map_attrs: 0,
                },
            )]),
            names: HashMap::new(),
            search_order: vec![0],
            current_file: 0,
        });
        disp.loaded_handles.insert(handle, (data_ptr, *b"TEST", 7));
        disp.resource_handle_files.insert(handle, 0);

        let sp = TEST_SP;
        bus.write_long(sp, handle);
        let result = disp.dispatch_toolbox(true, 0x1AD, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
        assert_eq!(bus.read_word(0x0A60), 0);
        assert_eq!(bus.read_long(handle), data_ptr);
        assert!(!disp.loaded_handles.contains_key(&handle));
        assert!(!disp.resource_handle_files.contains_key(&handle));
        let file = &disp.resources.as_ref().unwrap().files[&0];
        assert!(!file.loaded.contains_key(&(*b"TEST", 7)));
        assert!(!file.attrs.contains_key(&(*b"TEST", 7)));
        assert!(file.named.is_empty());
    }

    // Inside Macintosh Volume I (1985), p. I-124: resProtected resources are
    // not removed and RmvResource returns rmvResFailed.
    #[test]
    fn rmveresource_protected_handle_returns_rmvresfailed() {
        let (mut disp, mut cpu, mut bus) = setup();
        let data_ptr = bus.alloc(4);
        let handle = bus.alloc(4);
        bus.write_long(handle, data_ptr);

        disp.resources = Some(LoadedResources {
            files: HashMap::from([(
                0,
                ResourceFileMap {
                    loaded: HashMap::from([((*b"PROT", 9), data_ptr)]),
                    named: HashMap::new(),
                    names_by_id: HashMap::new(),
                    attrs: HashMap::from([((*b"PROT", 9), 0x0008u8)]),
                    map_attrs: 0,
                },
            )]),
            names: HashMap::new(),
            search_order: vec![0],
            current_file: 0,
        });
        disp.loaded_handles.insert(handle, (data_ptr, *b"PROT", 9));
        disp.resource_handle_files.insert(handle, 0);

        let sp = TEST_SP;
        bus.write_long(sp, handle);
        let result = disp.dispatch_toolbox(true, 0x1AD, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
        assert_eq!(bus.read_word(0x0A60) as i16, -196);
        assert!(disp.loaded_handles.contains_key(&handle));
        assert!(disp.resources.as_ref().unwrap().files[&0]
            .loaded
            .contains_key(&(*b"PROT", 9)));
    }

    // Inside Macintosh Volume I (1985), p. I-124: noncurrent-file resources
    // fail with rmvResFailed.
    #[test]
    fn rmveresource_noncurrent_file_handle_returns_rmvresfailed() {
        let (mut disp, mut cpu, mut bus) = setup();
        let data_ptr = bus.alloc(4);
        let handle = bus.alloc(4);
        bus.write_long(handle, data_ptr);

        disp.resources = Some(LoadedResources {
            files: HashMap::from([
                (0, ResourceFileMap::default()),
                (
                    2,
                    ResourceFileMap {
                        loaded: HashMap::from([((*b"OTHR", 3), data_ptr)]),
                        named: HashMap::new(),
                        names_by_id: HashMap::new(),
                        attrs: HashMap::from([((*b"OTHR", 3), 0u8)]),
                        map_attrs: 0,
                    },
                ),
            ]),
            names: HashMap::new(),
            search_order: vec![0, 2],
            current_file: 0,
        });
        disp.loaded_handles.insert(handle, (data_ptr, *b"OTHR", 3));
        disp.resource_handle_files.insert(handle, 2);

        let sp = TEST_SP;
        bus.write_long(sp, handle);
        let result = disp.dispatch_toolbox(true, 0x1AD, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
        assert_eq!(bus.read_word(0x0A60) as i16, -196);
        assert!(disp.loaded_handles.contains_key(&handle));
        assert!(disp.resources.as_ref().unwrap().files[&2]
            .loaded
            .contains_key(&(*b"OTHR", 3)));
    }

    // Inside Macintosh Volume I (1985), p. I-124: non-resource handles return
    // rmvResFailed.
    #[test]
    fn rmveresource_non_resource_handle_returns_rmvresfailed() {
        let (mut disp, mut cpu, mut bus) = setup();
        let fake_ptr = bus.alloc(4);
        let fake_handle = bus.alloc(4);
        bus.write_long(fake_handle, fake_ptr);

        let sp = TEST_SP;
        bus.write_long(sp, fake_handle);
        let result = disp.dispatch_toolbox(true, 0x1AD, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
        assert_eq!(bus.read_word(0x0A60) as i16, -196);
        assert_eq!(bus.read_long(fake_handle), fake_ptr);
    }

    // Inside Macintosh Volume I (1985), p. I-124: RmveReference is an
    // obsolete alias for RmveResource / RemoveResource and leaves the
    // handle data allocated.
    #[test]
    fn rmverereference_removes_current_file_reference_without_disposing_handle_data() {
        let (mut disp, mut cpu, mut bus) = setup();
        let data_ptr = bus.alloc(6);
        bus.write_bytes(data_ptr, &[0x10, 0x20, 0x30, 0x40, 0x50, 0x60]);
        let handle = bus.alloc(4);
        bus.write_long(handle, data_ptr);

        disp.resources = Some(LoadedResources {
            files: HashMap::from([(
                0,
                ResourceFileMap {
                    loaded: HashMap::from([((*b"TEST", 7), data_ptr)]),
                    named: HashMap::from([((*b"TEST", "Sample".to_string()), (7, data_ptr))]),
                    names_by_id: HashMap::new(),
                    attrs: HashMap::from([((*b"TEST", 7), 0u8)]),
                    map_attrs: 0,
                },
            )]),
            names: HashMap::new(),
            search_order: vec![0],
            current_file: 0,
        });
        disp.loaded_handles.insert(handle, (data_ptr, *b"TEST", 7));
        disp.resource_handle_files.insert(handle, 0);

        let sp = TEST_SP;
        bus.write_long(sp, handle);
        let result = disp.dispatch_toolbox(true, 0x1AE, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
        assert_eq!(bus.read_word(0x0A60), 0);
        assert_eq!(bus.read_long(handle), data_ptr);
        assert!(!disp.loaded_handles.contains_key(&handle));
        assert!(!disp.resource_handle_files.contains_key(&handle));
        let file = &disp.resources.as_ref().unwrap().files[&0];
        assert!(!file.loaded.contains_key(&(*b"TEST", 7)));
        assert!(!file.attrs.contains_key(&(*b"TEST", 7)));
        assert!(file.named.is_empty());
    }

    // Inside Macintosh Volume I (1985), p. I-125: UpdateResFile flushes
    // changed state for the requested open resource file.
    #[test]
    fn updateresfile_clears_reschanged_bits_for_target_open_file() {
        let (mut disp, mut cpu, mut bus) = setup();
        let changed = super::super::TrapDispatcher::RES_CHANGED_ATTR as u8;

        disp.resources = Some(LoadedResources {
            files: HashMap::from([
                (
                    0,
                    ResourceFileMap {
                        loaded: HashMap::from([((*b"CURR", 1), 0x1000)]),
                        named: HashMap::new(),
                        names_by_id: HashMap::new(),
                        attrs: HashMap::from([((*b"CURR", 1), changed)]),
                        map_attrs: 0,
                    },
                ),
                (
                    2,
                    ResourceFileMap {
                        loaded: HashMap::from([((*b"TARG", 2), 0x2000)]),
                        named: HashMap::new(),
                        names_by_id: HashMap::new(),
                        attrs: HashMap::from([((*b"TARG", 2), changed | 0x0008u8)]),
                        map_attrs: 0,
                    },
                ),
            ]),
            names: HashMap::new(),
            search_order: vec![0, 2],
            current_file: 0,
        });

        let sp = TEST_SP;
        bus.write_word(sp, 2);
        let result = disp.dispatch_toolbox(true, 0x199, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
        assert_eq!(bus.read_word(0x0A60), 0);

        let resources = disp.resources.as_ref().unwrap();
        let target_attrs = resources.files[&2].attrs[&(*b"TARG", 2)];
        let current_attrs = resources.files[&0].attrs[&(*b"CURR", 1)];
        assert_eq!(target_attrs & changed, 0);
        assert_eq!(target_attrs & 0x0008u8, 0x0008u8);
        assert_ne!(current_attrs & changed, 0);
    }

    // Inside Macintosh Volume I (1985), p. I-125: UpdateResFile on an unknown
    // refNum reports resFNotFound.
    #[test]
    fn updateresfile_invalid_refnum_returns_resfnotfound() {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.resources = Some(LoadedResources {
            files: HashMap::from([(0, ResourceFileMap::default())]),
            names: HashMap::new(),
            search_order: vec![0],
            current_file: 0,
        });

        let sp = TEST_SP;
        bus.write_word(sp, 99);
        bus.write_word(0x0A60, 0);
        let result = disp.dispatch_toolbox(true, 0x199, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
        assert_eq!(bus.read_word(0x0A60) as i16, -193);
    }

    // Inside Macintosh Volume I (1985), p. I-126: SetResPurge takes one
    // BOOLEAN argument and therefore consumes one word from the stack.
    #[test]
    fn setrespurge_reads_high_byte_boolean_and_consumes_argument() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_word(sp, 0x00FF);
        bus.write_word(sp + 2, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x193, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert!(!disp.res_purge);
        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
        assert_eq!(bus.read_word(sp + 2), 0xBEEF);
    }

    // Inside Macintosh Volume I (1985), p. I-126: SetResPurge installs
    // or removes the resource purge handler based on install.
    #[test]
    fn setrespurge_toggles_resource_purge_install_flag() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_word(sp, 0x0100);
        let result = disp.dispatch_toolbox(true, 0x193, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert!(disp.res_purge);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x00FF);
        let result = disp.dispatch_toolbox(true, 0x193, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert!(!disp.res_purge);
    }

    // IM:I I-302 + IM:I I-91: DragGrayRgn is the gray-outline alias of
    // DragTheRgn, and the non-drag path returns the $80008000 sentinel.
    #[test]
    fn draggrayrgn_returns_no_drag_sentinel_and_consumes_arguments() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP - 22;
        cpu.write_reg(Register::A7, sp);
        for i in 0..22u32 {
            bus.write_byte(sp + i, 0xAA);
        }
        bus.write_long(sp + 22, 0xDEAD_BEEF);

        let result = disp.dispatch_toolbox(true, 0x105, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
        assert_eq!(bus.read_long(TEST_SP), 0x8000_8000);
    }

    // SetResLoad ($A99B) — Mac Pascal Boolean is in the high byte
    #[test]
    fn test_set_res_load_reads_high_byte_boolean() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        // IM:More Macintosh Toolbox 1993, 1-79 plus MPW stack convention:
        // Boolean FALSE is $00 in the high byte. The low byte is padding and
        // must not turn the parameter true.
        disp.res_load = true;
        bus.write_word(sp, 0x00FF);
        bus.write_word(0x0A60, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x19B, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert!(!disp.res_load);
        assert_eq!(bus.read_word(0x0A60), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 2);

        // TRUE is $01 in the high byte.
        cpu.write_reg(Register::A7, sp);
        bus.write_word(0x0A60, 0xBEEF);
        bus.write_word(sp, 0x0100);
        let result = disp.dispatch_toolbox(true, 0x19B, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert!(disp.res_load);
        assert_eq!(bus.read_word(0x0A60), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
    }

    // CountResources ($A99C)
    #[test]
    fn test_count_resources() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        // SP+0: type(4)
        bus.write_long(sp, 0x49434E23); // 'ICN#'
        bus.write_word(sp + 4, 0xBEEF); // result placeholder

        let result = disp.dispatch_toolbox(true, 0x19C, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 4), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
    }

    // Count1Resources ($A80D)
    #[test]
    fn test_count1_resources() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_long(sp, 0x49434E23); // 'ICN#'
        bus.write_word(sp + 4, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x00D, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 4), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
    }

    // GetNamedResource ($A9A1)
    #[test]
    fn test_get_named_resource_searches_resource_chain() {
        let (mut disp, mut cpu, mut bus) = setup();
        let data_ptr = bus.alloc(16);
        bus.write_bytes(data_ptr, &[0x42; 16]);

        let mut loaded = HashMap::new();
        loaded.insert((*b"STR ", 500i16), data_ptr);
        let mut named = HashMap::new();
        named.insert((*b"STR ", "MyString".to_string()), (500i16, data_ptr));

        disp.resources = Some(LoadedResources {
            files: HashMap::from([
                (0, ResourceFileMap::default()),
                (
                    2,
                    ResourceFileMap {
                        loaded,
                        named,
                        names_by_id: HashMap::new(),
                        attrs: HashMap::new(),
                        map_attrs: 0,
                    },
                ),
            ]),
            names: HashMap::new(),
            search_order: vec![0, 2],
            current_file: 2,
        });

        let name_addr = 0x200000u32;
        bus.write_byte(name_addr, 8);
        bus.write_bytes(name_addr + 1, b"MyString");

        let sp = TEST_SP;
        bus.write_long(sp, name_addr);
        bus.write_long(sp + 4, u32::from_be_bytes(*b"STR "));

        let result = disp.dispatch_toolbox(true, 0x1A1, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert_eq!(bus.read_word(0x0A60), 0);
        assert_ne!(bus.read_long(sp + 8), 0);
    }

    // GetResAttrs ($A9A6) — handler now lives in resource.rs (see
    // dispatcher chain in dispatch.rs: dispatch_resource runs before
    // dispatch_toolbox). Test against dispatch_resource directly so the
    // assertion exercises the canonical path.
    #[test]
    fn test_get_res_attrs() {
        let (mut disp, mut cpu, mut bus) = setup();
        let data_ptr = bus.alloc(8);
        bus.write_bytes(data_ptr, &[0x11; 8]);
        let handle = bus.alloc(4);
        bus.write_long(handle, data_ptr);
        disp.loaded_handles.insert(handle, (data_ptr, *b"TEST", 7));
        disp.resource_handle_files.insert(handle, 0);
        disp.resources = Some(LoadedResources {
            files: HashMap::from([(
                0,
                ResourceFileMap {
                    loaded: HashMap::from([((*b"TEST", 7), data_ptr)]),
                    named: HashMap::new(),
                    names_by_id: HashMap::new(),
                    attrs: HashMap::from([((*b"TEST", 7), 0x002Cu8)]),
                    map_attrs: 0,
                },
            )]),
            names: HashMap::new(),
            search_order: vec![0],
            current_file: 0,
        });

        let sp = TEST_SP;
        // SP+0: handle(4)
        bus.write_long(sp, handle);
        bus.write_word(sp + 4, 0xBEEF);

        let result = disp.dispatch_resource(true, 0x1A6, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 4), 0x002C);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
        assert_eq!(bus.read_word(0x0A60), 0);
    }

    // ========== Misc Toolbox ==========

    // Inside Macintosh Volume I (1985), p. I-287: DrawGrowIcon draws
    // delimiter lines 15 pixels in from the right/bottom edges of portRect.
    #[test]
    fn drawgrowicon_draws_scrollbar_delimiter_lines_15_pixels_in_from_portrect_edges() {
        let (mut disp, mut cpu, mut bus) = setup();
        let screen_base = bus.alloc(64 * 342);
        disp.screen_mode = (screen_base, 64, 512, 342, 1);

        let window_ptr = bus.alloc(256);
        bus.write_word(window_ptr + 6, 0x0000); // GrafPort path, not CGrafPort
        bus.write_word(window_ptr + 8, 0); // portBits.bounds.top
        bus.write_word(window_ptr + 10, 0); // portBits.bounds.left
        bus.write_word(window_ptr + 16, 30); // portRect.top
        bus.write_word(window_ptr + 18, 20); // portRect.left
        bus.write_word(window_ptr + 20, 70); // portRect.bottom
        bus.write_word(window_ptr + 22, 100); // portRect.right

        let sp = TEST_SP - 4;
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, window_ptr);

        let result = disp.dispatch_toolbox(true, 0x104, &mut cpu, &mut bus);
        assert!(result.is_some(), "DrawGrowIcon should be handled");
        assert!(result.unwrap().is_ok(), "DrawGrowIcon should succeed");
        assert_eq!(
            cpu.read_reg(Register::A7),
            TEST_SP,
            "DrawGrowIcon should pop one WindowPtr argument"
        );

        let sep_x = 100 - 15;
        let sep_y = 70 - 15;
        let row_bytes = 64;

        assert!(
            read_screen_pixel_1bpp(&bus, screen_base, row_bytes, sep_x, 30),
            "vertical delimiter should start at content top"
        );
        assert!(
            read_screen_pixel_1bpp(&bus, screen_base, row_bytes, sep_x, 69),
            "vertical delimiter should reach content bottom-1"
        );
        assert!(
            read_screen_pixel_1bpp(&bus, screen_base, row_bytes, 19, sep_y),
            "horizontal delimiter should start at content left-1"
        );
        assert!(
            read_screen_pixel_1bpp(&bus, screen_base, row_bytes, 101, sep_y),
            "horizontal delimiter should reach content right+1"
        );
    }

    // Inside Macintosh Volume I (1985), p. I-287: DrawGrowIcon is a
    // PROCEDURE DrawGrowIcon(theWindow: WindowPtr).
    #[test]
    fn drawgrowicon_consumes_windowptr_argument() {
        let (mut disp, mut cpu, mut bus) = setup();
        let window_ptr = 0x234000u32;
        let sp = TEST_SP - 4;
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, window_ptr);

        let result = disp.dispatch_toolbox(true, 0x104, &mut cpu, &mut bus);
        assert!(result.is_some(), "DrawGrowIcon should be handled");
        assert!(result.unwrap().is_ok(), "DrawGrowIcon should succeed");
        assert_eq!(
            cpu.read_reg(Register::A7),
            TEST_SP,
            "DrawGrowIcon should pop one WindowPtr argument"
        );
    }

    // Munger ($A9E0)
    // IM:I 1985 p. I-468: replace first found target and return offset of
    // first byte past replacement.
    #[test]
    fn munger_replaces_first_occurrence_and_returns_offset_past_replacement() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let ptr1 = 0x310000u32;
        let ptr2 = 0x310100u32;
        bus.write_bytes(ptr1, b"^0");
        bus.write_bytes(ptr2, b"Ace");

        let data_ptr = bus.alloc(9);
        bus.write_bytes(data_ptr, b"Hello, ^0");
        let handle = bus.alloc(4);
        bus.write_long(handle, data_ptr);

        bus.write_long(sp, 3); // len2
        bus.write_long(sp + 4, ptr2);
        bus.write_long(sp + 8, 2); // len1
        bus.write_long(sp + 12, ptr1);
        bus.write_long(sp + 16, 0); // offset
        bus.write_long(sp + 20, handle);
        bus.write_long(sp + 24, 0);

        let result = disp.dispatch_toolbox(true, 0x1E0, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_long(sp + 24), 10);
        assert_eq!(cpu.read_reg(Register::A7), sp + 24);

        let final_ptr = bus.read_long(handle);
        let final_size = bus.get_alloc_size(final_ptr).unwrap();
        assert_eq!(
            bus.read_bytes(final_ptr, final_size as usize),
            b"Hello, Ace"
        );
    }

    // XMunger ($A819) is a phantom trap word that BasiliskII exposes
    // as a no-op/no-pop stub.
    #[test]
    fn xmunger_phantom_noop_leaves_handle_unchanged_and_stack_unbalanced() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let ptr1 = 0x312000u32;
        let ptr2 = 0x312100u32;
        bus.write_bytes(ptr1, b"^0");
        bus.write_bytes(ptr2, b"Ace");

        let data_ptr = bus.alloc(9);
        bus.write_bytes(data_ptr, b"Hello, ^0");
        let handle = bus.alloc(4);
        bus.write_long(handle, data_ptr);

        bus.write_long(sp, 3); // len2
        bus.write_long(sp + 4, ptr2);
        bus.write_long(sp + 8, 2); // len1
        bus.write_long(sp + 12, ptr1);
        bus.write_long(sp + 16, 0); // offset
        bus.write_long(sp + 20, handle);
        bus.write_long(sp + 24, 0);

        let result = disp.dispatch_toolbox(true, 0x019, &mut cpu, &mut bus);

        assert!(result.is_some(), "XMunger should be handled");
        assert!(result.unwrap().is_ok(), "XMunger should succeed");
        assert_eq!(bus.read_long(sp + 24), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp);
        assert_eq!(
            bus.read_long(handle),
            data_ptr,
            "XMunger should leave the handle pointer unchanged"
        );

        let final_ptr = bus.read_long(handle);
        let final_size = bus.get_alloc_size(final_ptr).unwrap();
        assert_eq!(bus.read_bytes(final_ptr, final_size as usize), b"Hello, ^0");
    }

    // IM:I 1985 p. I-469: if ptr2 is NIL, return match offset and leave
    // destination bytes unchanged.
    #[test]
    fn munger_search_only_mode_returns_match_offset_without_modifying_destination() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let ptr1 = 0x310000u32;
        bus.write_bytes(ptr1, b"the");

        let initial = b"there's the apple";
        let data_ptr = bus.alloc(initial.len() as u32);
        bus.write_bytes(data_ptr, initial);
        let handle = bus.alloc(4);
        bus.write_long(handle, data_ptr);

        bus.write_long(sp, 0); // len2
        bus.write_long(sp + 4, 0); // ptr2 = NIL
        bus.write_long(sp + 8, 3); // len1
        bus.write_long(sp + 12, ptr1);
        bus.write_long(sp + 16, 4); // offset
        bus.write_long(sp + 20, handle);
        bus.write_long(sp + 24, 0);

        let result = disp.dispatch_toolbox(true, 0x1E0, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_long(sp + 24), 8);
        assert_eq!(cpu.read_reg(Register::A7), sp + 24);

        let final_ptr = bus.read_long(handle);
        let final_size = bus.get_alloc_size(final_ptr).unwrap();
        assert_eq!(bus.read_bytes(final_ptr, final_size as usize), initial);
    }

    // IM:I 1985 p. I-469: len1 == 0 inserts replacement bytes at offset and
    // returns first byte past insertion.
    #[test]
    fn munger_len1_zero_inserts_replacement_at_offset() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let ptr2 = 0x310100u32;
        bus.write_bytes(ptr2, b"X");

        let data_ptr = bus.alloc(5);
        bus.write_bytes(data_ptr, b"apple");
        let handle = bus.alloc(4);
        bus.write_long(handle, data_ptr);

        bus.write_long(sp, 1); // len2
        bus.write_long(sp + 4, ptr2);
        bus.write_long(sp + 8, 0); // len1
        bus.write_long(sp + 12, 0); // ptr1 ignored
        bus.write_long(sp + 16, 2); // offset
        bus.write_long(sp + 20, handle);
        bus.write_long(sp + 24, 0);

        let result = disp.dispatch_toolbox(true, 0x1E0, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_long(sp + 24), 3);
        assert_eq!(cpu.read_reg(Register::A7), sp + 24);

        let final_ptr = bus.read_long(handle);
        let final_size = bus.get_alloc_size(final_ptr).unwrap();
        assert_eq!(bus.read_bytes(final_ptr, final_size as usize), b"apXple");
    }

    // IM:I 1985 p. I-469: len2 == 0 with non-NIL ptr2 deletes the target
    // substring and returns the deletion offset.
    #[test]
    fn munger_len2_zero_deletes_target_and_returns_deletion_offset() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let ptr1 = 0x310000u32;
        let ptr2 = 0x310100u32;
        bus.write_bytes(ptr1, b"123");
        bus.write_bytes(ptr2, b"z");

        let data_ptr = bus.alloc(9);
        bus.write_bytes(data_ptr, b"abc123def");
        let handle = bus.alloc(4);
        bus.write_long(handle, data_ptr);

        bus.write_long(sp, 0); // len2
        bus.write_long(sp + 4, ptr2); // ptr2 non-NIL keeps delete path
        bus.write_long(sp + 8, 3); // len1
        bus.write_long(sp + 12, ptr1);
        bus.write_long(sp + 16, 0); // offset
        bus.write_long(sp + 20, handle);
        bus.write_long(sp + 24, 0);

        let result = disp.dispatch_toolbox(true, 0x1E0, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_long(sp + 24), 3);
        assert_eq!(cpu.read_reg(Register::A7), sp + 24);

        let final_ptr = bus.read_long(handle);
        let final_size = bus.get_alloc_size(final_ptr).unwrap();
        assert_eq!(bus.read_bytes(final_ptr, final_size as usize), b"abcdef");
    }

    // IM:I 1985 p. I-469: returns a negative value when target is not found.
    #[test]
    fn munger_returns_negative_when_target_not_found() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let ptr1 = 0x310000u32;
        let ptr2 = 0x310100u32;
        bus.write_bytes(ptr1, b"zz");
        bus.write_bytes(ptr2, b"A");

        let data_ptr = bus.alloc(5);
        bus.write_bytes(data_ptr, b"hello");
        let handle = bus.alloc(4);
        bus.write_long(handle, data_ptr);

        bus.write_long(sp, 1); // len2
        bus.write_long(sp + 4, ptr2);
        bus.write_long(sp + 8, 2); // len1
        bus.write_long(sp + 12, ptr1);
        bus.write_long(sp + 16, 0); // offset
        bus.write_long(sp + 20, handle);
        bus.write_long(sp + 24, 0);

        let result = disp.dispatch_toolbox(true, 0x1E0, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_long(sp + 24) as i32, -1);
        assert_eq!(cpu.read_reg(Register::A7), sp + 24);

        let final_ptr = bus.read_long(handle);
        let final_size = bus.get_alloc_size(final_ptr).unwrap();
        assert_eq!(bus.read_bytes(final_ptr, final_size as usize), b"hello");
    }

    // BasiliskII/System 7.5 ROM: if the tail at offset only partially
    // matches the beginning of the target, return -1 and leave the
    // destination unchanged.
    #[test]
    fn munger_partial_tail_match_at_offset_returns_negative_and_leaves_tail_unchanged() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let ptr1 = 0x310000u32;
        let ptr2 = 0x310100u32;
        bus.write_bytes(ptr1, b"abc");
        bus.write_bytes(ptr2, b"Z");

        let data_ptr = bus.alloc(2);
        bus.write_bytes(data_ptr, b"ab");
        let handle = bus.alloc(4);
        bus.write_long(handle, data_ptr);

        bus.write_long(sp, 1); // len2
        bus.write_long(sp + 4, ptr2);
        bus.write_long(sp + 8, 3); // len1
        bus.write_long(sp + 12, ptr1);
        bus.write_long(sp + 16, 0); // offset
        bus.write_long(sp + 20, handle);
        bus.write_long(sp + 24, 0);

        let result = disp.dispatch_toolbox(true, 0x1E0, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_long(sp + 24) as i32, -1);
        assert_eq!(cpu.read_reg(Register::A7), sp + 24);

        let final_ptr = bus.read_long(handle);
        let final_size = bus.get_alloc_size(final_ptr).unwrap();
        assert_eq!(bus.read_bytes(final_ptr, final_size as usize), b"ab");
    }

    // PBOpenRF ($A00A) — OS trap, file not found
    #[test]
    fn test_pb_open_rf_not_found() {
        let (mut disp, mut cpu, mut bus) = setup();
        let pb = 0x300000u32;
        cpu.write_reg(Register::A0, pb);

        // Set up Pascal string filename at 0x310000
        let name_ptr = 0x310000u32;
        bus.write_pstring(name_ptr, b"MissingFile.rsrc");

        // Write name_ptr into param block at pb+18
        bus.write_long(pb + 18, name_ptr);
        // Clear ioResult at pb+16
        bus.write_word(pb + 16, 0);

        let result = disp.dispatch_toolbox(false, 0x0A, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        // ioResult at pb+16 = -43 (fnfErr)
        assert_eq!(bus.read_word(pb + 16), (-43i16) as u16);
        assert_eq!(cpu.read_reg(Register::D0), (-43i32) as u32);
    }

    // HCreateResFile ($A81B) — missing file creation plus PBOpenRF visibility.
    #[test]
    fn test_hcreate_res_file_creates_missing_file_and_resource_fork() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let name_ptr = 0x310000u32;
        bus.write_pstring(name_ptr, b"Prefs.RSRC");

        bus.write_long(sp, name_ptr);
        bus.write_long(sp + 4, 0); // dirID
        bus.write_word(sp + 8, 0); // vRefNum
        cpu.write_reg(Register::A7, sp);

        let result = disp.dispatch_toolbox(true, 0x01B, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
        assert_eq!(bus.read_word(0x0A60), 0);
        assert!(disp.vfs.contains_key("Prefs.RSRC"));
        assert!(disp.vfs_rsrc.contains_key("Prefs.RSRC"));

        let pb = 0x300000u32;
        cpu.write_reg(Register::A0, pb);
        bus.write_long(pb + 18, name_ptr);
        bus.write_word(pb + 22, 0);
        bus.write_byte(pb + 27, 3); // fsRdWrPerm
        bus.write_long(pb + 48, 0);

        let result = disp.dispatch_toolbox(false, 0x0A, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(pb + 16), 0);
        assert!(bus.read_word(pb + 24) > 0);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    #[test]
    fn test_hcreate_res_file_existing_resource_fork_returns_dupfnerr_and_preserves_file() {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.vfs.insert("Prefs.RSRC".to_string(), vec![0x11, 0x22]);
        disp.vfs_rsrc
            .insert("Prefs.RSRC".to_string(), vec![0x33, 0x44]);

        let sp = TEST_SP;
        let name_ptr = 0x310100u32;
        bus.write_pstring(name_ptr, b"Prefs.RSRC");
        bus.write_long(sp, name_ptr);
        bus.write_long(sp + 4, 0);
        bus.write_word(sp + 8, 0);
        cpu.write_reg(Register::A7, sp);

        let result = disp.dispatch_toolbox(true, 0x01B, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
        assert_eq!(bus.read_word(0x0A60) as i16, -48);
        assert_eq!(disp.vfs.get("Prefs.RSRC").unwrap(), &vec![0x11, 0x22]);
        assert_eq!(disp.vfs_rsrc.get("Prefs.RSRC").unwrap(), &vec![0x33, 0x44]);
    }

    // PBOpenRF on a freshly-PBCreate'd rsrc fork that's already had
    // FSWrite bytes appended must NOT clobber the in-memory rsrc bytes
    // (vfs[__rsrc__name]) with the snapshot from vfs_rsrc.
    #[test]
    fn test_pb_open_rf_preserves_in_progress_writes() {
        let (mut disp, mut cpu, mut bus) = setup();

        let name = "InstallerTemp";
        let rsrc_key = "__rsrc__InstallerTemp";
        // Simulate a previous open that wrote 4 bytes through the
        // open-files path. vfs has the new bytes; vfs_rsrc still has
        // the original empty snapshot.
        disp.vfs_rsrc.insert(name.to_string(), Vec::new());
        disp.vfs
            .insert(rsrc_key.to_string(), vec![0xCA, 0xFE, 0xBA, 0xBE]);

        let pb = 0x300000u32;
        cpu.write_reg(Register::A0, pb);
        let name_ptr = 0x310000u32;
        bus.write_pstring(name_ptr, name.as_bytes());
        bus.write_long(pb + 18, name_ptr);

        disp.dispatch_toolbox(false, 0x0A, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert_eq!(
            disp.vfs.get(rsrc_key).unwrap(),
            &vec![0xCA, 0xFE, 0xBA, 0xBE],
            "Re-opening must not clobber the in-progress rsrc-fork \
             writes with the on-disk snapshot"
        );
    }

    // Pack0 / List Manager ($A9E7) — LNew selector $0044
    // IM:IV 1986 p. IV-269: LNew returns ListHandle; selFlags=0 and active=TRUE.
    #[test]
    fn pack0_lnew_returns_non_nil_listhandle_with_default_selection_and_active_flags() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let view_rect_ptr = 0x350000u32;
        let data_bounds_ptr = 0x350100u32;
        let window_ptr = 0x210000u32;

        // view = (0,0)-(40,80), dataBounds = rows 0..2, cols 0..1
        bus.write_word(view_rect_ptr, 0);
        bus.write_word(view_rect_ptr + 2, 0);
        bus.write_word(view_rect_ptr + 4, 40);
        bus.write_word(view_rect_ptr + 6, 80);
        bus.write_word(data_bounds_ptr, 0);
        bus.write_word(data_bounds_ptr + 2, 0);
        bus.write_word(data_bounds_ptr + 4, 2);
        bus.write_word(data_bounds_ptr + 6, 1);

        bus.write_word(sp, 0x0044); // LNew selector
        bus.write_word(sp + 2, 0x0100); // drawIt = TRUE
        bus.write_word(sp + 4, 0); // hasGrow = FALSE
        bus.write_word(sp + 6, 0); // scrollHoriz = FALSE
        bus.write_word(sp + 8, 0); // scrollVert = FALSE
        bus.write_long(sp + 10, window_ptr);
        bus.write_word(sp + 14, 0); // default LDEF
        bus.write_word(sp + 16, 0); // cSize.v => default
        bus.write_word(sp + 18, 0); // cSize.h => default
        bus.write_long(sp + 20, data_bounds_ptr);
        bus.write_long(sp + 24, view_rect_ptr);
        bus.write_long(sp + 28, 0); // result slot

        let result = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 28);

        let list_handle = bus.read_long(sp + 28);
        assert_ne!(list_handle, 0);
        let list_ptr = bus.read_long(list_handle);
        assert_ne!(list_ptr, 0);
        assert_eq!(
            bus.read_byte(list_ptr + 36),
            0,
            "selFlags should default to 0"
        );
        assert_eq!(
            bus.read_byte(list_ptr + 37),
            1,
            "lActive should default to TRUE"
        );
    }

    // Pack0 / List Manager ($A9E7) — LAddRow selector $0008
    // IM:IV 1986 p. IV-271: returns first added row and increases dataBounds.bottom.
    #[test]
    fn pack0_laddrow_returns_insert_row_and_extends_databounds_bottom() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let view_rect_ptr = 0x351000u32;
        let data_bounds_ptr = 0x351100u32;

        bus.write_word(view_rect_ptr, 0);
        bus.write_word(view_rect_ptr + 2, 0);
        bus.write_word(view_rect_ptr + 4, 40);
        bus.write_word(view_rect_ptr + 6, 80);
        bus.write_word(data_bounds_ptr, 0);
        bus.write_word(data_bounds_ptr + 2, 0);
        bus.write_word(data_bounds_ptr + 4, 2);
        bus.write_word(data_bounds_ptr + 6, 1);

        bus.write_word(sp, 0x0044); // LNew
        bus.write_word(sp + 2, 0);
        bus.write_word(sp + 4, 0);
        bus.write_word(sp + 6, 0);
        bus.write_word(sp + 8, 0);
        bus.write_long(sp + 10, 0x210000);
        bus.write_word(sp + 14, 0);
        bus.write_word(sp + 16, 10);
        bus.write_word(sp + 18, 40);
        bus.write_long(sp + 20, data_bounds_ptr);
        bus.write_long(sp + 24, view_rect_ptr);
        bus.write_long(sp + 28, 0);
        let create = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(create.is_some());
        assert!(create.unwrap().is_ok());
        let list_handle = bus.read_long(sp + 28);

        cpu.write_reg(Register::A7, sp);
        // Pascal calling convention: lHandle (last arg) is closest to the
        // selector, count (first arg) is at the deepest slot.
        bus.write_word(sp, 0x0008); // LAddRow
        bus.write_long(sp + 2, list_handle);
        bus.write_word(sp + 6, 1); // rowNum
        bus.write_word(sp + 8, 1); // count
        bus.write_word(sp + 10, 0xBEEF); // INTEGER result slot
        let add = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(add.is_some());
        assert!(add.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 10), 1);
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);

        let list_ptr = bus.read_long(list_handle);
        let data_bounds_bottom = bus.read_word(list_ptr + 76) as i16;
        assert_eq!(data_bounds_bottom, 3);
    }

    // Pack0 / List Manager ($A9E7) — LAddToCell selector $000C
    // IM:IV 1986 p. IV-272: append bytes into an existing cell; invalid cells are ignored.
    #[test]
    fn pack0_laddtocell_appends_data_and_ignores_invalid_cells() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let view_rect_ptr = 0x352000u32;
        let data_bounds_ptr = 0x352100u32;
        let seed_ptr = 0x352200u32;
        let append_ptr = 0x352210u32;
        let valid_out_ptr = 0x352220u32;
        let valid_len_ptr = 0x352230u32;

        bus.write_word(view_rect_ptr, 0);
        bus.write_word(view_rect_ptr + 2, 0);
        bus.write_word(view_rect_ptr + 4, 48);
        bus.write_word(view_rect_ptr + 6, 80);
        bus.write_word(data_bounds_ptr, 0);
        bus.write_word(data_bounds_ptr + 2, 0);
        bus.write_word(data_bounds_ptr + 4, 1);
        bus.write_word(data_bounds_ptr + 6, 1);

        bus.write_word(sp, 0x0044); // LNew
        bus.write_word(sp + 2, 0);
        bus.write_word(sp + 4, 0);
        bus.write_word(sp + 6, 0);
        bus.write_word(sp + 8, 0);
        bus.write_long(sp + 10, 0x210000);
        bus.write_word(sp + 14, 0);
        bus.write_word(sp + 16, 12);
        bus.write_word(sp + 18, 24);
        bus.write_long(sp + 20, data_bounds_ptr);
        bus.write_long(sp + 24, view_rect_ptr);
        bus.write_long(sp + 28, 0);
        let create = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(create.is_some());
        assert!(create.unwrap().is_ok());
        let list_handle = bus.read_long(sp + 28);
        assert_ne!(list_handle, 0);

        bus.write_bytes(seed_ptr, b"A");
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0058); // LSetCell
        bus.write_long(sp + 2, list_handle);
        bus.write_word(sp + 6, 0);
        bus.write_word(sp + 8, 0);
        bus.write_word(sp + 10, 1);
        bus.write_long(sp + 12, seed_ptr);
        let set_seed = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(set_seed.is_some());
        assert!(set_seed.unwrap().is_ok());

        bus.write_bytes(append_ptr, b"BC");
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x000C); // LAddToCell
        bus.write_long(sp + 2, list_handle);
        bus.write_word(sp + 6, 0);
        bus.write_word(sp + 8, 0);
        bus.write_word(sp + 10, 2);
        bus.write_long(sp + 12, append_ptr);
        let add = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(add.is_some());
        assert!(add.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 16);

        bus.write_word(valid_len_ptr, 8);
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0038); // LGetCell
        bus.write_long(sp + 2, list_handle);
        bus.write_word(sp + 6, 0);
        bus.write_word(sp + 8, 0);
        bus.write_long(sp + 10, valid_len_ptr);
        bus.write_long(sp + 14, valid_out_ptr);
        let get_valid = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(get_valid.is_some());
        assert!(get_valid.unwrap().is_ok());
        assert_eq!(bus.read_word(valid_len_ptr), 3);
        assert_eq!(bus.read_byte(valid_out_ptr), b'A');
        assert_eq!(bus.read_byte(valid_out_ptr + 1), b'B');
        assert_eq!(bus.read_byte(valid_out_ptr + 2), b'C');

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x000C); // LAddToCell
        bus.write_long(sp + 2, list_handle);
        bus.write_word(sp + 6, 1); // invalid row
        bus.write_word(sp + 8, 0);
        bus.write_word(sp + 10, 2);
        bus.write_long(sp + 12, append_ptr);
        let add_invalid = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(add_invalid.is_some());
        assert!(add_invalid.unwrap().is_ok());

        bus.write_word(valid_len_ptr, 8);
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0038); // LGetCell
        bus.write_long(sp + 2, list_handle);
        bus.write_word(sp + 6, 0);
        bus.write_word(sp + 8, 0);
        bus.write_long(sp + 10, valid_len_ptr);
        bus.write_long(sp + 14, valid_out_ptr);
        let get_invalid = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(get_invalid.is_some());
        assert!(get_invalid.unwrap().is_ok());
        assert_eq!(bus.read_word(valid_len_ptr), 3);
        assert_eq!(bus.read_byte(valid_out_ptr), b'A');
        assert_eq!(bus.read_byte(valid_out_ptr + 1), b'B');
        assert_eq!(bus.read_byte(valid_out_ptr + 2), b'C');

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0028); // LDispose
        bus.write_long(sp + 2, list_handle);
        let dispose = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(dispose.is_some());
        assert!(dispose.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 6);
    }

    // Pack0 / List Manager ($A9E7) — LDelRow selector $0024
    // IM:IV 1986 p. IV-271: rows after rowNum shift by count; dataBounds.bottom decreases.
    #[test]
    fn pack0_ldelrow_deletes_rows_compacts_following_rows_and_reduces_databounds_bottom() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let view_rect_ptr = 0x352000u32;
        let data_bounds_ptr = 0x352100u32;
        let data_a_ptr = 0x352200u32;
        let data_b_ptr = 0x352210u32;
        let out_ptr = 0x352220u32;
        let out_len_ptr = 0x352230u32;

        bus.write_word(view_rect_ptr, 0);
        bus.write_word(view_rect_ptr + 2, 0);
        bus.write_word(view_rect_ptr + 4, 40);
        bus.write_word(view_rect_ptr + 6, 80);
        bus.write_word(data_bounds_ptr, 0);
        bus.write_word(data_bounds_ptr + 2, 0);
        bus.write_word(data_bounds_ptr + 4, 2); // two rows: 0 and 1
        bus.write_word(data_bounds_ptr + 6, 1); // one column

        bus.write_word(sp, 0x0044); // LNew
        bus.write_word(sp + 2, 0);
        bus.write_word(sp + 4, 0);
        bus.write_word(sp + 6, 0);
        bus.write_word(sp + 8, 0);
        bus.write_long(sp + 10, 0x210000);
        bus.write_word(sp + 14, 0);
        bus.write_word(sp + 16, 10);
        bus.write_word(sp + 18, 40);
        bus.write_long(sp + 20, data_bounds_ptr);
        bus.write_long(sp + 24, view_rect_ptr);
        bus.write_long(sp + 28, 0);
        let create = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(create.is_some());
        assert!(create.unwrap().is_ok());
        let list_handle = bus.read_long(sp + 28);

        // LSetCell row 0 = "A"
        bus.write_bytes(data_a_ptr, b"A");
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0058); // LSetCell
        bus.write_long(sp + 2, list_handle);
        bus.write_word(sp + 6, 0); // row
        bus.write_word(sp + 8, 0); // col
        bus.write_word(sp + 10, 1); // dataLen
        bus.write_long(sp + 12, data_a_ptr);
        let set_a = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(set_a.is_some());
        assert!(set_a.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 16);

        // LSetCell row 1 = "B"
        bus.write_bytes(data_b_ptr, b"B");
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0058); // LSetCell
        bus.write_long(sp + 2, list_handle);
        bus.write_word(sp + 6, 1); // row
        bus.write_word(sp + 8, 0); // col
        bus.write_word(sp + 10, 1); // dataLen
        bus.write_long(sp + 12, data_b_ptr);
        let set_b = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(set_b.is_some());
        assert!(set_b.unwrap().is_ok());

        // Delete row 0; row 1 should compact into row 0.
        // Pascal order: lHandle deepest under selector, then rowNum, then count.
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0024); // LDelRow
        bus.write_long(sp + 2, list_handle);
        bus.write_word(sp + 6, 0); // rowNum
        bus.write_word(sp + 8, 1); // count
        let del = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(del.is_some());
        assert!(del.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0038); // LGetCell
        bus.write_long(sp + 2, list_handle);
        bus.write_word(sp + 6, 0); // row
        bus.write_word(sp + 8, 0); // col
        bus.write_long(sp + 10, out_len_ptr); // VAR dataLen
        bus.write_long(sp + 14, out_ptr); // dataPtr
        bus.write_word(out_len_ptr, 1); // max bytes to copy
        let get = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(get.is_some());
        assert!(get.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 18);
        assert_eq!(bus.read_word(out_len_ptr), 1);
        assert_eq!(bus.read_byte(out_ptr), b'B');

        let list_ptr = bus.read_long(list_handle);
        let data_bounds_bottom = bus.read_word(list_ptr + 76) as i16;
        assert_eq!(data_bounds_bottom, 1);
    }

    // Pack0 / List Manager ($A9E7) — LSetSelect selector $005C and
    // LGetSelect selector $003C.
    // IM:IV 1986 p. IV-273: selection toggles are driven by
    // LSetSelect(setIt,theCell,lHandle), and LGetSelect checks a
    // specific cell or searches from a probe cell forward.
    #[test]
    fn pack0_lsetselect_lgetselect_use_pascal_argument_order_and_selection_state() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let view_rect_ptr = 0x353400u32;
        let data_bounds_ptr = 0x353500u32;
        let query_cell_ptr = 0x353600u32;

        bus.write_word(view_rect_ptr, 0);
        bus.write_word(view_rect_ptr + 2, 0);
        bus.write_word(view_rect_ptr + 4, 48);
        bus.write_word(view_rect_ptr + 6, 80);
        bus.write_word(data_bounds_ptr, 0);
        bus.write_word(data_bounds_ptr + 2, 0);
        bus.write_word(data_bounds_ptr + 4, 3);
        bus.write_word(data_bounds_ptr + 6, 1);

        bus.write_word(sp, 0x0044); // LNew
        bus.write_word(sp + 2, 0);
        bus.write_word(sp + 4, 0);
        bus.write_word(sp + 6, 0);
        bus.write_word(sp + 8, 0);
        bus.write_long(sp + 10, 0x210000);
        bus.write_word(sp + 14, 0);
        bus.write_word(sp + 16, 16);
        bus.write_word(sp + 18, 32);
        bus.write_long(sp + 20, data_bounds_ptr);
        bus.write_long(sp + 24, view_rect_ptr);
        bus.write_long(sp + 28, 0);
        let create = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(create.is_some());
        assert!(create.unwrap().is_ok());
        let list_handle = bus.read_long(sp + 28);

        // LSetSelect(TRUE, Cell(1,0), hList) with Pascal order:
        // lHandle closest to selector, then Cell, then Boolean.
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x005C); // LSetSelect
        bus.write_long(sp + 2, list_handle);
        bus.write_word(sp + 6, 1); // cell.v = row
        bus.write_word(sp + 8, 0); // cell.h = col
        bus.write_word(sp + 10, 0x0100); // setIt = TRUE
        let set = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(set.is_some());
        assert!(set.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 12);

        // LGetSelect(FALSE, &Cell(1,0), hList) should report selected.
        bus.write_word(query_cell_ptr, 1);
        bus.write_word(query_cell_ptr + 2, 0);
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x003C); // LGetSelect
        bus.write_long(sp + 2, list_handle);
        bus.write_long(sp + 6, query_cell_ptr);
        bus.write_word(sp + 10, 0x0000); // next = FALSE
        bus.write_word(sp + 12, 0xBEEF);
        let get_exact = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(get_exact.is_some());
        assert!(get_exact.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 12), 0x0100);
        assert_eq!(cpu.read_reg(Register::A7), sp + 12);
        assert_eq!(bus.read_word(query_cell_ptr), 1);
        assert_eq!(bus.read_word(query_cell_ptr + 2), 0);

        // LGetSelect(TRUE, &Cell(0,0), hList) should advance the probe
        // to the selected cell at or after the starting point.
        bus.write_word(query_cell_ptr, 0);
        bus.write_word(query_cell_ptr + 2, 0);
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x003C); // LGetSelect
        bus.write_long(sp + 2, list_handle);
        bus.write_long(sp + 6, query_cell_ptr);
        bus.write_word(sp + 10, 0x0100); // next = TRUE
        bus.write_word(sp + 12, 0xBEEF);
        let get_next = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(get_next.is_some());
        assert!(get_next.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 12), 0x0100);
        assert_eq!(cpu.read_reg(Register::A7), sp + 12);
        assert_eq!(bus.read_word(query_cell_ptr), 1);
        assert_eq!(bus.read_word(query_cell_ptr + 2), 0);

        // LSetSelect(FALSE, Cell(1,0), hList) should clear the cell.
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x005C); // LSetSelect
        bus.write_long(sp + 2, list_handle);
        bus.write_word(sp + 6, 1);
        bus.write_word(sp + 8, 0);
        bus.write_word(sp + 10, 0x00FF); // setIt = FALSE; low byte is padding garbage
        let clear = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(clear.is_some());
        assert!(clear.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 12);

        bus.write_word(query_cell_ptr, 1);
        bus.write_word(query_cell_ptr + 2, 0);
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x003C); // LGetSelect
        bus.write_long(sp + 2, list_handle);
        bus.write_long(sp + 6, query_cell_ptr);
        bus.write_word(sp + 10, 0x0000); // next = FALSE
        bus.write_word(sp + 12, 0xBEEF);
        let get_cleared = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(get_cleared.is_some());
        assert!(get_cleared.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 12), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 12);
    }

    // Pack0 / List Manager ($A9E7) — LClick selector $0018
    // IM:IV 1986 p. IV-273: TRUE on double-click in same cell; LLastClick reports clicked cell.
    #[test]
    fn pack0_lclick_same_cell_double_click_returns_true_and_lastclick_tracks_cell() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let view_rect_ptr = 0x353000u32;
        let data_bounds_ptr = 0x353100u32;

        bus.write_word(view_rect_ptr, 0);
        bus.write_word(view_rect_ptr + 2, 0);
        bus.write_word(view_rect_ptr + 4, 40);
        bus.write_word(view_rect_ptr + 6, 80);
        bus.write_word(data_bounds_ptr, 0);
        bus.write_word(data_bounds_ptr + 2, 0);
        bus.write_word(data_bounds_ptr + 4, 2);
        bus.write_word(data_bounds_ptr + 6, 1);

        bus.write_word(sp, 0x0044); // LNew
        bus.write_word(sp + 2, 0);
        bus.write_word(sp + 4, 0);
        bus.write_word(sp + 6, 0);
        bus.write_word(sp + 8, 0);
        bus.write_long(sp + 10, 0x210000);
        bus.write_word(sp + 14, 0);
        bus.write_word(sp + 16, 10); // cSize.v
        bus.write_word(sp + 18, 20); // cSize.h
        bus.write_long(sp + 20, data_bounds_ptr);
        bus.write_long(sp + 24, view_rect_ptr);
        bus.write_long(sp + 28, 0);
        let create = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(create.is_some());
        assert!(create.unwrap().is_ok());
        let list_handle = bus.read_long(sp + 28);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0018); // LClick
        bus.write_long(sp + 2, list_handle);
        bus.write_word(sp + 6, 0); // modifiers
        bus.write_word(sp + 8, 5); // pt.v in row 0
        bus.write_word(sp + 10, 5); // pt.h in col 0
        bus.write_word(sp + 12, 0xBEEF); // Boolean result
        let first = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(first.is_some());
        assert!(first.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 12), 0);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0018); // LClick
        bus.write_long(sp + 2, list_handle);
        bus.write_word(sp + 6, 0);
        bus.write_word(sp + 8, 5);
        bus.write_word(sp + 10, 5);
        bus.write_word(sp + 12, 0xBEEF);
        let second = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(second.is_some());
        assert!(second.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 12), 0x0100);
        assert_eq!(cpu.read_reg(Register::A7), sp + 12);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0040); // LLastClick
        bus.write_long(sp + 2, list_handle);
        bus.write_long(sp + 6, 0xFFFF_FFFF); // result cell placeholder
        let last_click = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(last_click.is_some());
        assert!(last_click.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 6), 0); // row
        assert_eq!(bus.read_word(sp + 8), 0); // col
        assert_eq!(cpu.read_reg(Register::A7), sp + 6);
    }

    #[test]
    fn pack0_lclick_miss_clears_lastclick_history() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let view_rect_ptr = 0x355000u32;
        let data_bounds_ptr = 0x355100u32;

        bus.write_word(view_rect_ptr, 0);
        bus.write_word(view_rect_ptr + 2, 0);
        bus.write_word(view_rect_ptr + 4, 40);
        bus.write_word(view_rect_ptr + 6, 80);
        bus.write_word(data_bounds_ptr, 0);
        bus.write_word(data_bounds_ptr + 2, 0);
        bus.write_word(data_bounds_ptr + 4, 2);
        bus.write_word(data_bounds_ptr + 6, 1);

        bus.write_word(sp, 0x0044); // LNew
        bus.write_word(sp + 2, 0);
        bus.write_word(sp + 4, 0);
        bus.write_word(sp + 6, 0);
        bus.write_word(sp + 8, 0);
        bus.write_long(sp + 10, 0x210200);
        bus.write_word(sp + 14, 0);
        bus.write_word(sp + 16, 10); // cSize.v
        bus.write_word(sp + 18, 20); // cSize.h
        bus.write_long(sp + 20, data_bounds_ptr);
        bus.write_long(sp + 24, view_rect_ptr);
        bus.write_long(sp + 28, 0);
        let create = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(create.is_some());
        assert!(create.unwrap().is_ok());
        let list_handle = bus.read_long(sp + 28);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0018); // LClick
        bus.write_long(sp + 2, list_handle);
        bus.write_word(sp + 6, 0); // modifiers
        bus.write_word(sp + 8, 5); // pt.v in row 0
        bus.write_word(sp + 10, 5); // pt.h in col 0
        bus.write_word(sp + 12, 0xBEEF);
        let first = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(first.is_some());
        assert!(first.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 12), 0);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0018); // LClick
        bus.write_long(sp + 2, list_handle);
        bus.write_word(sp + 6, 0);
        bus.write_word(sp + 8, 200); // miss
        bus.write_word(sp + 10, 200);
        bus.write_word(sp + 12, 0xBEEF);
        let miss = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(miss.is_some());
        assert!(miss.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 12), 0);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0040); // LLastClick
        bus.write_long(sp + 2, list_handle);
        bus.write_long(sp + 6, 0xFFFF_FFFF);
        let last_click = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(last_click.is_some());
        assert!(last_click.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 6), 0xFFFF);
        assert_eq!(bus.read_word(sp + 8), 0xFFFF);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0018); // LClick
        bus.write_long(sp + 2, list_handle);
        bus.write_word(sp + 6, 0);
        bus.write_word(sp + 8, 5);
        bus.write_word(sp + 10, 5);
        bus.write_word(sp + 12, 0xBEEF);
        let second = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(second.is_some());
        assert!(second.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 12), 0);
    }

    #[test]
    fn pack0_llastclick_before_any_click_returns_negative_cell() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let view_rect_ptr = 0x354000u32;
        let data_bounds_ptr = 0x354100u32;

        bus.write_word(view_rect_ptr, 0);
        bus.write_word(view_rect_ptr + 2, 0);
        bus.write_word(view_rect_ptr + 4, 40);
        bus.write_word(view_rect_ptr + 6, 80);
        bus.write_word(data_bounds_ptr, 0);
        bus.write_word(data_bounds_ptr + 2, 0);
        bus.write_word(data_bounds_ptr + 4, 1);
        bus.write_word(data_bounds_ptr + 6, 1);

        bus.write_word(sp, 0x0044); // LNew
        bus.write_word(sp + 2, 0);
        bus.write_word(sp + 4, 0);
        bus.write_word(sp + 6, 0);
        bus.write_word(sp + 8, 0);
        bus.write_long(sp + 10, 0x210100);
        bus.write_word(sp + 14, 0);
        bus.write_word(sp + 16, 10);
        bus.write_word(sp + 18, 20);
        bus.write_long(sp + 20, data_bounds_ptr);
        bus.write_long(sp + 24, view_rect_ptr);
        bus.write_long(sp + 28, 0);
        let create = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(create.is_some());
        assert!(create.unwrap().is_ok());
        let list_handle = bus.read_long(sp + 28);
        assert_ne!(list_handle, 0);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0040); // LLastClick
        bus.write_long(sp + 2, list_handle);
        bus.write_long(sp + 6, 0xBEEF_BEEF); // result cell placeholder
        let last_click = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(last_click.is_some());
        assert!(last_click.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 6);
        assert_eq!(bus.read_word(sp + 6), 0xFFFF);
        assert_eq!(bus.read_word(sp + 8), 0xFFFF);
    }

    #[test]
    fn pack0_lupdate_draws_visible_cell_text_and_restores_qd_state() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let screen_base = 0x300000u32;
        let row_bytes = 128u32;
        let window_ptr = 0x210000u32;
        let view_rect_ptr = 0x356000u32;
        let data_bounds_ptr = 0x356100u32;
        let data_ptr = 0x356200u32;

        disp.set_screen_mode_for_test(screen_base, row_bytes, 128, 96, 8);
        bus.write_long(0x0824, screen_base);
        bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 0);
        for offset in 0..(row_bytes * 96) {
            bus.write_byte(screen_base + offset, 0);
        }
        disp.init_cgraf_window(
            &mut bus,
            &mut cpu,
            window_ptr,
            screen_base,
            0,
            0,
            96,
            128,
            "",
            0,
            true,
            true,
            false,
            0,
        );

        for y in 10..22 {
            for x in 10..100 {
                bus.write_byte(screen_base + y * row_bytes + x, 255);
            }
        }

        bus.write_word(view_rect_ptr, 10);
        bus.write_word(view_rect_ptr + 2, 10);
        bus.write_word(view_rect_ptr + 4, 22);
        bus.write_word(view_rect_ptr + 6, 100);
        bus.write_word(data_bounds_ptr, 0);
        bus.write_word(data_bounds_ptr + 2, 0);
        bus.write_word(data_bounds_ptr + 4, 1);
        bus.write_word(data_bounds_ptr + 6, 1);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0044); // LNew
        bus.write_word(sp + 2, 0x0100); // drawIt = TRUE
        bus.write_word(sp + 4, 0);
        bus.write_word(sp + 6, 0);
        bus.write_word(sp + 8, 0);
        bus.write_long(sp + 10, window_ptr);
        bus.write_word(sp + 14, 128); // custom LDEF id: fallback renderer still applies
        bus.write_word(sp + 16, 12);
        bus.write_word(sp + 18, 90);
        bus.write_long(sp + 20, data_bounds_ptr);
        bus.write_long(sp + 24, view_rect_ptr);
        bus.write_long(sp + 28, 0);
        let create = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(create.is_some());
        assert!(create.unwrap().is_ok());
        let list_handle = bus.read_long(sp + 28);

        bus.write_bytes(data_ptr, b"Mission");
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0058); // LSetCell
        bus.write_long(sp + 2, list_handle);
        bus.write_word(sp + 6, 0);
        bus.write_word(sp + 8, 0);
        bus.write_word(sp + 10, 7);
        bus.write_long(sp + 12, data_ptr);
        let set_cell = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(set_cell.is_some());
        assert!(set_cell.unwrap().is_ok());

        disp.fg_color = (0x1111, 0x2222, 0x3333);
        disp.bg_color = (0xAAAA, 0xBBBB, 0xCCCC);
        disp.pn_loc = (7, 8);
        disp.tx_mode = 2;
        disp.tx_size = 12;
        disp.sync_current_port_draw_state(&mut bus);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0064); // LUpdate
        bus.write_long(sp + 2, list_handle);
        bus.write_long(sp + 6, 0); // update region
        let update = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(update.is_some());
        assert!(update.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);

        let mut white_pixels = 0usize;
        for y in 10..22 {
            for x in 10..100 {
                if bus.read_byte(screen_base + y * row_bytes + x) == 0 {
                    white_pixels += 1;
                }
            }
        }
        assert!(
            white_pixels > 0,
            "LUpdate should render opposite-colored text pixels into a dark visible cell"
        );
        assert_eq!(disp.current_port, window_ptr);
        assert_eq!(disp.fg_color, (0x1111, 0x2222, 0x3333));
        assert_eq!(disp.bg_color, (0xAAAA, 0xBBBB, 0xCCCC));
        assert_eq!(disp.pn_loc, (7, 8));
        assert_eq!(disp.tx_mode, 2);
        assert_eq!(disp.tx_size, 12);
    }

    #[test]
    fn pack0_lupdate_draws_selected_empty_cell_with_hilite_color() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let screen_base = 0x300000u32;
        let row_bytes = 128u32;
        let window_ptr = 0x210000u32;
        let view_rect_ptr = 0x356000u32;
        let data_bounds_ptr = 0x356100u32;
        let hilite = (0x0000, 0x8000, 0x0000);

        disp.set_screen_mode_for_test(screen_base, row_bytes, 128, 96, 8);
        disp.device_clut = [[0xFFFF, 0xFFFF, 0xFFFF]; 256];
        disp.device_clut[0] = [0x0000, 0x0000, 0x0000];
        disp.device_clut[42] = [hilite.0, hilite.1, hilite.2];
        disp.hilite_color = hilite;
        bus.write_long(0x0824, screen_base);
        bus.write_word(crate::memory::globals::addr::MBAR_HEIGHT, 0);
        for offset in 0..(row_bytes * 96) {
            bus.write_byte(screen_base + offset, 255);
        }
        disp.init_cgraf_window(
            &mut bus,
            &mut cpu,
            window_ptr,
            screen_base,
            0,
            0,
            96,
            128,
            "",
            0,
            true,
            true,
            false,
            0,
        );

        bus.write_word(view_rect_ptr, 10);
        bus.write_word(view_rect_ptr + 2, 10);
        bus.write_word(view_rect_ptr + 4, 22);
        bus.write_word(view_rect_ptr + 6, 100);
        bus.write_word(data_bounds_ptr, 0);
        bus.write_word(data_bounds_ptr + 2, 0);
        bus.write_word(data_bounds_ptr + 4, 1);
        bus.write_word(data_bounds_ptr + 6, 1);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0044); // LNew
        bus.write_word(sp + 2, 0x0100); // drawIt = TRUE
        bus.write_word(sp + 4, 0);
        bus.write_word(sp + 6, 0);
        bus.write_word(sp + 8, 0);
        bus.write_long(sp + 10, window_ptr);
        bus.write_word(sp + 14, 128);
        bus.write_word(sp + 16, 12);
        bus.write_word(sp + 18, 90);
        bus.write_long(sp + 20, data_bounds_ptr);
        bus.write_long(sp + 24, view_rect_ptr);
        bus.write_long(sp + 28, 0);
        let create = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(create.is_some());
        assert!(create.unwrap().is_ok());
        let list_handle = bus.read_long(sp + 28);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x005C); // LSetSelect
        bus.write_long(sp + 2, list_handle);
        bus.write_word(sp + 6, 0);
        bus.write_word(sp + 8, 0);
        bus.write_word(sp + 10, 0x0100);
        let select = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(select.is_some());
        assert!(select.unwrap().is_ok());

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0064); // LUpdate
        bus.write_long(sp + 2, list_handle);
        bus.write_long(sp + 6, 0);
        let update = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(update.is_some());
        assert!(update.unwrap().is_ok());

        let interior = bus.read_byte(screen_base + 11 * row_bytes + 95);
        assert_eq!(
            interior, 42,
            "selected list cells should paint with HiliteColor, not plain white"
        );
    }

    #[test]
    fn pack0_lupdate_custom_ldef_arms_draw_proc_trampoline() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let view_rect_ptr = 0x357000u32;
        let data_bounds_ptr = 0x357100u32;
        let window_ptr = 0x210000u32;
        let return_pc = 0x1234_5678;

        bus.write_word(view_rect_ptr, 10);
        bus.write_word(view_rect_ptr + 2, 20);
        bus.write_word(view_rect_ptr + 4, 22);
        bus.write_word(view_rect_ptr + 6, 120);
        bus.write_word(data_bounds_ptr, 0);
        bus.write_word(data_bounds_ptr + 2, 0);
        bus.write_word(data_bounds_ptr + 4, 1);
        bus.write_word(data_bounds_ptr + 6, 1);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0044); // LNew
        bus.write_word(sp + 2, 0x0100); // drawIt = TRUE
        bus.write_word(sp + 4, 0);
        bus.write_word(sp + 6, 0);
        bus.write_word(sp + 8, 0);
        bus.write_long(sp + 10, window_ptr);
        bus.write_word(sp + 14, 128);
        bus.write_word(sp + 16, 12);
        bus.write_word(sp + 18, 100);
        bus.write_long(sp + 20, data_bounds_ptr);
        bus.write_long(sp + 24, view_rect_ptr);
        bus.write_long(sp + 28, 0);
        let create = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(create.is_some());
        assert!(create.unwrap().is_ok());
        let list_handle = bus.read_long(sp + 28);
        let list_ptr = bus.read_long(list_handle);

        let proc_addr = bus.alloc(2);
        bus.write_word(proc_addr, 0x4E56); // plausible 68k proc entry
        let proc_handle = bus.alloc(4);
        bus.write_long(proc_handle, proc_addr);
        bus.write_long(
            list_ptr + super::super::TrapDispatcher::LIST_DEF_PROC_OFFSET,
            proc_handle,
        );

        let cell_text_ptr = 0x357200u32;
        bus.write_bytes(cell_text_ptr, b"Hi");
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0058); // LSetCell
        bus.write_long(sp + 2, list_handle);
        bus.write_word(sp + 6, 0);
        bus.write_word(sp + 8, 0);
        bus.write_word(sp + 10, 2);
        bus.write_long(sp + 12, cell_text_ptr);
        let set_cell = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(set_cell.is_some());
        assert!(set_cell.unwrap().is_ok());

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x005C); // LSetSelect
        bus.write_long(sp + 2, list_handle);
        bus.write_word(sp + 6, 0);
        bus.write_word(sp + 8, 0);
        bus.write_word(sp + 10, 0x0100);
        let select = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(select.is_some());
        assert!(select.unwrap().is_ok());

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::PC, return_pc);
        bus.write_word(sp, 0x0064); // LUpdate
        bus.write_long(sp + 2, list_handle);
        bus.write_long(sp + 6, 0);
        let update = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(update.is_some());
        assert!(update.unwrap().is_ok());

        let tramp = cpu.read_reg(Register::PC);
        assert_eq!(tramp, disp.list_def_trampoline);
        assert_eq!(cpu.read_reg(Register::A7), sp + 6);
        assert_eq!(bus.read_long(sp + 6), return_pc);
        assert_eq!(
            bus.read_word(tramp + 6),
            super::super::TrapDispatcher::LIST_LDRAW_MSG as u16
        );
        assert_eq!(bus.read_word(tramp + 10), 0x0100);
        assert_eq!(bus.read_long(tramp + 40), proc_addr);
        assert_eq!(bus.read_long(tramp + 34), list_handle);
        assert_eq!(bus.read_word(tramp + 26), 0);
        assert_eq!(bus.read_word(tramp + 30), 2);
        assert_eq!(bus.read_long(tramp + 20), 0);
        let cells_handle =
            bus.read_long(list_ptr + super::super::TrapDispatcher::LIST_CELLS_OFFSET);
        let cells_ptr = bus.read_long(cells_handle);
        assert_eq!(bus.read_bytes(cells_ptr, 2), b"Hi");
        let rect_ptr = bus.read_long(tramp + 14);
        assert_eq!(bus.read_word(rect_ptr), 10);
        assert_eq!(bus.read_word(rect_ptr + 2), 20);
        assert_eq!(bus.read_word(rect_ptr + 4), 22);
        assert_eq!(bus.read_word(rect_ptr + 6), 120);
        assert_eq!(bus.read_word(tramp + 54), 0x4E75);
    }

    #[test]
    fn pack0_lclick_custom_ldef_arms_hilite_for_selection_delta() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let view_rect_ptr = 0x357300u32;
        let data_bounds_ptr = 0x357400u32;
        let window_ptr = 0x210000u32;
        let return_pc = 0x2233_4455;

        bus.write_word(view_rect_ptr, 0);
        bus.write_word(view_rect_ptr + 2, 0);
        bus.write_word(view_rect_ptr + 4, 20);
        bus.write_word(view_rect_ptr + 6, 80);
        bus.write_word(data_bounds_ptr, 0);
        bus.write_word(data_bounds_ptr + 2, 0);
        bus.write_word(data_bounds_ptr + 4, 2);
        bus.write_word(data_bounds_ptr + 6, 1);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0044); // LNew
        bus.write_word(sp + 2, 0x0100); // drawIt = TRUE
        bus.write_word(sp + 4, 0);
        bus.write_word(sp + 6, 0);
        bus.write_word(sp + 8, 0);
        bus.write_long(sp + 10, window_ptr);
        bus.write_word(sp + 14, 128);
        bus.write_word(sp + 16, 10);
        bus.write_word(sp + 18, 80);
        bus.write_long(sp + 20, data_bounds_ptr);
        bus.write_long(sp + 24, view_rect_ptr);
        bus.write_long(sp + 28, 0);
        let create = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(create.is_some());
        assert!(create.unwrap().is_ok());
        let list_handle = bus.read_long(sp + 28);
        let list_ptr = bus.read_long(list_handle);

        let proc_addr = bus.alloc(2);
        bus.write_word(proc_addr, 0x4E56); // plausible 68k proc entry
        let proc_handle = bus.alloc(4);
        bus.write_long(proc_handle, proc_addr);
        bus.write_long(
            list_ptr + super::super::TrapDispatcher::LIST_DEF_PROC_OFFSET,
            proc_handle,
        );
        bus.write_byte(
            list_ptr + super::super::TrapDispatcher::LIST_SEL_FLAGS_OFFSET,
            0x80,
        );
        disp.list_states
            .get_mut(&list_handle)
            .unwrap()
            .selected
            .insert((0, 0));

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::PC, return_pc);
        bus.write_word(sp, 0x0018); // LClick
        bus.write_long(sp + 2, list_handle);
        bus.write_word(sp + 6, 0); // modifiers
        bus.write_word(sp + 8, 15); // pt.v in row 1
        bus.write_word(sp + 10, 5); // pt.h in col 0
        bus.write_word(sp + 12, 0xBEEF);
        let click = disp.dispatch_toolbox(true, 0x1E7, &mut cpu, &mut bus);
        assert!(click.is_some());
        assert!(click.unwrap().is_ok());

        let first_tramp = disp.list_def_trampoline;
        assert_eq!(cpu.read_reg(Register::PC), first_tramp);
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
        assert_eq!(bus.read_long(sp + 8), return_pc);
        assert_eq!(bus.read_word(sp + 12), 0);
        assert_eq!(
            bus.read_word(first_tramp + 6),
            super::super::TrapDispatcher::LIST_LHILITE_MSG as u16
        );
        assert_eq!(bus.read_word(first_tramp + 10), 0);
        assert_eq!(bus.read_long(first_tramp + 20), 0);
        assert_eq!(bus.read_word(first_tramp + 54), 0x4EF9);

        let second_tramp = bus.read_long(first_tramp + 56);
        assert_ne!(second_tramp, 0);
        assert_eq!(
            bus.read_word(second_tramp + 6),
            super::super::TrapDispatcher::LIST_LHILITE_MSG as u16
        );
        assert_eq!(bus.read_word(second_tramp + 10), 0x0100);
        assert_eq!(bus.read_long(second_tramp + 20), 0x0001_0000);
        assert_eq!(bus.read_word(second_tramp + 54), 0x4E75);
        let expected = [(1, 0)]
            .into_iter()
            .collect::<std::collections::BTreeSet<_>>();
        assert_eq!(
            &disp.list_states.get(&list_handle).unwrap().selected,
            &expected
        );
    }

    // Pack1 / List Manager ($A9E8) — LNew selector $0044
    // IM:IV 1986 pp. IV-269 to IV-270: LNew returns a live handle and initializes
    // selFlags=0 with lActive=TRUE.
    #[test]
    fn pack1_lnew_returns_non_nil_listhandle_with_default_selection_and_active_flags() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let view_rect_ptr = 0x356000u32;
        let data_bounds_ptr = 0x356100u32;
        let window_ptr = 0x210800u32;

        bus.write_word(view_rect_ptr, 0);
        bus.write_word(view_rect_ptr + 2, 0);
        bus.write_word(view_rect_ptr + 4, 96);
        bus.write_word(view_rect_ptr + 6, 96);
        bus.write_word(data_bounds_ptr, 0);
        bus.write_word(data_bounds_ptr + 2, 0);
        bus.write_word(data_bounds_ptr + 4, 1);
        bus.write_word(data_bounds_ptr + 6, 1);

        bus.write_word(sp, 0x0044); // LNew
        bus.write_word(sp + 2, 0);
        bus.write_word(sp + 4, 0);
        bus.write_word(sp + 6, 0);
        bus.write_word(sp + 8, 0);
        bus.write_long(sp + 10, window_ptr);
        bus.write_word(sp + 14, 0); // default LDEF
        bus.write_word(sp + 16, 16);
        bus.write_word(sp + 18, 16);
        bus.write_long(sp + 20, data_bounds_ptr);
        bus.write_long(sp + 24, view_rect_ptr);
        bus.write_long(sp + 28, 0xDEAD_BEEF);

        let result = disp.dispatch_toolbox(true, 0x1E8, &mut cpu, &mut bus);
        assert!(result.is_some(), "Pack1 should be handled");
        assert!(result.unwrap().is_ok(), "Pack1 should return");
        assert_eq!(cpu.read_reg(Register::A7), sp + 28);
        let list_handle = bus.read_long(sp + 28);
        assert_ne!(list_handle, 0);
        let list_ptr = bus.read_long(list_handle);
        assert_ne!(list_ptr, 0);
        assert_eq!(
            bus.read_byte(list_ptr + 36),
            0,
            "selFlags should default to 0"
        );
        assert_eq!(
            bus.read_byte(list_ptr + 37),
            1,
            "lActive should default to TRUE"
        );

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0028); // LDispose
        bus.write_long(sp + 2, list_handle);
        let dispose = disp.dispatch_toolbox(true, 0x1E8, &mut cpu, &mut bus);
        assert!(dispose.is_some(), "Pack1 should be handled");
        assert!(dispose.unwrap().is_ok(), "Pack1 should return");
        assert_eq!(cpu.read_reg(Register::A7), sp + 6);
        assert!(!disp.list_states.contains_key(&list_handle));
    }

    // Pack1 / List Manager ($A9E8) — LSearch selector $0054
    // IM:IV 1986 p. IV-274: nil-list fallback returns FALSE and leaves the probe cell unchanged.
    #[test]
    fn pack1_lsearch_nil_list_returns_false_and_preserves_probe_cell() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let probe_ptr = 0x357000u32;
        let data_ptr = 0x357100u32;

        bus.write_word(probe_ptr, 0x1122);
        bus.write_word(probe_ptr + 2, 0x3344);
        bus.write_bytes(data_ptr, b"Rose");

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0054); // LSearch
        bus.write_long(sp + 2, 0); // nil list handle
        bus.write_long(sp + 6, probe_ptr);
        bus.write_long(sp + 10, 0xDEAD_BEEF); // bogus callback pointer, must not be entered
        bus.write_word(sp + 14, 4);
        bus.write_long(sp + 16, data_ptr);
        bus.write_word(sp + 20, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x1E8, &mut cpu, &mut bus);
        assert!(result.is_some(), "Pack1 should be handled");
        assert!(result.unwrap().is_ok(), "Pack1 should return");
        assert_eq!(cpu.read_reg(Register::A7), sp + 20);
        assert_eq!(bus.read_word(sp + 20), 0);
        assert_eq!(bus.read_word(probe_ptr), 0x1122);
        assert_eq!(bus.read_word(probe_ptr + 2), 0x3344);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    // PackBits / UnpackBits ($A8CF / $A8D0)
    // Inside Macintosh Volume I (1985), p. I-470.
    #[test]
    fn packbits_compresses_runs_and_advances_var_pointers() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let src_ptr_ptr = 0x320000u32;
        let dst_ptr_ptr = 0x320004u32;
        let src_data = 0x320100u32;
        let dst_data = 0x320200u32;

        bus.write_bytes(src_data, &[0xAA, 0xAA, 0xAA, 0xAA]);
        bus.write_long(src_ptr_ptr, src_data);
        bus.write_long(dst_ptr_ptr, dst_data);
        bus.write_word(sp, 4); // srcBytes
        bus.write_long(sp + 2, dst_ptr_ptr); // VAR dstPtr
        bus.write_long(sp + 6, src_ptr_ptr); // VAR srcPtr

        let result = disp.dispatch_toolbox(true, 0x0CF, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
        assert_eq!(bus.read_long(src_ptr_ptr), src_data + 4);
        assert_eq!(bus.read_long(dst_ptr_ptr), dst_data + 2);
        assert_eq!(bus.read_byte(dst_data), 0xFD); // -(4-1)
        assert_eq!(bus.read_byte(dst_data + 1), 0xAA);
    }

    #[test]
    fn packbits_literal_sequence_emits_literal_packet_and_pops_10_bytes() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let src_ptr_ptr = 0x321000u32;
        let dst_ptr_ptr = 0x321004u32;
        let src_data = 0x321100u32;
        let dst_data = 0x321200u32;

        bus.write_bytes(src_data, &[0x10, 0x20, 0x30]);
        bus.write_long(src_ptr_ptr, src_data);
        bus.write_long(dst_ptr_ptr, dst_data);
        bus.write_word(sp, 3); // srcBytes
        bus.write_long(sp + 2, dst_ptr_ptr); // VAR dstPtr
        bus.write_long(sp + 6, src_ptr_ptr); // VAR srcPtr

        let result = disp.dispatch_toolbox(true, 0x0CF, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
        assert_eq!(bus.read_long(src_ptr_ptr), src_data + 3);
        assert_eq!(bus.read_long(dst_ptr_ptr), dst_data + 4);
        assert_eq!(bus.read_byte(dst_data), 0x02); // literal len 3 => flag 2
        assert_eq!(bus.read_byte(dst_data + 1), 0x10);
        assert_eq!(bus.read_byte(dst_data + 2), 0x20);
        assert_eq!(bus.read_byte(dst_data + 3), 0x30);
    }

    #[test]
    fn unpackbits_expands_packbits_stream_and_advances_var_pointers() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let src_ptr_ptr = 0x322000u32;
        let dst_ptr_ptr = 0x322004u32;
        let src_data = 0x322100u32;
        let dst_data = 0x322200u32;

        // Encodes bytes: [AA, AA, AA, AA, 55]
        // repeat packet (0xFD, 0xAA) + literal packet (0x00, 0x55).
        bus.write_bytes(src_data, &[0xFD, 0xAA, 0x00, 0x55]);
        bus.write_long(src_ptr_ptr, src_data);
        bus.write_long(dst_ptr_ptr, dst_data);
        bus.write_word(sp, 5); // dstBytes
        bus.write_long(sp + 2, dst_ptr_ptr); // VAR dstPtr
        bus.write_long(sp + 6, src_ptr_ptr); // VAR srcPtr

        let result = disp.dispatch_toolbox(true, 0x0D0, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
        assert_eq!(bus.read_long(src_ptr_ptr), src_data + 4);
        assert_eq!(bus.read_long(dst_ptr_ptr), dst_data + 5);
        assert_eq!(
            bus.read_bytes(dst_data, 5),
            vec![0xAA, 0xAA, 0xAA, 0xAA, 0x55]
        );
    }

    // System 7+ Dispatch Manager stubs
    // Inside Macintosh: More Macintosh Toolbox (1993),
    // pp. 6-6/6-29/6-98, 5-18/5-71, and 7-12/7-66.

    #[test]
    fn componentdispatch_call_path_pops_selector_instance_and_args_and_returns_noerr() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0); // component call path

        // Inline component-call glue pushes [paramSize:callNum] at SP.
        bus.write_long(sp, 0x0004_0000);
        bus.write_long(sp + 4, 0x00C0_FFEE); // ComponentInstance
        bus.write_long(sp + 8, 0xA5A5_5A5A); // sentinel argument data

        let result = disp.dispatch_toolbox(true, 0x02A, &mut cpu, &mut bus);
        assert!(result.is_some(), "ComponentDispatch should be handled");
        assert!(result.unwrap().is_ok(), "ComponentDispatch should return");
        assert_eq!(cpu.read_reg(Register::D0), 0, "stub should return noErr");
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp + 12,
            "component call path should consume selector + instance + args"
        );
        assert_eq!(bus.read_long(sp), 0x0004_0000);
        assert_eq!(bus.read_long(sp + 4), 0x00C0_FFEE);
        assert_eq!(bus.read_long(sp + 8), 0xA5A5_5A5A);
    }

    #[test]
    fn componentdispatch_internal_request_preserves_non_d0_registers_and_stack() {
        let (mut disp, mut cpu, mut bus) = setup();
        cpu.write_reg(Register::D0, 0xFFFF_1234); // manager-internal call path
        cpu.write_reg(Register::D1, 0x1111_2222);
        cpu.write_reg(Register::A0, 0x3333_4444);
        cpu.write_reg(Register::A1, 0x5555_6666);
        let sp_before = cpu.read_reg(Register::A7);

        let result = disp.dispatch_toolbox(true, 0x02A, &mut cpu, &mut bus);
        assert!(result.is_some(), "ComponentDispatch should be handled");
        assert!(result.unwrap().is_ok(), "ComponentDispatch should return");
        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert_eq!(cpu.read_reg(Register::D1), 0x1111_2222);
        assert_eq!(cpu.read_reg(Register::A0), 0x3333_4444);
        assert_eq!(cpu.read_reg(Register::A1), 0x5555_6666);
        assert_eq!(cpu.read_reg(Register::A7), sp_before);
    }

    #[test]
    fn textservicesdispatch_returns_noerr_and_preserves_stack_pointer_in_noop_path() {
        let (mut disp, mut cpu, mut bus) = setup();
        cpu.write_reg(Register::D0, 0x1234_5678);
        cpu.write_reg(Register::D1, 0x1111_2222);
        cpu.write_reg(Register::A0, 0x3333_4444);
        cpu.write_reg(Register::A1, 0x5555_6666);
        let sp_before = cpu.read_reg(Register::A7);

        let result = disp.dispatch_toolbox(true, 0x254, &mut cpu, &mut bus);
        assert!(result.is_some(), "TextServicesDispatch should be handled");
        assert!(
            result.unwrap().is_ok(),
            "TextServicesDispatch should succeed"
        );
        assert_eq!(
            cpu.read_reg(Register::D0),
            0,
            "TextServicesDispatch should return noErr"
        );
        assert_eq!(cpu.read_reg(Register::D1), 0x1111_2222);
        assert_eq!(cpu.read_reg(Register::A0), 0x3333_4444);
        assert_eq!(cpu.read_reg(Register::A1), 0x5555_6666);
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_before,
            "TextServicesDispatch should preserve the caller stack pointer"
        );
    }

    #[test]
    fn puticon_preserves_a7() {
        // PutIcon is an undocumented internal trap. The conservative HLE stub is
        // a no-op, so the proof checks that it preserves A7 and returns cleanly.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_before = TEST_SP;
        cpu.write_reg(Register::A7, sp_before);
        bus.write_long(sp_before, 0x1122_3344);
        bus.write_word(sp_before + 4, 0x5566);

        let result = disp.dispatch_toolbox(true, 0x1CA, &mut cpu, &mut bus);
        assert!(result.is_some(), "PutIcon should be handled");
        assert!(result.unwrap().is_ok(), "PutIcon should return");
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp_before,
            "PutIcon should preserve A7"
        );
        assert_eq!(
            bus.read_long(sp_before),
            0x1122_3344,
            "PutIcon should not modify the caller's stack word"
        );
        assert_eq!(
            bus.read_word(sp_before + 4),
            0x5566,
            "PutIcon should not modify the trailing stack halfword"
        );
    }

    #[test]
    fn pack13_initdbpack_selector_returns_noerr_and_preserves_stack() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x0000_0100); // InitDBPack selector
        bus.write_long(sp, 0x1122_3344); // sentinel stack word

        let result = disp.dispatch_toolbox(true, 0x02F, &mut cpu, &mut bus);
        assert!(result.is_some(), "Pack13 should be handled");
        assert!(result.unwrap().is_ok(), "Pack13 should return");
        assert_eq!(
            cpu.read_reg(Register::D0),
            0,
            "selector 0x0100 should return noErr"
        );
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp,
            "Pack13 selector-only call should preserve A7"
        );
        assert_eq!(
            bus.read_long(sp),
            0x1122_3344,
            "Pack13 selector-only call should not touch the stack frame"
        );
    }

    #[test]
    fn pack9_aliases_stackspace_and_preserves_stack_slot() {
        let (mut toolbox_disp, mut toolbox_cpu, mut toolbox_bus) = setup();
        let (mut memory_disp, mut memory_cpu, mut memory_bus) = setup();
        let sp = TEST_SP;

        toolbox_cpu.write_reg(Register::A7, sp);
        memory_cpu.write_reg(Register::A7, sp);
        toolbox_bus.write_long(sp, 0x1122_3344);
        memory_bus.write_long(sp, 0x1122_3344);

        let toolbox_result =
            toolbox_disp.dispatch_toolbox(true, 0x02B, &mut toolbox_cpu, &mut toolbox_bus);
        let memory_result =
            memory_disp.dispatch_memory(false, 0x65, &mut memory_cpu, &mut memory_bus);

        assert!(toolbox_result.is_some(), "Pack9 should be handled");
        assert!(toolbox_result.unwrap().is_ok(), "Pack9 should return");
        assert!(memory_result.is_some(), "StackSpace should be handled");
        assert!(memory_result.unwrap().is_ok(), "StackSpace should return");
        assert_eq!(
            toolbox_cpu.read_reg(Register::D0),
            memory_cpu.read_reg(Register::D0),
            "Pack9 should return the same D0 value as StackSpace"
        );
        assert_eq!(
            toolbox_cpu.read_reg(Register::A7),
            sp,
            "Pack9 should preserve the caller's stack pointer"
        );
        assert_eq!(
            toolbox_bus.read_long(sp),
            0x1122_3344,
            "Pack9 should not write a Pascal result slot on the caller stack"
        );
        assert_eq!(
            memory_cpu.read_reg(Register::A7),
            sp,
            "StackSpace should preserve the caller's stack pointer"
        );
        assert_eq!(
            memory_bus.read_long(sp),
            0x1122_3344,
            "StackSpace should not write a Pascal result slot on the caller stack"
        );
    }

    #[test]
    fn pack10_aliases_newemptyhandle_and_preserves_stack_slot() {
        let (mut toolbox_disp, mut toolbox_cpu, mut toolbox_bus) = setup();
        let (mut memory_disp, mut memory_cpu, mut memory_bus) = setup();
        let sp = TEST_SP;

        toolbox_cpu.write_reg(Register::A7, sp);
        memory_cpu.write_reg(Register::A7, sp);
        toolbox_bus.write_long(sp, 0x1122_3344);
        memory_bus.write_long(sp, 0x1122_3344);

        let toolbox_result =
            toolbox_disp.dispatch_toolbox(true, 0x02C, &mut toolbox_cpu, &mut toolbox_bus);
        let memory_result =
            memory_disp.dispatch_memory(false, 0x66, &mut memory_cpu, &mut memory_bus);

        assert!(toolbox_result.is_some(), "Pack10 should be handled");
        assert!(toolbox_result.unwrap().is_ok(), "Pack10 should return");
        assert!(memory_result.is_some(), "NewEmptyHandle should be handled");
        assert!(
            memory_result.unwrap().is_ok(),
            "NewEmptyHandle should return"
        );
        assert_eq!(
            toolbox_cpu.read_reg(Register::D0),
            memory_cpu.read_reg(Register::D0),
            "Pack10 should return the same D0 value as NewEmptyHandle"
        );
        assert_eq!(
            toolbox_cpu.read_reg(Register::A7),
            sp,
            "Pack10 should preserve the caller's stack pointer"
        );
        assert_eq!(
            toolbox_bus.read_long(sp),
            0x1122_3344,
            "Pack10 should not write a Pascal result slot on the caller stack"
        );
        assert_ne!(
            toolbox_cpu.read_reg(Register::A0),
            0,
            "Pack10 should return a non-NIL handle in A0"
        );
        assert_eq!(
            toolbox_bus.read_long(toolbox_cpu.read_reg(Register::A0)),
            0,
            "Pack10 should initialize the returned master pointer to NIL"
        );
        assert_ne!(
            memory_cpu.read_reg(Register::A0),
            0,
            "NewEmptyHandle should return a non-NIL handle in A0"
        );
        assert_eq!(
            memory_bus.read_long(memory_cpu.read_reg(Register::A0)),
            0,
            "NewEmptyHandle should initialize the returned master pointer to NIL"
        );
        assert_eq!(
            memory_cpu.read_reg(Register::A7),
            sp,
            "NewEmptyHandle should preserve the caller's stack pointer"
        );
        assert_eq!(
            memory_bus.read_long(sp),
            0x1122_3344,
            "NewEmptyHandle should not write a Pascal result slot on the caller stack"
        );
    }

    #[test]
    fn icondispatch_selector_zero_returns_noerr_and_pops_eight_bytes() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x0000_0000); // selector 0 (NewIconSuite / no-op stub path)
        bus.write_long(sp, 0x1122_3344);
        bus.write_long(sp + 4, 0x5566_7788);

        let result = disp.dispatch_toolbox(true, 0x3C9, &mut cpu, &mut bus);
        assert!(result.is_some(), "IconDispatch should be handled");
        assert!(result.unwrap().is_ok(), "IconDispatch should return");
        assert_eq!(
            cpu.read_reg(Register::D0),
            0,
            "selector 0 should return noErr"
        );
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp + 8,
            "selector 0 should pop eight bytes"
        );
        assert_eq!(bus.read_long(sp), 0x1122_3344);
        assert_eq!(bus.read_long(sp + 4), 0x5566_7788);
    }

    #[test]
    fn icondispatch_unsupported_selector_returns_param_err_and_pops_eight_bytes() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x0000_050B); // representative unsupported selector
        bus.write_long(sp, 0x1122_3344);
        bus.write_long(sp + 4, 0x5566_7788);

        let result = disp.dispatch_toolbox(true, 0x3C9, &mut cpu, &mut bus);
        assert!(result.is_some(), "IconDispatch should be handled");
        assert!(result.unwrap().is_ok(), "IconDispatch should return");
        assert_eq!(
            cpu.read_reg(Register::D0) as i16,
            -50,
            "stub should return paramErr"
        );
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp + 8,
            "unsupported selector should still pop eight bytes"
        );
        assert_eq!(bus.read_long(sp), 0x1122_3344);
        assert_eq!(bus.read_long(sp + 4), 0x5566_7788);
    }

    #[test]
    fn icondispatch_selector_zero_preserves_non_d0_registers_and_pops_eight_bytes() {
        let (mut disp, mut cpu, mut bus) = setup();
        cpu.write_reg(Register::D1, 0x2222_3333);
        cpu.write_reg(Register::A0, 0x4444_5555);
        cpu.write_reg(Register::A1, 0x6666_7777);
        let sp_before = cpu.read_reg(Register::A7);

        cpu.write_reg(Register::D0, 0x0000_0000);
        let result = disp.dispatch_toolbox(true, 0x3C9, &mut cpu, &mut bus);
        assert!(result.is_some(), "IconDispatch should be handled");
        assert!(result.unwrap().is_ok(), "IconDispatch should return");
        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert_eq!(cpu.read_reg(Register::D1), 0x2222_3333);
        assert_eq!(cpu.read_reg(Register::A0), 0x4444_5555);
        assert_eq!(cpu.read_reg(Register::A1), 0x6666_7777);
        assert_eq!(cpu.read_reg(Register::A7), sp_before + 8);
    }

    #[test]
    fn translationdispatch_selector_zero_returns_param_err_and_pops_four_bytes() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x0000_001C); // GetFileTypesThatAppCanNativelyOpen
        bus.write_long(sp, 0x99AA_BBCC);
        bus.write_long(sp + 4, 0xDDEE_F011);

        let result = disp.dispatch_toolbox(true, 0x3FC, &mut cpu, &mut bus);
        assert!(result.is_some(), "TranslationDispatch should be handled");
        assert!(result.unwrap().is_ok(), "TranslationDispatch should return");
        assert_eq!(
            cpu.read_reg(Register::D0) as i16,
            -50,
            "stub should return paramErr"
        );
        assert_eq!(
            cpu.read_reg(Register::A7),
            sp + 4,
            "stub should pop four bytes"
        );
        assert_eq!(bus.read_long(sp), 0x99AA_BBCC);
        assert_eq!(bus.read_long(sp + 4), 0xDDEE_F011);
    }

    #[test]
    fn translationdispatch_selector_zero_preserves_non_d0_registers_and_pops_four_bytes() {
        let (mut disp, mut cpu, mut bus) = setup();
        cpu.write_reg(Register::D0, 0x0000_0009);
        cpu.write_reg(Register::D1, 0x7777_8888);
        cpu.write_reg(Register::A0, 0x9999_AAAA);
        cpu.write_reg(Register::A1, 0xBBBB_CCCC);
        let sp_before = cpu.read_reg(Register::A7);

        let result = disp.dispatch_toolbox(true, 0x3FC, &mut cpu, &mut bus);
        assert!(result.is_some(), "TranslationDispatch should be handled");
        assert!(result.unwrap().is_ok(), "TranslationDispatch should return");
        assert_eq!(cpu.read_reg(Register::D0) as i16, -50);
        assert_eq!(cpu.read_reg(Register::D1), 0x7777_8888);
        assert_eq!(cpu.read_reg(Register::A0), 0x9999_AAAA);
        assert_eq!(cpu.read_reg(Register::A1), 0xBBBB_CCCC);
        assert_eq!(cpu.read_reg(Register::A7), sp_before + 4);
    }

    #[test]
    fn threaddispatch_begin_and_end_critical_roundtrip() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0xBEEF);

        cpu.write_reg(Register::D0, 0x0000_000B);
        let begin = disp.dispatch_toolbox(true, 0x3F2, &mut cpu, &mut bus);
        assert!(
            begin.is_some(),
            "ThreadDispatch should handle ThreadBeginCritical"
        );
        assert!(begin.unwrap().is_ok(), "ThreadBeginCritical should return");
        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp);
        assert_eq!(bus.read_word(sp), 0);
        assert_eq!(disp.thread_critical_nesting, 1);

        cpu.write_reg(Register::D0, 0x0000_000C);
        let end = disp.dispatch_toolbox(true, 0x3F2, &mut cpu, &mut bus);
        assert!(
            end.is_some(),
            "ThreadDispatch should handle ThreadEndCritical"
        );
        assert!(end.unwrap().is_ok(), "ThreadEndCritical should return");
        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp);
        assert_eq!(bus.read_word(sp), 0);
        assert_eq!(disp.thread_critical_nesting, 0);
    }

    #[test]
    fn threaddispatch_endcritical_underflow_returns_thread_protocol_err() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0xBEEF);
        cpu.write_reg(Register::D0, 0x0000_000C);

        let result = disp.dispatch_toolbox(true, 0x3F2, &mut cpu, &mut bus);
        assert!(
            result.is_some(),
            "ThreadDispatch should handle ThreadEndCritical"
        );
        assert!(result.unwrap().is_ok(), "ThreadEndCritical should return");
        assert_eq!(cpu.read_reg(Register::D0) as i16, -619);
        assert_eq!(cpu.read_reg(Register::A7), sp);
        assert_eq!(bus.read_word(sp), (-619i16) as u16);
        assert_eq!(disp.thread_critical_nesting, 0);
    }

    #[test]
    fn threaddispatch_unsupported_selector_returns_param_err() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = cpu.read_reg(Register::A7);
        bus.write_word(sp, 0xBEEF);
        cpu.write_reg(Register::D0, 0x0000_0000);
        cpu.write_reg(Register::D1, 0x1111_2222);
        cpu.write_reg(Register::A0, 0x3333_4444);
        cpu.write_reg(Register::A1, 0x5555_6666);

        let result = disp.dispatch_toolbox(true, 0x3F2, &mut cpu, &mut bus);
        assert!(result.is_some(), "ThreadDispatch should be handled");
        assert!(result.unwrap().is_ok(), "ThreadDispatch should return");
        assert_eq!(cpu.read_reg(Register::D0) as i16, -50);
        assert_eq!(cpu.read_reg(Register::D1), 0x1111_2222);
        assert_eq!(cpu.read_reg(Register::A0), 0x3333_4444);
        assert_eq!(cpu.read_reg(Register::A1), 0x5555_6666);
        assert_eq!(cpu.read_reg(Register::A7), sp);
        assert_eq!(bus.read_word(sp), (-50i16) as u16);
        assert_eq!(disp.thread_critical_nesting, 0);
    }

    // DictionaryDispatch ($AA53)
    // Inside Macintosh: Text (1993), pp. 8-11, 8-21 to 8-24, 8-34.
    // InitializeDictionary is the public client call that creates the
    // internal B*-tree for a freshly created dictionary file. The 68K
    // Pascal wrapper pushes a 10-byte argument frame:
    //   FSSpecPtr (4) + maximumKeyLength (2) + keyAttributes (2) + script (2)
    #[test]
    fn dictionarydispatch_initialize_dictionary_selector_pops_ten_byte_frame_and_returns_noerr() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let dict_spec = bus.alloc(64);

        cpu.write_reg(Register::D0, 0x0500);
        bus.write_long(sp, dict_spec);
        bus.write_word(sp + 4, 129);
        bus.write_word(sp + 6, 0x0010);
        bus.write_word(sp + 8, 0x0000);
        bus.write_word(sp + 10, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x253, &mut cpu, &mut bus);
        assert!(result.is_some(), "DictionaryDispatch should be handled");
        assert!(
            result.unwrap().is_ok(),
            "InitializeDictionary should return"
        );
        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
        assert_eq!(bus.read_word(sp + 10), 0);
    }

    // Pack2 / DiskInit ($A9E9)
    // Inside Macintosh: Files (1992), pp. 5-15 to 5-21 and p. 5-24.
    #[test]
    fn pack2_dibadmount_selector_0000_returns_zero_and_pops_selector_plus_point_and_evtmessage() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_word(sp, 0x0000); // DIBadMount
        bus.write_word(sp + 2, 12); // where.v
        bus.write_word(sp + 4, 34); // where.h
        bus.write_long(sp + 6, 0x1122_3344); // evtMessage
        bus.write_word(sp + 10, 0xBEEF); // Integer result slot

        let result = disp.dispatch_toolbox(true, 0x1E9, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 10), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    #[test]
    fn pack2_diload_and_diunload_selectors_pop_selector_only_and_return_noerr() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_word(sp, 0x0002); // DILoad
        bus.write_word(sp + 2, 0xA55A); // sentinel after selector
        let sp_pre_load = cpu.read_reg(Register::A7);
        let load = disp.dispatch_toolbox(true, 0x1E9, &mut cpu, &mut bus);
        let sp_post_load = cpu.read_reg(Register::A7);
        assert!(load.is_some());
        assert!(load.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
        assert_eq!(sp_post_load, sp_pre_load + 2);
        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert_eq!(bus.read_word(sp + 2), 0xA55A);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0004); // DIUnload
        bus.write_word(sp + 2, 0x5AA5); // sentinel after selector
        let sp_pre_unload = cpu.read_reg(Register::A7);
        let unload = disp.dispatch_toolbox(true, 0x1E9, &mut cpu, &mut bus);
        let sp_post_unload = cpu.read_reg(Register::A7);
        assert!(unload.is_some());
        assert!(unload.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
        assert_eq!(sp_post_unload, sp_pre_unload + 2);
        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert_eq!(bus.read_word(sp + 2), 0x5AA5);
    }

    #[test]
    fn pack2_diformat_and_diverify_selectors_return_noerr_and_pop_drvnum_argument() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_word(sp, 0x0006); // DIFormat
        bus.write_word(sp + 2, 7); // drvNum
        bus.write_word(sp + 4, 0xBEEF); // OSErr result slot
        let format = disp.dispatch_toolbox(true, 0x1E9, &mut cpu, &mut bus);
        assert!(format.is_some());
        assert!(format.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 4), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
        assert_eq!(cpu.read_reg(Register::D0), 0);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0008); // DIVerify
        bus.write_word(sp + 2, 7); // drvNum
        bus.write_word(sp + 4, 0xBEEF); // OSErr result slot
        let verify = disp.dispatch_toolbox(true, 0x1E9, &mut cpu, &mut bus);
        assert!(verify.is_some());
        assert!(verify.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 4), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    #[test]
    fn pack2_dizero_selector_000a_consumes_pascal_str255_by_value_and_returns_noerr() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_word(sp, 0x000A); // DIZero
        bus.write_word(sp + 2, 3); // drvNum
        bus.write_byte(sp + 4, 5); // Str255 length
        bus.write_bytes(sp + 5, b"Disk0");
        bus.write_word(sp + 260, 0xBEEF); // OSErr result slot

        let result = disp.dispatch_toolbox(true, 0x1E9, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 260), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 260);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    #[test]
    fn pack2_selector_table_integrated_contract_covers_all_documented_selector_frames() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        // DIBadMount: selector + Point + evtMessage + result slot.
        bus.write_word(sp, 0x0000);
        bus.write_word(sp + 2, 12);
        bus.write_word(sp + 4, 34);
        bus.write_long(sp + 6, 0x1122_3344);
        bus.write_word(sp + 10, 0xBEEF);
        let bad_mount = disp.dispatch_toolbox(true, 0x1E9, &mut cpu, &mut bus);
        assert!(bad_mount.is_some());
        assert!(bad_mount.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 10), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);

        // DILoad / DIUnload: selector only.
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0002);
        bus.write_word(sp + 2, 0xA55A);
        let load = disp.dispatch_toolbox(true, 0x1E9, &mut cpu, &mut bus);
        assert!(load.is_some());
        assert!(load.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
        assert_eq!(bus.read_word(sp + 2), 0xA55A);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0004);
        bus.write_word(sp + 2, 0x5AA5);
        let unload = disp.dispatch_toolbox(true, 0x1E9, &mut cpu, &mut bus);
        assert!(unload.is_some());
        assert!(unload.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
        assert_eq!(bus.read_word(sp + 2), 0x5AA5);

        // DIFormat / DIVerify: selector + drvNum + result slot.
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0006);
        bus.write_word(sp + 2, 7);
        bus.write_word(sp + 4, 0xBEEF);
        let format = disp.dispatch_toolbox(true, 0x1E9, &mut cpu, &mut bus);
        assert!(format.is_some());
        assert!(format.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 4), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x0008);
        bus.write_word(sp + 2, 7);
        bus.write_word(sp + 4, 0xBEEF);
        let verify = disp.dispatch_toolbox(true, 0x1E9, &mut cpu, &mut bus);
        assert!(verify.is_some());
        assert!(verify.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 4), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);

        // DIZero: selector + drvNum + by-value Str255 + result slot.
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0x000A);
        bus.write_word(sp + 2, 3);
        bus.write_byte(sp + 4, 5);
        bus.write_bytes(sp + 5, b"Disk0");
        bus.write_word(sp + 260, 0xBEEF);
        let zero = disp.dispatch_toolbox(true, 0x1E9, &mut cpu, &mut bus);
        assert!(zero.is_some());
        assert!(zero.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 260), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 260);
    }

    // Pack3 / Standard File ($A9EA) — StandardGetFile selector $0006
    // IM:Files 1992 pp. 3-50 and 3-61: cancel sets sfGood to FALSE.
    #[test]
    fn standard_get_file_cancel_sets_sfgood_false_and_pops_16_bytes() {
        let _env = clear_standard_file_env();
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let reply_ptr = 0x320000u32;
        bus.write_byte(reply_ptr, 0xFF);
        bus.write_byte(reply_ptr + 1, 0xAB);
        bus.write_word(sp, 0x0006); // StandardGetFile selector
        bus.write_long(sp + 2, reply_ptr); // VAR reply

        let result = disp.dispatch_toolbox(true, 0x1EA, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_byte(reply_ptr), 0);
        assert_eq!(bus.read_byte(reply_ptr + 1), 0xAB);
        assert_eq!(cpu.read_reg(Register::A7), sp + 16);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    // Pack3 / Standard File ($A9EA) — StandardGetFile selector $0006
    // IM:Files 1992 pp. 3-42 and 3-50: Open returns sfType and an FSSpec.
    #[test]
    fn standard_get_file_env_selection_returns_fsspec_and_pops_16_bytes() {
        let _env = set_standard_file_env("Tom Fighter Paris");
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let reply_ptr = 0x320080u32;
        let pilots_dir = disp.ensure_vfs_directory("Pilots");
        disp.default_dir_id = pilots_dir;
        disp.vfs_rsrc
            .insert("Pilots/Tom Fighter Paris".to_string(), vec![1, 2, 3]);
        disp.set_vfs_entry_metadata("Pilots/Tom Fighter Paris", *b"PIL ", *b"EVO!", 0x4000);
        bus.write_word(sp, 0x0006); // StandardGetFile selector
        bus.write_long(sp + 2, reply_ptr); // VAR reply

        let result = disp.dispatch_toolbox(true, 0x1EA, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_byte(reply_ptr), 0xFF);
        assert_eq!(bus.read_byte(reply_ptr + 1), 0);
        assert_eq!(bus.read_long(reply_ptr + 2), u32::from_be_bytes(*b"PIL "));
        assert_eq!(
            bus.read_word(reply_ptr + 6),
            crate::trap::dispatch::TrapDispatcher::boot_volume_ref_num_u16()
        );
        assert_eq!(bus.read_long(reply_ptr + 8), pilots_dir);
        assert_eq!(
            bus.read_pstring(reply_ptr + 12),
            b"Tom Fighter Paris".to_vec()
        );
        assert_eq!(bus.read_word(reply_ptr + 78), 0x4000);
        assert_eq!(cpu.read_reg(Register::A7), sp + 16);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    // Pack3 / Standard File ($A9EA) — StandardGetFile selector $0006.
    // IM:Files 1992 p. 3-50 and IM:VI 26-22: a positive numTypes/typeList
    // filters the Open dialog's display list by Finder file type.
    #[test]
    fn standard_get_file_auto_selects_matching_current_directory_type() {
        let _env = clear_standard_file_env();
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let reply_ptr = 0x320500u32;
        let type_list_ptr = 0x320600u32;
        let app_dir = disp.ensure_vfs_directory("Data Folder");
        disp.default_dir_id = app_dir;
        disp.vfs
            .insert("Data Folder/Selected".to_string(), vec![1, 2, 3]);
        disp.set_vfs_entry_metadata("Data Folder/Selected", *b"DATA", *b"TEST", 0x4000);
        disp.vfs
            .insert("Other Folder/Selected".to_string(), vec![4, 5, 6]);
        disp.set_vfs_entry_metadata("Other Folder/Selected", *b"DATA", *b"TEST", 0x8000);
        bus.write_long(type_list_ptr, u32::from_be_bytes(*b"DATA"));
        bus.write_word(sp, 0x0006); // StandardGetFile selector
        bus.write_long(sp + 2, reply_ptr); // VAR reply
        bus.write_long(sp + 6, type_list_ptr); // typeList
        bus.write_word(sp + 10, 1); // numTypes
        bus.write_long(sp + 12, 0); // fileFilter

        let result = disp.dispatch_toolbox(true, 0x1EA, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_byte(reply_ptr), 0xFF);
        assert_eq!(bus.read_long(reply_ptr + 2), u32::from_be_bytes(*b"DATA"));
        assert_eq!(
            bus.read_word(reply_ptr + 6),
            crate::trap::dispatch::TrapDispatcher::boot_volume_ref_num_u16()
        );
        assert_eq!(bus.read_long(reply_ptr + 8), app_dir);
        assert_eq!(bus.read_pstring(reply_ptr + 12), b"Selected".to_vec());
        assert_eq!(bus.read_word(reply_ptr + 78), 0x4000);
        assert_eq!(cpu.read_reg(Register::A7), sp + 16);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    // Pack3 / Standard File ($A9EA) — StandardGetFile selector $0006.
    // IM:Files 1992 p. 3-50: numTypes = -1 lets all file types pass.
    #[test]
    fn standard_get_file_minus_one_types_auto_selects_single_current_directory_file() {
        let _env = clear_standard_file_env();
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let reply_ptr = 0x320900u32;
        let app_dir = disp.ensure_vfs_directory("Data Folder");
        disp.default_dir_id = app_dir;
        disp.vfs
            .insert("Data Folder/Only Choice".to_string(), vec![1, 2, 3]);
        disp.set_vfs_entry_metadata("Data Folder/Only Choice", *b"Flux", *b"Geek", 0x2000);
        disp.vfs
            .insert("Other Folder/Only Choice".to_string(), vec![4, 5, 6]);
        disp.set_vfs_entry_metadata("Other Folder/Only Choice", *b"DATA", *b"TEST", 0);
        bus.write_word(sp, 0x0006); // StandardGetFile selector
        bus.write_long(sp + 2, reply_ptr); // VAR reply
        bus.write_long(sp + 6, 0); // typeList is ignored for numTypes = -1
        bus.write_word(sp + 10, (-1i16) as u16); // numTypes = all file types
        bus.write_long(sp + 12, 0); // fileFilter

        let result = disp.dispatch_toolbox(true, 0x1EA, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_byte(reply_ptr), 0xFF);
        assert_eq!(bus.read_long(reply_ptr + 2), u32::from_be_bytes(*b"Flux"));
        assert_eq!(
            bus.read_word(reply_ptr + 6),
            crate::trap::dispatch::TrapDispatcher::boot_volume_ref_num_u16()
        );
        assert_eq!(bus.read_long(reply_ptr + 8), app_dir);
        assert_eq!(bus.read_pstring(reply_ptr + 12), b"Only Choice".to_vec());
        assert_eq!(bus.read_word(reply_ptr + 78), 0x2000);
        assert_eq!(cpu.read_reg(Register::A7), sp + 16);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    // Pack3 / Standard File ($A9EA) — StandardGetFile selector $0006.
    // Without real UI, all-types mode stays canceled when more than one
    // current-directory file would be displayed.
    #[test]
    fn standard_get_file_minus_one_types_cancels_ambiguous_current_directory_files() {
        let _env = clear_standard_file_env();
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let reply_ptr = 0x320A00u32;
        let app_dir = disp.ensure_vfs_directory("Data Folder");
        disp.default_dir_id = app_dir;
        disp.vfs
            .insert("Data Folder/First".to_string(), vec![1, 2, 3]);
        disp.set_vfs_entry_metadata("Data Folder/First", *b"Flux", *b"Geek", 0);
        disp.vfs
            .insert("Data Folder/Second".to_string(), vec![4, 5, 6]);
        disp.set_vfs_entry_metadata("Data Folder/Second", *b"DATA", *b"TEST", 0);
        bus.write_byte(reply_ptr, 0xFF);
        bus.write_word(sp, 0x0006); // StandardGetFile selector
        bus.write_long(sp + 2, reply_ptr); // VAR reply
        bus.write_long(sp + 6, 0); // typeList
        bus.write_word(sp + 10, (-1i16) as u16); // numTypes = all file types
        bus.write_long(sp + 12, 0); // fileFilter

        let result = disp.dispatch_toolbox(true, 0x1EA, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_byte(reply_ptr), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 16);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    // Pack3 / Standard File ($A9EA) — StandardGetFile selector $0006.
    // IM:VI 26-22: numTypes = 0 filters out all files.
    #[test]
    fn standard_get_file_zero_types_still_cancels() {
        let _env = clear_standard_file_env();
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let reply_ptr = 0x320700u32;
        let type_list_ptr = 0x320800u32;
        let app_dir = disp.ensure_vfs_directory("Data Folder");
        disp.default_dir_id = app_dir;
        disp.vfs
            .insert("Data Folder/Selected".to_string(), vec![1, 2, 3]);
        disp.set_vfs_entry_metadata("Data Folder/Selected", *b"Flux", *b"Geek", 0);
        bus.write_byte(reply_ptr, 0xFF);
        bus.write_long(type_list_ptr, u32::from_be_bytes(*b"Flux"));
        bus.write_word(sp, 0x0006); // StandardGetFile selector
        bus.write_long(sp + 2, reply_ptr); // VAR reply
        bus.write_long(sp + 6, type_list_ptr); // typeList
        bus.write_word(sp + 10, 0); // numTypes
        bus.write_long(sp + 12, 0); // fileFilter

        let result = disp.dispatch_toolbox(true, 0x1EA, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_byte(reply_ptr), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 16);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    // Pack3 / Standard File ($A9EA) — SFGetFile selector $0002
    // IM:Files 1992 pp. 3-53 and 3-61; IM:I I-523..I-526.
    #[test]
    fn sf_get_file_cancel_sets_good_false_and_pops_28_bytes() {
        let _env = clear_standard_file_env();
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let reply_ptr = 0x320100u32;
        bus.write_byte(reply_ptr, 0xFF);
        bus.write_word(sp, 0x0002); // SFGetFile selector
        bus.write_long(sp + 2, reply_ptr); // VAR reply

        let result = disp.dispatch_toolbox(true, 0x1EA, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_byte(reply_ptr), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 28);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    // Pack3 / Standard File ($A9EA) — SFGetFile selector $0002.
    // IM:Files 1992 p. 1-12: old SFReply.vRefNum is a WDRefNum.
    #[test]
    fn sf_get_file_env_selection_returns_working_directory_refnum_and_pops_28_bytes() {
        let _env = set_standard_file_env("Pilots/Tom Fighter Paris");
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let reply_ptr = 0x320140u32;
        let pilots_dir = disp.ensure_vfs_directory("Pilots");
        disp.vfs
            .insert("Pilots/Tom Fighter Paris".to_string(), vec![1, 2, 3]);
        disp.set_vfs_entry_metadata("Pilots/Tom Fighter Paris", *b"PIL ", *b"EVO!", 0);
        bus.write_word(sp, 0x0002); // SFGetFile selector
        bus.write_long(sp + 2, reply_ptr); // VAR reply

        let result = disp.dispatch_toolbox(true, 0x1EA, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        let wd_ref = bus.read_word(reply_ptr + 6) as i16;
        let wd_info = disp
            .working_directory_info(wd_ref)
            .expect("SFGetFile should return a WDRefNum for the selected file directory");
        assert_eq!(bus.read_byte(reply_ptr), 0xFF);
        assert_eq!(bus.read_byte(reply_ptr + 1), 0);
        assert_eq!(bus.read_long(reply_ptr + 2), u32::from_be_bytes(*b"PIL "));
        assert_eq!(wd_info.dir_id, pilots_dir);
        assert_eq!(bus.read_word(reply_ptr + 8), 0);
        assert_eq!(
            bus.read_pstring(reply_ptr + 10),
            b"Tom Fighter Paris".to_vec()
        );
        assert_eq!(cpu.read_reg(Register::A7), sp + 28);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    // Pack3 / Standard File ($A9EA) — StandardPutFile selector $0005
    // Non-interactive runtimes accept the default filename in the current dir.
    #[test]
    fn standard_put_file_returns_default_name_fsspec_and_pops_14_bytes() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let reply_ptr = 0x320180u32;
        let default_name_ptr = 0x320300u32;
        disp.default_dir_id = 18;
        disp.app_wd_refnum = crate::trap::dispatch::TrapDispatcher::boot_volume_ref_num();
        bus.write_pstring(default_name_ptr, b"Twilight Save");
        bus.write_word(sp, 0x0005); // StandardPutFile selector
        bus.write_long(sp + 2, reply_ptr); // VAR reply
        bus.write_long(sp + 6, default_name_ptr); // defaultName

        let result = disp.dispatch_toolbox(true, 0x1EA, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_byte(reply_ptr), 0xFF);
        assert_eq!(bus.read_byte(reply_ptr + 1), 0);
        assert_eq!(
            bus.read_word(reply_ptr + 6),
            crate::trap::dispatch::TrapDispatcher::boot_volume_ref_num_u16()
        );
        assert_eq!(bus.read_long(reply_ptr + 8), 18);
        assert_eq!(bus.read_pstring(reply_ptr + 12), b"Twilight Save".to_vec());
        assert_eq!(cpu.read_reg(Register::A7), sp + 14);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    // Pack3 / Standard File ($A9EA) — SFPutFile selector $0001
    // Old SFReply uses the original name and working-directory refnum.
    #[test]
    fn sf_put_file_returns_original_name_and_pops_22_bytes() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let reply_ptr = 0x320380u32;
        let original_name_ptr = 0x320500u32;
        disp.app_wd_refnum = -37;
        bus.write_pstring(original_name_ptr, b"Old Save");
        bus.write_word(sp, 0x0001); // SFPutFile selector
        bus.write_long(sp + 2, reply_ptr); // VAR reply
        bus.write_long(sp + 10, original_name_ptr); // origName

        let result = disp.dispatch_toolbox(true, 0x1EA, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_byte(reply_ptr), 0xFF);
        assert_eq!(bus.read_byte(reply_ptr + 1), 0);
        assert_eq!(bus.read_word(reply_ptr + 6), (-37i16) as u16);
        assert_eq!(bus.read_pstring(reply_ptr + 10), b"Old Save".to_vec());
        assert_eq!(cpu.read_reg(Register::A7), sp + 22);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    // Pack3 / Standard File ($A9EA) — CustomGetFile selector $0008
    // IM:Files 1992 pp. 3-51..3-54: reply pointer is still the VAR output.
    #[test]
    fn custom_get_file_cancel_uses_reply_pointer_at_sp_plus_28_and_pops_42_bytes() {
        let _env = clear_standard_file_env();
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let decoy_reply_ptr = 0x320180u32;
        let actual_reply_ptr = 0x320200u32;
        bus.write_byte(decoy_reply_ptr, 0xEE);
        bus.write_byte(actual_reply_ptr, 0xFF);
        bus.write_word(sp, 0x0008); // CustomGetFile selector
        bus.write_long(sp + 2, decoy_reply_ptr); // should be ignored
        bus.write_long(sp + 28, actual_reply_ptr); // VAR reply for selector $0008

        let result = disp.dispatch_toolbox(true, 0x1EA, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_byte(decoy_reply_ptr), 0xEE);
        assert_eq!(bus.read_byte(actual_reply_ptr), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 42);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    // Pack6 / Intl Utilities ($A9ED) — IUMetric selector $0004
    // IM:I I-505: returns TRUE when metric, otherwise FALSE.
    #[test]
    fn iumetric_returns_false_in_result_slot_and_pops_selector_only() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_word(sp, 0x0004); // IUMetric selector
        bus.write_word(sp + 2, 0xFFFF); // Boolean result slot

        let result = disp.dispatch_toolbox(true, 0x1ED, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 2), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    // Pack6 / Intl Utilities ($A9ED) — IUMagIDString selector $000C
    // IM:I I-507: returns 0 for equal (ignoring secondary ordering), 1 otherwise.
    #[test]
    fn iumagidstring_case_insensitive_equal_returns_zero_and_pops_14_bytes() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let a_ptr = 0x330000u32;
        let b_ptr = 0x330100u32;
        bus.write_bytes(a_ptr, b"Rose");
        bus.write_bytes(b_ptr, b"rOsE");
        bus.write_word(sp, 0x000C); // IUMagIDString selector
        bus.write_word(sp + 2, 4); // bLen
        bus.write_word(sp + 4, 4); // aLen
        bus.write_long(sp + 6, b_ptr);
        bus.write_long(sp + 10, a_ptr);
        bus.write_word(sp + 14, 0xFFFF); // result slot

        let result = disp.dispatch_toolbox(true, 0x1ED, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 14), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 14);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    #[test]
    fn iumagidstring_case_insensitive_mismatch_returns_one_and_pops_14_bytes() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let a_ptr = 0x330200u32;
        let b_ptr = 0x330300u32;
        bus.write_bytes(a_ptr, b"Rose");
        bus.write_bytes(b_ptr, b"Moss");
        bus.write_word(sp, 0x000C); // IUMagIDString selector
        bus.write_word(sp + 2, 4); // bLen
        bus.write_word(sp + 4, 4); // aLen
        bus.write_long(sp + 6, b_ptr);
        bus.write_long(sp + 10, a_ptr);
        bus.write_word(sp + 14, 0xFFFF); // result slot

        let result = disp.dispatch_toolbox(true, 0x1ED, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 14), 1);
        assert_eq!(cpu.read_reg(Register::A7), sp + 14);
        assert_eq!(cpu.read_reg(Register::D0), 1);
    }

    // Pack12 / Color Picker ($A82E)
    // Inside Macintosh Volume VI (1991), pp. 19-10 to 19-13.
    #[test]
    fn pack12_fix2smallfract_selector_0001_returns_low_word_and_pops_selector_plus_fixed() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_word(sp, 0x0001); // Fix2SmallFract selector
        bus.write_long(sp + 2, 0x0000_5678); // Fixed input in 0..1 range
        bus.write_word(sp + 6, 0xBEEF); // SmallFract result slot

        let result = disp.dispatch_toolbox(true, 0x02E, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 6), 0x5678);
        assert_eq!(cpu.read_reg(Register::A7), sp + 6);
    }

    #[test]
    fn pack12_smallfract2fix_selector_0002_returns_low_word_fixed_and_pops_selector_plus_smallfract(
    ) {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_word(sp, 0x0002); // SmallFract2Fix selector
        bus.write_word(sp + 2, 0x89AB); // SmallFract input
        bus.write_long(sp + 4, 0xDEAD_BEEF); // Fixed result slot

        let result = disp.dispatch_toolbox(true, 0x02E, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_long(sp + 4), 0x0000_89AB);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
    }

    #[test]
    fn pack12_getcolor_selector_0009_returns_false_and_pops_selector_plus_arguments() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let out_ptr = 0x350000u32;
        let in_ptr = 0x350100u32;

        bus.write_word(in_ptr, 0x1234);
        bus.write_word(in_ptr + 2, 0x5678);
        bus.write_word(in_ptr + 4, 0x9ABC);
        bus.write_word(out_ptr, 0x1234);
        bus.write_word(out_ptr + 2, 0x5678);
        bus.write_word(out_ptr + 4, 0x9ABC);

        bus.write_word(sp, 0x0009); // GetColor selector
        bus.write_long(sp + 2, out_ptr); // outColor pointer
        bus.write_long(sp + 6, in_ptr); // inColor pointer
        bus.write_long(sp + 10, 0x350200); // prompt pointer
        bus.write_long(sp + 14, 0x000A_0014); // Point(where)
        bus.write_word(sp + 18, 0xFFFF); // Boolean result slot

        let result = disp.dispatch_toolbox(true, 0x02E, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(out_ptr), 0x1234);
        assert_eq!(bus.read_word(out_ptr + 2), 0x5678);
        assert_eq!(bus.read_word(out_ptr + 4), 0x9ABC);
        assert_eq!(bus.read_word(sp + 18), 0);
        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 18);
    }

    #[test]
    fn pack12_rgb2hsl_selector_0006_converts_pure_red_to_expected_hsl_words() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let src_ptr = 0x350000u32;
        let dst_ptr = 0x350100u32;

        bus.write_word(src_ptr, 0xFFFF);
        bus.write_word(src_ptr + 2, 0x0000);
        bus.write_word(src_ptr + 4, 0x0000);
        bus.write_word(dst_ptr, 0xDEAD);
        bus.write_word(dst_ptr + 2, 0xBEEF);
        bus.write_word(dst_ptr + 4, 0xCAFE);

        bus.write_word(sp, 0x0006); // RGB2HSL selector
        bus.write_long(sp + 2, dst_ptr);
        bus.write_long(sp + 6, src_ptr);

        let result = disp.dispatch_toolbox(true, 0x02E, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(dst_ptr), 0x0000);
        assert_eq!(bus.read_word(dst_ptr + 2), 0xFFFF);
        assert_eq!(bus.read_word(dst_ptr + 4), 0x8000);
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
    }

    #[test]
    fn pack12_hsv2rgb_selector_0007_converts_pure_red_hsv_to_rgb_words() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let src_ptr = 0x350000u32;
        let dst_ptr = 0x350100u32;

        bus.write_word(src_ptr, 0x0000);
        bus.write_word(src_ptr + 2, 0xFFFF);
        bus.write_word(src_ptr + 4, 0xFFFF);
        bus.write_word(dst_ptr, 0xDEAD);
        bus.write_word(dst_ptr + 2, 0xBEEF);
        bus.write_word(dst_ptr + 4, 0xCAFE);

        bus.write_word(sp, 0x0007); // HSV2RGB selector
        bus.write_long(sp + 2, dst_ptr);
        bus.write_long(sp + 6, src_ptr);

        let result = disp.dispatch_toolbox(true, 0x02E, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(dst_ptr), 0xFFFF);
        assert_eq!(bus.read_word(dst_ptr + 2), 0x0000);
        assert_eq!(bus.read_word(dst_ptr + 4), 0x0000);
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
    }

    #[test]
    fn pack12_rgb2hsv_selector_0008_converts_pure_red_to_expected_hsv_words() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let src_ptr = 0x350000u32;
        let dst_ptr = 0x350100u32;

        bus.write_word(src_ptr, 0xFFFF);
        bus.write_word(src_ptr + 2, 0x0000);
        bus.write_word(src_ptr + 4, 0x0000);
        bus.write_word(dst_ptr, 0xDEAD);
        bus.write_word(dst_ptr + 2, 0xBEEF);
        bus.write_word(dst_ptr + 4, 0xCAFE);

        bus.write_word(sp, 0x0008); // RGB2HSV selector
        bus.write_long(sp + 2, dst_ptr);
        bus.write_long(sp + 6, src_ptr);

        let result = disp.dispatch_toolbox(true, 0x02E, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(dst_ptr), 0x0000);
        assert_eq!(bus.read_word(dst_ptr + 2), 0xFFFF);
        assert_eq!(bus.read_word(dst_ptr + 4), 0xFFFF);
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
    }

    #[test]
    fn pack12_cmy2rgb_selector_0003_converts_each_smallfract_channel_to_complementary_rgb_word() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let src_ptr = 0x350000u32;
        let dst_ptr = 0x350100u32;

        bus.write_word(src_ptr, 0x1111);
        bus.write_word(src_ptr + 2, 0x2222);
        bus.write_word(src_ptr + 4, 0x3333);
        bus.write_word(dst_ptr, 0xDEAD);
        bus.write_word(dst_ptr + 2, 0xBEEF);
        bus.write_word(dst_ptr + 4, 0xCAFE);

        bus.write_word(sp, 0x0003); // CMY2RGB selector
        bus.write_long(sp + 2, dst_ptr);
        bus.write_long(sp + 6, src_ptr);

        let result = disp.dispatch_toolbox(true, 0x02E, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(dst_ptr), 0xEEEE);
        assert_eq!(bus.read_word(dst_ptr + 2), 0xDDDD);
        assert_eq!(bus.read_word(dst_ptr + 4), 0xCCCC);
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
    }

    // Pack14 / Help Manager ($A830)
    // Public MPW declarations: HelpMgr.h selector-trap shims
    //   HMGetHelpMenuHandle(VAR mh: MenuHandle): OSErr;
    //   HMGetFont(VAR font: Integer): OSErr;
    // Inside Macintosh: More Macintosh Toolbox 1993, pp. 3-109 to 3-111;
    // selector table p. 3-173.
    #[test]
    fn pack14_hmgethelpmenuhandle_writes_nil_and_returns_hmhelpmanagernotinited() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let mh_ptr = bus.alloc(4);

        bus.write_long(mh_ptr, 0xDEAD_BEEF);
        bus.write_word(sp, 0x0200); // HMGetHelpMenuHandle selector
        bus.write_long(sp + 2, mh_ptr);
        bus.write_word(sp + 6, 0xBEEF); // result slot poison

        let result = disp.dispatch_toolbox(true, 0x030, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_long(mh_ptr), 0);
        assert_eq!(bus.read_word(sp + 6), (-855i16) as u16);
        assert_eq!(cpu.read_reg(Register::D0), (-855i16) as i32 as u32);
        assert_eq!(cpu.read_reg(Register::A7), sp + 6);
    }

    #[test]
    fn pack14_hmgetfont_writes_zero_and_returns_noerr() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let font_ptr = bus.alloc(2);

        bus.write_word(font_ptr, 0x1357);
        bus.write_word(sp, 0x020A); // HMGetFont selector
        bus.write_long(sp + 2, font_ptr);
        bus.write_word(sp + 6, 0xBEEF); // result slot poison

        let result = disp.dispatch_toolbox(true, 0x030, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(font_ptr), 0);
        assert_eq!(bus.read_word(sp + 6), 0);
        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 6);
    }

    #[test]
    fn pack12_rgb2cmy_selector_0004_converts_each_rgb_word_to_complementary_smallfract_channel() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let src_ptr = 0x350200u32;
        let dst_ptr = 0x350300u32;

        bus.write_word(src_ptr, 0x1357);
        bus.write_word(src_ptr + 2, 0x2468);
        bus.write_word(src_ptr + 4, 0x369C);
        bus.write_word(dst_ptr, 0xDEAD);
        bus.write_word(dst_ptr + 2, 0xBEEF);
        bus.write_word(dst_ptr + 4, 0xCAFE);

        bus.write_word(sp, 0x0004); // RGB2CMY selector
        bus.write_long(sp + 2, dst_ptr);
        bus.write_long(sp + 6, src_ptr);

        let result = disp.dispatch_toolbox(true, 0x02E, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(dst_ptr), 0xECA8);
        assert_eq!(bus.read_word(dst_ptr + 2), 0xDB97);
        assert_eq!(bus.read_word(dst_ptr + 4), 0xC963);
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
    }

    #[test]
    fn pack12_hsl2rgb_selector_0005_converts_zero_saturation_to_grayscale_rgb_words() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let src_ptr = 0x350400u32;
        let dst_ptr = 0x350500u32;

        bus.write_word(src_ptr, 0x1357);
        bus.write_word(src_ptr + 2, 0x0000);
        bus.write_word(src_ptr + 4, 0x2468);
        bus.write_word(dst_ptr, 0xDEAD);
        bus.write_word(dst_ptr + 2, 0xBEEF);
        bus.write_word(dst_ptr + 4, 0xCAFE);

        bus.write_word(sp, 0x0005); // HSL2RGB selector
        bus.write_long(sp + 2, dst_ptr);
        bus.write_long(sp + 6, src_ptr);

        let result = disp.dispatch_toolbox(true, 0x02E, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(dst_ptr), 0x2468);
        assert_eq!(bus.read_word(dst_ptr + 2), 0x2468);
        assert_eq!(bus.read_word(dst_ptr + 4), 0x2468);
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
    }

    // AliasDispatch ($A823) / selector $0000 FindFolder
    // IM:VI 1991 pp. 9-43..9-44: FindFolder writes foundVRefNum and foundDirID.
    #[test]
    fn aliasdispatch_findfolder_preferences_type_returns_found_refs() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let found_dir_id_ptr = 0x340000u32;
        let found_vref_ptr = 0x340100u32;

        cpu.write_reg(Register::D0, 0x0000); // FindFolder selector
        bus.write_long(sp, found_dir_id_ptr);
        bus.write_long(sp + 4, found_vref_ptr);
        bus.write_word(sp + 8, 1); // createFolder = TRUE
        bus.write_long(sp + 10, u32::from_be_bytes(*b"pref"));
        bus.write_word(sp + 14, 0x8000); // kOnSystemDisk
        bus.write_word(sp + 16, 0xBEEF); // result slot

        let result = disp.dispatch_toolbox(true, 0x023, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 16), 0); // noErr
        assert_eq!(cpu.read_reg(Register::A7), sp + 16);
        assert_eq!(bus.read_word(found_vref_ptr), (-1i16) as u16);

        let found_dir_id = bus.read_long(found_dir_id_ptr);
        assert_ne!(found_dir_id, 0);
        assert_eq!(
            disp.directory_path_for_id(found_dir_id),
            Some("System Folder/Preferences")
        );
    }

    // AliasDispatch ($A823) / selector $0000 FindFolder
    // IM:VI 1991 pp. 9-42..9-44: kTemporaryFolderType ('temp') locates the
    // root-level Temporary Items folder and returns its vRefNum/dirID.
    #[test]
    fn aliasdispatch_findfolder_temporary_type_returns_temporary_items_dir() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let found_dir_id_ptr = 0x340200u32;
        let found_vref_ptr = 0x340300u32;

        cpu.write_reg(Register::D0, 0x0000); // FindFolder selector
        bus.write_long(sp, found_dir_id_ptr);
        bus.write_long(sp + 4, found_vref_ptr);
        bus.write_word(sp + 8, 1); // createFolder = TRUE
        bus.write_long(sp + 10, u32::from_be_bytes(*b"temp"));
        bus.write_word(sp + 14, 0x8000); // kOnSystemDisk
        bus.write_word(sp + 16, 0xBEEF); // result slot

        let result = disp.dispatch_toolbox(true, 0x023, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 16), 0); // noErr
        assert_eq!(cpu.read_reg(Register::A7), sp + 16);
        assert_eq!(bus.read_word(found_vref_ptr), (-1i16) as u16);

        let found_dir_id = bus.read_long(found_dir_id_ptr);
        assert_ne!(found_dir_id, 0);
        assert_eq!(
            disp.directory_path_for_id(found_dir_id),
            Some("Temporary Items")
        );
    }

    // AliasDispatch ($A823) / selector $0002 NewAlias
    // IM:VI 1991 pp. 27-12..27-13: NewAlias allocates storage and writes AliasHandle.
    #[test]
    fn aliasdispatch_newalias_returns_allocated_alias_handle() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let alias_out_ptr = 0x341000u32;
        let target_spec_ptr = 0x341100u32;

        // FSSpec target record (vRefNum, dirID, name).
        bus.write_word(target_spec_ptr, (-1i16) as u16);
        bus.write_long(target_spec_ptr + 2, 2);
        bus.write_byte(target_spec_ptr + 6, 5);
        bus.write_bytes(target_spec_ptr + 7, b"Prefs");

        cpu.write_reg(Register::D0, 0x0002); // NewAlias selector
        bus.write_long(sp, alias_out_ptr);
        bus.write_long(sp + 4, target_spec_ptr);
        bus.write_long(sp + 8, 0); // fromFile = NIL
        bus.write_word(sp + 12, 0xBEEF); // result slot

        let result = disp.dispatch_toolbox(true, 0x023, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 12), 0); // noErr
        assert_eq!(cpu.read_reg(Register::A7), sp + 12);

        let alias_handle = bus.read_long(alias_out_ptr);
        assert_ne!(alias_handle, 0);
        let alias_data_ptr = bus.read_long(alias_handle);
        assert_ne!(alias_data_ptr, 0);
        let alias_size = bus.read_word(alias_data_ptr + 4) as usize;
        assert!(
            alias_size >= 150,
            "alias record should use the classic fixed header size"
        );
        assert_eq!(bus.read_word(alias_data_ptr + 6), 2);
        assert_eq!(bus.read_word(alias_data_ptr + 8), 0);
        assert_eq!(
            bus.read_pstring(alias_data_ptr + 10),
            b"MacintoshHD".to_vec()
        );
        assert_eq!(bus.read_pstring(alias_data_ptr + 50), b"Prefs".to_vec());
        assert_eq!(bus.read_word(alias_data_ptr + 150) as i16, 0);
        let parent_len = bus.read_word(alias_data_ptr + 152) as usize;
        let end_offset = 154 + parent_len + (parent_len % 2);
        assert_eq!(bus.read_word(alias_data_ptr + end_offset as u32) as i16, -1);
    }

    // AliasDispatch ($A823) / selector $000C ResolveAliasFile
    // IM:VI 1991 pp. 9-30..9-31: non-alias input returns noErr and wasAliased=FALSE.
    #[test]
    fn aliasdispatch_resolvealiasfile_non_alias_returns_false_flags_and_preserves_spec() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let was_aliased_ptr = 0x342000u32;
        let target_is_folder_ptr = 0x342100u32;
        let spec_ptr = 0x342200u32;

        // FSSpec input for a regular file path.
        bus.write_word(spec_ptr, (-1i16) as u16);
        bus.write_long(spec_ptr + 2, 2);
        bus.write_byte(spec_ptr + 6, 8);
        bus.write_bytes(spec_ptr + 7, b"ReadMe!!");
        let spec_before = bus.read_bytes(spec_ptr, 32);

        bus.write_byte(was_aliased_ptr, 0xFF);
        bus.write_byte(target_is_folder_ptr, 0xFF);

        cpu.write_reg(Register::D0, 0x000C); // ResolveAliasFile selector
        bus.write_long(sp, was_aliased_ptr);
        bus.write_long(sp + 4, target_is_folder_ptr);
        bus.write_word(sp + 8, 1); // resolveAliasChains = TRUE
        bus.write_long(sp + 10, spec_ptr);
        bus.write_word(sp + 14, 0xBEEF); // result slot

        let result = disp.dispatch_toolbox(true, 0x023, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 14), 0); // noErr
        assert_eq!(cpu.read_reg(Register::A7), sp + 14);
        assert_eq!(bus.read_byte(was_aliased_ptr), 0);
        assert_eq!(bus.read_byte(target_is_folder_ptr), 0);
        assert_eq!(bus.read_bytes(spec_ptr, 32), spec_before);
    }

    // Pack8 / Apple Events ($A816)
    // Inside Macintosh: Interapplication Communication 1993:
    // AEInstallEventHandler pp.4-62..4-64, AEProcessAppleEvent pp.4-66..4-67.

    #[test]
    fn pack8_unhandled_selector_pops_param_words_and_returns_noerr() {
        // High byte of D0.W encodes parameter word count for Pack8 calls.
        // Keep this generic selector-pop contract pinned for stubbed routines.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        cpu.write_reg(Register::D0, 0x0720); // 7 words params, routine $20
        for i in 0..14 {
            bus.write_byte(sp + i, 0);
        }
        bus.write_word(sp + 14, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 14), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 14);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    #[test]
    fn pack8_aeinstalleventhandler_installs_dispatch_entry_for_event_class_and_id() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let event_class = u32::from_be_bytes(*b"aevt");
        let event_id = u32::from_be_bytes(*b"oapp");
        let handler_ptr = 0x00AB_CDEFu32;
        let handler_refcon = 0x0102_0304u32;

        // Selector $091F => AEInstallEventHandler.
        cpu.write_reg(Register::D0, 0x091F);
        bus.write_word(sp, 0); // isSysHandler = FALSE
        bus.write_long(sp + 2, handler_refcon);
        bus.write_long(sp + 6, handler_ptr);
        bus.write_long(sp + 10, event_id);
        bus.write_long(sp + 14, event_class);
        bus.write_word(sp + 18, 0xBEEF); // OSErr result slot

        let result = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 18), 0); // noErr
        assert_eq!(cpu.read_reg(Register::A7), sp + 18);
        assert_eq!(
            disp.ae_handlers.get(&(event_class, event_id)),
            Some(&(handler_ptr, handler_refcon))
        );
    }

    #[test]
    fn pack8_aeinstalleventhandler_replaces_existing_entry_for_same_event_key() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let event_class = u32::from_be_bytes(*b"aevt");
        let event_id = u32::from_be_bytes(*b"oapp");

        // Inside Macintosh: Interapplication Communication p.4-64:
        // an existing entry for the same class/ID is replaced.
        cpu.write_reg(Register::D0, 0x091F);
        bus.write_word(sp, 0); // isSysHandler
        bus.write_long(sp + 2, 0x1111_2222); // refcon #1
        bus.write_long(sp + 6, 0x00AA_0001); // handler #1
        bus.write_long(sp + 10, event_id);
        bus.write_long(sp + 14, event_class);
        bus.write_word(sp + 18, 0xBEEF);
        let first = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
        assert!(first.is_some());
        assert!(first.unwrap().is_ok());

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x091F);
        bus.write_word(sp, 0); // isSysHandler
        bus.write_long(sp + 2, 0x3333_4444); // refcon #2
        bus.write_long(sp + 6, 0x00BB_0002); // handler #2
        bus.write_long(sp + 10, event_id);
        bus.write_long(sp + 14, event_class);
        bus.write_word(sp + 18, 0xBEEF);
        let second = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
        assert!(second.is_some());
        assert!(second.unwrap().is_ok());

        assert_eq!(
            disp.ae_handlers.get(&(event_class, event_id)),
            Some(&(0x00BB_0002u32, 0x3333_4444u32))
        );
    }

    #[test]
    fn pack8_aeprocessappleevent_dispatches_matching_event_to_installed_handler() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let event_class = u32::from_be_bytes(*b"aevt");
        let event_id = u32::from_be_bytes(*b"oapp");
        let handler_ptr = 0x0040_8000u32;
        let handler_refcon = 0xDEAD_BEEFu32;
        let event_record_ptr = 0x0032_0000u32;
        let return_pc = 0x00F0_1234u32;

        // Install OAPP handler first (selector $091F).
        cpu.write_reg(Register::D0, 0x091F);
        bus.write_word(sp, 0); // isSysHandler
        bus.write_long(sp + 2, handler_refcon);
        bus.write_long(sp + 6, handler_ptr);
        bus.write_long(sp + 10, event_id);
        bus.write_long(sp + 14, event_class);
        bus.write_word(sp + 18, 0xBEEF);
        let install = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
        assert!(install.is_some());
        assert!(install.unwrap().is_ok());

        // Selector $021B => AEProcessAppleEvent(eventRecord).
        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::PC, return_pc);
        cpu.write_reg(Register::D0, 0x021B);
        bus.write_long(sp, event_record_ptr);
        bus.write_word(sp + 4, 0xBEEF); // OSErr result slot

        let process = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
        assert!(process.is_some());
        assert!(process.unwrap().is_ok());

        // AEProcessAppleEvent dispatches to the matching handler.
        assert_eq!(cpu.read_reg(Register::PC), handler_ptr);
        assert_eq!(cpu.read_reg(Register::A7), sp - 12);
        assert!(disp.fired_oapp_handler);

        let state = disp
            .ae_call_state
            .clone()
            .expect("expected in-flight AE call state");
        assert_eq!(state.return_pc, return_pc);
        assert_eq!(state.expected_sp_after_rtd, sp + 4);
        assert_eq!(
            bus.read_long(sp - 12),
            disp.ae_trampoline_addr
                .expect("trampoline should be allocated")
        );
        assert_eq!(bus.read_long(sp - 8), handler_refcon);
        assert_ne!(bus.read_long(sp - 4), 0, "reply AEDesc pointer");

        let passed_event_desc = bus.read_long(sp);
        assert_ne!(
            passed_event_desc, event_record_ptr,
            "handler must receive an AppleEvent AEDesc, not the source EventRecord"
        );
        assert_eq!(bus.read_long(passed_event_desc), event_class);
        assert_ne!(bus.read_long(passed_event_desc + 4), 0);
    }

    #[test]
    fn pack8_aeprocessappleevent_dispatches_double_wildcard_handler() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let handler_ptr = 0x0040_8800u32;
        let handler_refcon = 0xCAFE_BABEu32;
        let return_pc = 0x00F0_2468u32;

        // Install a typeWildCard/typeWildCard handler. Inside Macintosh
        // Volume VI, 6-28 to 6-29 says this entry receives all Apple events.
        cpu.write_reg(Register::D0, 0x091F);
        bus.write_word(sp, 0); // isSysHandler
        bus.write_long(sp + 2, handler_refcon);
        bus.write_long(sp + 6, handler_ptr);
        bus.write_long(sp + 10, AE_TYPE_WILDCARD);
        bus.write_long(sp + 14, AE_TYPE_WILDCARD);
        bus.write_word(sp + 18, 0xBEEF);
        let install = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
        assert!(install.is_some());
        assert!(install.unwrap().is_ok());

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::PC, return_pc);
        cpu.write_reg(Register::D0, 0x021B);
        bus.write_long(sp, 0x0032_0000);
        bus.write_word(sp + 4, 0xBEEF);
        let process = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
        assert!(process.is_some());
        assert!(process.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::PC), handler_ptr);
        assert_eq!(cpu.read_reg(Register::A7), sp - 12);
        assert_eq!(bus.read_long(sp - 8), handler_refcon);
        assert!(disp.fired_oapp_handler);

        let state = disp
            .ae_call_state
            .clone()
            .expect("expected in-flight AE call state");
        assert_eq!(state.return_pc, return_pc);
        assert_eq!(state.expected_sp_after_rtd, sp + 4);
    }

    #[test]
    fn pack8_aegetparamdesc_missing_parameter_returns_err_and_null_desc() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let event_desc = 0x0030_0000u32;
        let result_desc = 0x0030_0100u32;

        bus.write_long(event_desc, AE_TYPE_APPLE_EVENT);
        bus.write_long(event_desc + 4, 0x0030_0200);
        disp.ae_events.insert(
            event_desc,
            crate::trap::dispatch::SyntheticAppleEvent {
                event_class: AE_TYPE_APPLE_EVENT,
                event_id: u32::from_be_bytes(*b"oapp"),
            },
        );

        // Selector $0812 => AEGetParamDesc(event, keyDirectObject,
        // typeWildCard, result). The synthetic OAPP event has no direct
        // parameter, so the result AEDesc must be typeNull and the OSErr
        // must be errAEDescNotFound.
        cpu.write_reg(Register::D0, 0x0812);
        bus.write_long(sp, result_desc);
        bus.write_long(sp + 4, AE_TYPE_WILDCARD);
        bus.write_long(sp + 8, u32::from_be_bytes(*b"----"));
        bus.write_long(sp + 12, event_desc);
        bus.write_word(sp + 16, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_long(result_desc), AE_TYPE_NULL);
        assert_eq!(bus.read_long(result_desc + 4), 0);
        assert_eq!(bus.read_word(sp + 16), AE_ERR_DESC_NOT_FOUND as u16);
        assert_eq!(
            cpu.read_reg(Register::D0),
            AE_ERR_DESC_NOT_FOUND as i32 as u32
        );
        assert_eq!(cpu.read_reg(Register::A7), sp + 16);
    }

    #[test]
    fn pack8_aegetattributeptr_returns_event_class_and_id_attributes() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let event_desc = 0x0030_0000u32;
        let type_code_ptr = 0x0030_0100u32;
        let data_ptr = 0x0030_0200u32;
        let actual_size_ptr = 0x0030_0300u32;
        let event_class = AE_TYPE_APPLE_EVENT;
        let event_id = u32::from_be_bytes(*b"oapp");

        bus.write_long(event_desc, AE_TYPE_APPLE_EVENT);
        bus.write_long(event_desc + 4, 0x0030_0400);
        disp.ae_events.insert(
            event_desc,
            crate::trap::dispatch::SyntheticAppleEvent {
                event_class,
                event_id,
            },
        );

        // Selector $0E15 => AEGetAttributePtr(event, keyEventIDAttr,
        // typeWildCard, typeCode, dataPtr, maximumSize, actualSize).
        cpu.write_reg(Register::D0, 0x0E15);
        bus.write_long(sp, actual_size_ptr);
        bus.write_long(sp + 4, 4);
        bus.write_long(sp + 8, data_ptr);
        bus.write_long(sp + 12, type_code_ptr);
        bus.write_long(sp + 16, AE_TYPE_WILDCARD);
        bus.write_long(sp + 20, AE_KEY_EVENT_ID_ATTR);
        bus.write_long(sp + 24, event_desc);
        bus.write_word(sp + 28, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 28), 0);
        assert_eq!(bus.read_long(type_code_ptr), AE_TYPE_TYPE);
        assert_eq!(bus.read_long(actual_size_ptr), 4);
        assert_eq!(bus.read_long(data_ptr), event_id);
        assert_eq!(cpu.read_reg(Register::A7), sp + 28);
        assert_eq!(cpu.read_reg(Register::D0), 0);

        // Repeat for keyEventClassAttr to make both required attributes
        // observable through the same pointer-returning routine.
        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x0E15);
        bus.write_long(sp, actual_size_ptr);
        bus.write_long(sp + 4, 4);
        bus.write_long(sp + 8, data_ptr);
        bus.write_long(sp + 12, type_code_ptr);
        bus.write_long(sp + 16, AE_TYPE_WILDCARD);
        bus.write_long(sp + 20, AE_KEY_EVENT_CLASS_ATTR);
        bus.write_long(sp + 24, event_desc);
        bus.write_word(sp + 28, 0xBEEF);
        let class_result = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
        assert!(class_result.is_some());
        assert!(class_result.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 28), 0);
        assert_eq!(bus.read_long(type_code_ptr), AE_TYPE_TYPE);
        assert_eq!(bus.read_long(actual_size_ptr), 4);
        assert_eq!(bus.read_long(data_ptr), event_class);
    }

    #[test]
    fn pack8_aeresolve_non_object_spec_returns_err_and_null_token() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let object_specifier = 0x0030_0000u32;
        let token = 0x0030_0100u32;

        bus.write_long(object_specifier, AE_TYPE_NULL);
        bus.write_long(object_specifier + 4, 0);
        bus.write_long(token, 0xDEAD_BEEFu32);
        bus.write_long(token + 4, 0xCAFE_BABEu32);

        // Selector $0536 => AEResolve(objectSpecifier, callbackFlags,
        // token). A non-object-specifier input must not resolve
        // successfully; Inside Macintosh says AEResolve returns a null
        // descriptor in `theToken` if an error occurs.
        cpu.write_reg(Register::D0, 0x0536);
        bus.write_long(sp, token);
        bus.write_word(sp + 4, 0); // kAEIDoMinimum
        bus.write_long(sp + 6, object_specifier);
        bus.write_word(sp + 10, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_long(token), AE_TYPE_NULL);
        assert_eq!(bus.read_long(token + 4), 0);
        assert_eq!(bus.read_word(sp + 10), AE_ERR_NOT_AN_OBJECT_SPEC as u16);
        assert_eq!(
            cpu.read_reg(Register::D0),
            AE_ERR_NOT_AN_OBJECT_SPEC as i32 as u32
        );
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
    }

    #[test]
    fn pack8_aeprocessappleevent_returns_handler_result_code_to_caller() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let event_class = u32::from_be_bytes(*b"aevt");
        let event_id = u32::from_be_bytes(*b"oapp");
        let handler_ptr = 0x0040_9000u32;
        let return_pc = 0x00F0_5678u32;
        let handler_result = (-1708i16) as u16; // errAEEventNotHandled

        // Install handler.
        cpu.write_reg(Register::D0, 0x091F);
        bus.write_word(sp, 0); // isSysHandler
        bus.write_long(sp + 2, 0x1111_2222);
        bus.write_long(sp + 6, handler_ptr);
        bus.write_long(sp + 10, event_id);
        bus.write_long(sp + 14, event_class);
        bus.write_word(sp + 18, 0xBEEF);
        let install = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
        assert!(install.is_some());
        assert!(install.unwrap().is_ok());

        // Enter AEProcessAppleEvent path and dispatch handler.
        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::PC, return_pc);
        cpu.write_reg(Register::D0, 0x021B);
        bus.write_long(sp, 0x0032_1000); // EventRecord ptr
        bus.write_word(sp + 4, 0xBEEF); // OSErr slot
        let process = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
        assert!(process.is_some());
        assert!(process.unwrap().is_ok());

        let state = disp
            .ae_call_state
            .clone()
            .expect("expected in-flight AE call state");

        // Simulate handler writing function result then returning through
        // trampoline (`MOVE.W #$FEFE, D0; _Pack8`).
        bus.write_word(state.expected_sp_after_rtd, handler_result);
        cpu.write_reg(Register::A7, state.expected_sp_after_rtd);
        cpu.write_reg(Register::D0, 0xFEFE);
        let trampoline = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
        assert!(trampoline.is_some());
        assert!(trampoline.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::PC), return_pc);
        assert_eq!(cpu.read_reg(Register::A7), state.expected_sp_after_rtd);
        assert_eq!(bus.read_word(state.expected_sp_after_rtd), handler_result);
        assert_eq!(
            cpu.read_reg(Register::D0),
            handler_result as i16 as i32 as u32
        );
        assert!(disp.ae_call_state.is_none());
    }

    #[test]
    fn pack8_aeprocessappleevent_dispatches_matching_event_on_repeated_calls() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let event_class = u32::from_be_bytes(*b"aevt");
        let event_id = u32::from_be_bytes(*b"oapp");
        let handler_ptr = 0x0040_A000u32;
        let handler_result = (-1708i16) as u16; // errAEEventNotHandled
        let first_return_pc = 0x00F0_1234u32;
        let second_return_pc = 0x00F0_5678u32;

        // Install handler.
        cpu.write_reg(Register::D0, 0x091F);
        bus.write_word(sp, 0); // isSysHandler
        bus.write_long(sp + 2, 0x1111_2222);
        bus.write_long(sp + 6, handler_ptr);
        bus.write_long(sp + 10, event_id);
        bus.write_long(sp + 14, event_class);
        bus.write_word(sp + 18, 0xBEEF);
        let install = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
        assert!(install.is_some());
        assert!(install.unwrap().is_ok());

        // First AEProcessAppleEvent dispatch.
        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::PC, first_return_pc);
        cpu.write_reg(Register::D0, 0x021B);
        bus.write_long(sp, 0x0032_1000);
        bus.write_word(sp + 4, 0xBEEF);
        let process1 = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
        assert!(process1.is_some());
        assert!(process1.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::PC), handler_ptr);
        assert!(disp.fired_oapp_handler);

        let state1 = disp
            .ae_call_state
            .clone()
            .expect("expected first in-flight AE call state");
        bus.write_word(state1.expected_sp_after_rtd, handler_result);
        cpu.write_reg(Register::A7, state1.expected_sp_after_rtd);
        cpu.write_reg(Register::D0, 0xFEFE);
        let tramp1 = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
        assert!(tramp1.is_some());
        assert!(tramp1.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::PC), first_return_pc);
        assert_eq!(
            cpu.read_reg(Register::D0),
            handler_result as i16 as i32 as u32
        );
        assert!(disp.ae_call_state.is_none());

        // Second AEProcessAppleEvent dispatch should still fire.
        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::PC, second_return_pc);
        cpu.write_reg(Register::D0, 0x021B);
        bus.write_long(sp, 0x0032_2000);
        bus.write_word(sp + 4, 0xBEEF);
        let process2 = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
        assert!(process2.is_some());
        assert!(process2.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::PC), handler_ptr);
        assert!(disp.fired_oapp_handler);

        let state2 = disp
            .ae_call_state
            .clone()
            .expect("expected second in-flight AE call state");
        bus.write_word(state2.expected_sp_after_rtd, handler_result);
        cpu.write_reg(Register::A7, state2.expected_sp_after_rtd);
        cpu.write_reg(Register::D0, 0xFEFE);
        let tramp2 = disp.dispatch_toolbox(true, 0x016, &mut cpu, &mut bus);
        assert!(tramp2.is_some());
        assert!(tramp2.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::PC), second_return_pc);
        assert_eq!(
            cpu.read_reg(Register::D0),
            handler_result as i16 as i32 as u32
        );
        assert!(disp.ae_call_state.is_none());
    }

    fn write_styled_line_break_frame(
        bus: &mut MacMemoryBus,
        sp: u32,
        text_ptr: u32,
        text_len: u32,
        text_start: u32,
        text_end: u32,
        text_width_ptr: u32,
        text_offset_ptr: u32,
    ) {
        bus.write_long(sp, 0x821C_FFFE);
        bus.write_long(sp + 4, text_offset_ptr);
        bus.write_long(sp + 8, text_width_ptr);
        bus.write_long(sp + 12, 0); // flags
        bus.write_long(sp + 16, text_end);
        bus.write_long(sp + 20, text_start);
        bus.write_long(sp + 24, text_len);
        bus.write_long(sp + 28, text_ptr);
        bus.write_word(sp + 32, 0xBEEF); // StyledLineBreakCode result
    }

    // ScriptUtil ($A8B5) selector 0 FontScript
    // IM:V 1988 pp. V-288 and V-315
    #[test]
    fn scriptutil_fontscript_returns_smroman_and_pops_selector_long() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_long(sp, 0x0000_0000); // selector 0 (FontScript)
        bus.write_word(sp + 4, 0xBEEF); // result slot

        let result = disp.dispatch_toolbox(true, 0x0B5, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 4), 0); // smRoman
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
    }

    // ScriptUtil ($A8B5) selector 8 GetEnvirons
    // IM:V 1988 pp. V-288 and V-311
    #[test]
    fn scriptutil_getenvirons_returns_zero_long_and_pops_selector_plus_verb() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_long(sp, 0x0000_0008); // selector 8
        bus.write_word(sp + 4, 0x1234); // verb
        bus.write_long(sp + 6, 0xDEAD_BEEF); // LongInt result slot

        let result = disp.dispatch_toolbox(true, 0x0B5, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_long(sp + 6), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 6);
    }

    // ScriptUtil ($A8B5) selector 10 SetEnvirons
    // IM:V 1988 pp. V-288 and V-311
    #[test]
    fn scriptutil_setenvirons_returns_noerr_and_pops_selector_verb_param() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_long(sp, 0x0000_000A); // selector 10
        bus.write_word(sp + 4, 0x0001); // verb
        bus.write_long(sp + 6, 0xCAFE_BABE); // param
        bus.write_word(sp + 10, 0xBEEF); // OSErr result slot

        let result = disp.dispatch_toolbox(true, 0x0B5, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 10), 0); // noErr
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
    }

    // ScriptUtil ($A8B5) selector 12 GetScript
    // IM:V 1988 pp. V-288 and V-312..V-313
    #[test]
    fn scriptutil_getscript_returns_zero_long_and_pops_selector_script_verb() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_long(sp, 0x0000_000C); // selector 12
        bus.write_word(sp + 4, 0); // smRoman
        bus.write_word(sp + 6, 1); // smScriptRight
        bus.write_long(sp + 8, 0xDEAD_BEEF); // LongInt result slot

        let result = disp.dispatch_toolbox(true, 0x0B5, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_long(sp + 8), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    // ScriptUtil ($A8B5) selector 14 SetScript
    // IM:V 1988 pp. V-288 and V-312..V-313
    #[test]
    fn scriptutil_setscript_returns_noerr_and_pops_selector_script_verb_param() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_long(sp, 0x0000_000E); // selector 14
        bus.write_word(sp + 4, 0); // smRoman
        bus.write_word(sp + 6, 1); // smScriptRight
        bus.write_long(sp + 8, 0); // param
        bus.write_word(sp + 12, 0xBEEF); // OSErr result slot

        let result = disp.dispatch_toolbox(true, 0x0B5, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 12), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 12);
        assert_eq!(cpu.read_reg(Register::D0), 0);
    }

    // ScriptUtil ($A8B5) selector 20 Pixel2Char
    // IM:V 1988 p. V-310
    // Pascal calling convention: leadingEdge VAR pointer is the LAST arg
    // pushed, so it lives at sp+4 just after the selector long; textBuf
    // is the FIRST arg pushed and lives deepest at sp+14.
    #[test]
    fn scriptutil_pixel2char_returns_offset_zero_and_clears_leadingedge() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let leading_edge_ptr = 0x362000u32;

        bus.write_long(sp, 0x0000_0014); // selector 20
        bus.write_long(sp + 4, leading_edge_ptr); // VAR leadingEdge (last arg)
        bus.write_word(sp + 8, 12); // pixelWidth
        bus.write_word(sp + 10, 0); // slop
        bus.write_word(sp + 12, 8); // textLen
        bus.write_long(sp + 14, 0x363000); // textBuf (first arg, deepest)
        bus.write_word(sp + 18, 0xBEEF); // INTEGER result
        bus.write_byte(leading_edge_ptr, 0xFF);

        let result = disp.dispatch_toolbox(true, 0x0B5, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_byte(leading_edge_ptr), 0); // FALSE
        assert_eq!(bus.read_word(sp + 18), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 18);
    }

    // ScriptUtil ($A8B5) selector 26 FindWord
    // IM:V 1988 pp. V-312..V-313
    // Pascal calling convention: VAR offsets pointer is the LAST arg
    // pushed (sp+4); textPtr is FIRST and deepest (sp+18). OffsetTable
    // is 12 bytes (ARRAY[0..2] OF OffPair) per IM:VI 33514.
    #[test]
    fn scriptutil_findword_zeros_offsettable_and_pops_selector_plus_args() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let offsets_ptr = 0x364000u32;

        bus.write_long(sp, 0x0000_001A); // selector 26
        bus.write_long(sp + 4, offsets_ptr); // VAR offsets (last arg)
        bus.write_long(sp + 8, 0x366000); // breaksPtr
        bus.write_word(sp + 12, 1); // leadingEdge
        bus.write_word(sp + 14, 4); // offset
        bus.write_word(sp + 16, 12); // textLength
        bus.write_long(sp + 18, 0x365000); // textPtr (first arg, deepest)
        bus.write_bytes(offsets_ptr, &[0xA5; 12]);

        let result = disp.dispatch_toolbox(true, 0x0B5, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_bytes(offsets_ptr, 12), vec![0; 12]);
        assert_eq!(cpu.read_reg(Register::A7), sp + 22);
    }

    // ScriptUtil ($A8B5) encoded selector $821CFFFE StyledLineBreak
    // Text 1993 pp. 5-79..5-81.
    #[test]
    fn scriptutil_styledlinebreak_full_style_run_fits_returns_overflow_and_decrements_width() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let text_ptr = 0x365000u32;
        let width_ptr = 0x366000u32;
        let offset_ptr = 0x366010u32;
        let text = b"SHORT";
        bus.write_bytes(text_ptr, text);
        let run_width =
            disp.scriptutil_measure_text_range_width(&bus, text_ptr, 0, text.len() as u32);
        let starting_width = run_width + 20;

        write_styled_line_break_frame(
            &mut bus,
            sp,
            text_ptr,
            text.len() as u32,
            0,
            text.len() as u32,
            width_ptr,
            offset_ptr,
        );
        bus.write_long(width_ptr, (starting_width as u32) << 16);
        bus.write_long(offset_ptr, 0xFFFF_FFFF);

        let result = disp.dispatch_toolbox(true, 0x0B5, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 32), 2); // smBreakOverflow
        assert_eq!(bus.read_long(offset_ptr), text.len() as u32);
        assert_eq!(bus.read_long(width_ptr), 20u32 << 16);
        assert_eq!(cpu.read_reg(Register::A7), sp + 32);
    }

    // ScriptUtil ($A8B5) encoded selector $821CFFFE StyledLineBreak
    // Text 1993 pp. 5-79..5-81.
    #[test]
    fn scriptutil_styledlinebreak_breaks_at_last_space_before_overflow() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let text_ptr = 0x365000u32;
        let width_ptr = 0x366000u32;
        let offset_ptr = 0x366010u32;
        let text = b"HELLO WORLD";
        bus.write_bytes(text_ptr, text);
        let available = disp.scriptutil_measure_text_range_width(&bus, text_ptr, 0, 7);
        let consumed = disp.scriptutil_measure_text_range_width(&bus, text_ptr, 0, 6);

        write_styled_line_break_frame(
            &mut bus,
            sp,
            text_ptr,
            text.len() as u32,
            0,
            text.len() as u32,
            width_ptr,
            offset_ptr,
        );
        bus.write_long(width_ptr, (available as u32) << 16);
        bus.write_long(offset_ptr, 0xFFFF_FFFF);

        let result = disp.dispatch_toolbox(true, 0x0B5, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 32), 0); // smBreakWord
        assert_eq!(bus.read_long(offset_ptr), 6); // after the space
        assert_eq!(
            bus.read_long(width_ptr),
            ((available - consumed) as u32) << 16
        );
        assert_eq!(cpu.read_reg(Register::A7), sp + 32);
    }

    // ScriptUtil ($A8B5) encoded selector $821CFFFE StyledLineBreak
    // Text 1993 pp. 5-79..5-81.
    #[test]
    fn scriptutil_styledlinebreak_first_long_word_breaks_on_character_boundary() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let text_ptr = 0x365000u32;
        let width_ptr = 0x366000u32;
        let offset_ptr = 0x366010u32;
        let text = b"ABCDEFGHIJ";
        bus.write_bytes(text_ptr, text);
        let available = disp.scriptutil_measure_text_range_width(&bus, text_ptr, 0, 3);

        write_styled_line_break_frame(
            &mut bus,
            sp,
            text_ptr,
            text.len() as u32,
            0,
            text.len() as u32,
            width_ptr,
            offset_ptr,
        );
        bus.write_long(width_ptr, (available as u32) << 16);
        bus.write_long(offset_ptr, 0xFFFF_FFFF);

        let result = disp.dispatch_toolbox(true, 0x0B5, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 32), 1); // smBreakChar
        assert_eq!(bus.read_long(offset_ptr), 3);
        assert_eq!(bus.read_long(width_ptr), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 32);
    }

    // ScriptUtil ($A8B5) selector $820CFFDE TruncText
    // IM:VI 1991 pp. 14-59..14-60 and Table C-3.
    #[test]
    fn scriptutil_trunctext_returns_not_truncated_and_pops_encoded_frame() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let length_ptr = 0x366000u32;

        bus.write_long(sp, 0x820C_FFDE); // TruncText encoded selector
        bus.write_word(sp + 4, 0); // truncWhere
        bus.write_long(sp + 6, length_ptr); // VAR length
        bus.write_long(sp + 10, 0x367000); // textPtr
        bus.write_word(sp + 14, 80); // width
        bus.write_word(sp + 16, 0xBEEF); // INTEGER result
        bus.write_word(length_ptr, 12);

        let result = disp.dispatch_toolbox(true, 0x0B5, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 16), 0); // smNotTruncated
        assert_eq!(bus.read_word(length_ptr), 12);
        assert_eq!(cpu.read_reg(Register::A7), sp + 16);
    }

    // ScriptUtil ($A8B5) encoded selector fallback
    // IM:VI Table C-3 stores result size and argument byte count in the high word.
    #[test]
    fn scriptutil_unknown_encoded_selector_uses_stack_metadata() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_long(sp, 0x8204_ABCD); // 2-byte result, 4 arg bytes
        bus.write_long(sp + 4, 0xCAFE_BABE); // opaque args
        bus.write_word(sp + 8, 0xBEEF); // result slot

        let result = disp.dispatch_toolbox(true, 0x0B5, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 8), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
    }

    // FMSwapFont ($A901)
    // IM:I 1985 pp. I-223 and I-225.
    #[test]
    fn fmswapfont_returns_fmoutptr_and_pops_fminput_frame() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        // FMInput PACKED RECORD by value (16 bytes).
        bus.write_word(sp, 3); // family
        bus.write_word(sp + 2, 12); // size
        bus.write_byte(sp + 4, 0); // face
        bus.write_byte(sp + 5, 1); // needBits
        bus.write_word(sp + 6, 0); // device
        bus.write_word(sp + 8, 1); // numer.v
        bus.write_word(sp + 10, 1); // numer.h
        bus.write_word(sp + 12, 1); // denom.v
        bus.write_word(sp + 14, 1); // denom.h
        bus.write_long(sp + 16, 0); // result slot

        let result = disp.dispatch_toolbox(true, 0x101, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        let fm_out_ptr = bus.read_long(sp + 16);
        assert_ne!(fm_out_ptr, 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 16);
    }

    // FMSwapFont ($A901)
    // IM:I 1985 p. I-225: FMOutput.errNum is 0 and the output record
    // carries a non-NIL fontHandle plus scaling fields.
    #[test]
    fn fmswapfont_writes_non_nil_font_handle_and_fixed_point_scaling_fields() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_word(sp, 3); // family
        bus.write_word(sp + 2, 12); // size
        bus.write_byte(sp + 4, 0); // face
        bus.write_byte(sp + 5, 1); // needBits
        bus.write_word(sp + 6, 0); // device
        bus.write_word(sp + 8, 1); // numer.v
        bus.write_word(sp + 10, 1); // numer.h
        bus.write_word(sp + 12, 1); // denom.v
        bus.write_word(sp + 14, 1); // denom.h
        bus.write_long(sp + 16, 0); // result slot

        let result = disp.dispatch_toolbox(true, 0x101, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        let fm_out_ptr = bus.read_long(sp + 16);
        assert_ne!(fm_out_ptr, 0);
        assert_eq!(bus.read_word(fm_out_ptr), 0); // errNum
        assert_ne!(bus.read_long(fm_out_ptr + 2), 0); // fontHandle is non-NIL
        assert_eq!(bus.read_byte(fm_out_ptr + 13), 9); // ascent for size 12
        assert_eq!(bus.read_byte(fm_out_ptr + 14), 1); // descent
        assert_eq!(bus.read_byte(fm_out_ptr + 15), 7); // widMax
        assert_eq!(bus.read_byte(fm_out_ptr + 16), 0); // leading
        assert_eq!(bus.read_word(fm_out_ptr + 18), 0x0100); // numer.v
        assert_eq!(bus.read_word(fm_out_ptr + 20), 0x0100); // numer.h
        assert_eq!(bus.read_word(fm_out_ptr + 22), 0x0100); // denom.v
        assert_eq!(bus.read_word(fm_out_ptr + 24), 0x0100); // denom.h
    }

    // FMSwapFont ($A901)
    // IM:I 1985 p. I-225: the output record's fontHandle is a handle to
    // the chosen font record, not the FMOutput block itself.
    #[test]
    fn fmswapfont_writes_distinct_non_nil_font_handle() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_word(sp, 3); // family
        bus.write_word(sp + 2, 12); // size
        bus.write_byte(sp + 4, 0); // face
        bus.write_byte(sp + 5, 1); // needBits
        bus.write_word(sp + 6, 0); // device
        bus.write_word(sp + 8, 1); // numer.v
        bus.write_word(sp + 10, 1); // numer.h
        bus.write_word(sp + 12, 1); // denom.v
        bus.write_word(sp + 14, 1); // denom.h
        bus.write_long(sp + 16, 0); // result slot

        let result = disp.dispatch_toolbox(true, 0x101, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        let fm_out_ptr = bus.read_long(sp + 16);
        assert_ne!(fm_out_ptr, 0);
        let font_handle = bus.read_long(fm_out_ptr + 2);
        assert_ne!(font_handle, 0);
        assert_ne!(font_handle, fm_out_ptr);
        assert_eq!(bus.read_word(fm_out_ptr + 18), 0x0100); // numer.v
        assert_eq!(bus.read_word(fm_out_ptr + 20), 0x0100); // numer.h
        assert_eq!(bus.read_word(fm_out_ptr + 22), 0x0100); // denom.v
        assert_eq!(bus.read_word(fm_out_ptr + 24), 0x0100); // denom.h
    }

    // FMSwapFont ($A901)
    // HLE retains a compact input signature in the auxiliary word of
    // the returned font-handle block for later font-manager consumers.
    #[test]
    fn fmswapfont_aux_font_handle_carries_input_signature() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_word(sp, 3); // family
        bus.write_word(sp + 2, 12); // size
        bus.write_byte(sp + 4, 0); // face
        bus.write_byte(sp + 5, 1); // needBits
        bus.write_word(sp + 6, 0); // device
        bus.write_word(sp + 8, 1); // numer.v
        bus.write_word(sp + 10, 1); // numer.h
        bus.write_word(sp + 12, 1); // denom.v
        bus.write_word(sp + 14, 1); // denom.h
        bus.write_long(sp + 16, 0); // result slot

        let result = disp.dispatch_toolbox(true, 0x101, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        let fm_out_ptr = bus.read_long(sp + 16);
        let font_handle = bus.read_long(fm_out_ptr + 2);
        assert_eq!(bus.read_long(font_handle + 4), 0x0003_000C);
    }

    // RealFont ($A902)
    // IM:I 1985 p. I-223: applFont always returns FALSE.
    #[test]
    fn realfont_applfont_always_returns_false_and_pops_stack() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_word(sp, 12); // size
        bus.write_word(sp + 2, 1); // fontNum = applFont
        bus.write_word(sp + 4, 0xBEEF); // Boolean result slot

        let result = disp.dispatch_toolbox(true, 0x102, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(bus.read_word(sp + 4), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
    }

    // RealFont ($A902)
    // IM:I 1985 p. I-223: return TRUE if size is available; FALSE if scaling needed.
    #[test]
    fn realfont_known_bitmap_size_true_and_nonstandard_size_false() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_word(sp, 12); // size
        bus.write_word(sp + 2, 3); // Geneva
        bus.write_word(sp + 4, 0xBEEF);
        let first = disp.dispatch_toolbox(true, 0x102, &mut cpu, &mut bus);
        assert!(first.is_some());
        assert!(first.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 4), 0x0100);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);

        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 11); // non-standard bitmap size in current HLE model
        bus.write_word(sp + 2, 3);
        bus.write_word(sp + 4, 0xBEEF);
        let second = disp.dispatch_toolbox(true, 0x102, &mut cpu, &mut bus);
        assert!(second.is_some());
        assert!(second.unwrap().is_ok());
        assert_eq!(bus.read_word(sp + 4), 0x0000);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
    }

    // RealFont ($A902)
    // IM:I 1985 p. I-223 line 7309: "RealFont will always return
    // FALSE if you pass applFont in fontNum." Systemless HLE follows
    // the Apple-canonical rule; BasiliskII System 7.5 ROM diverges
    // (returns TRUE because applFont is bound to a real font at
    // boot) — see a902_diag_realfont diagnostic.
    #[test]
    fn realfont_applfont_returns_false_for_all_canonical_sizes_per_apple_spec() {
        let canonical_sizes: [u16; 6] = [9, 10, 12, 14, 18, 24];
        for &size in canonical_sizes.iter() {
            let (mut disp, mut cpu, mut bus) = setup();
            let sp = TEST_SP;
            bus.write_word(sp, size);
            bus.write_word(sp + 2, 1); // applFont
            bus.write_word(sp + 4, 0xBEEF);
            let result = disp.dispatch_toolbox(true, 0x102, &mut cpu, &mut bus);
            assert!(result.is_some());
            assert!(result.unwrap().is_ok());
            assert_eq!(
                bus.read_word(sp + 4),
                0,
                "applFont must return FALSE at canonical size {size}",
            );
        }
    }

    // RealFont ($A902)
    // IM:I 1985 p. I-223 line 7307: "FALSE if the font has to be
    // scaled to that size." Systemless HLE returns FALSE for sizes
    // outside the canonical bitmap set {9, 10, 12, 14, 18, 24}
    // even for real bundled bitmap fonts. BasiliskII System 7.5
    // ROM diverges (treats any valid fontNum as truthy regardless
    // of size) — see a902_diag_realfont diagnostic.
    #[test]
    fn realfont_non_standard_size_returns_false_for_real_bitmap_font_per_apple_spec() {
        let non_standard_sizes: [u16; 5] = [8, 11, 13, 15, 100];
        for &size in non_standard_sizes.iter() {
            let (mut disp, mut cpu, mut bus) = setup();
            let sp = TEST_SP;
            bus.write_word(sp, size);
            bus.write_word(sp + 2, 3); // Geneva
            bus.write_word(sp + 4, 0xBEEF);
            let result = disp.dispatch_toolbox(true, 0x102, &mut cpu, &mut bus);
            assert!(result.is_some());
            assert!(result.unwrap().is_ok());
            assert_eq!(
                bus.read_word(sp + 4),
                0,
                "Geneva at non-canonical size {size} must return FALSE",
            );
        }
    }

    // SetFontLock ($A903)
    // IM:I 1985 p. I-223: PROCEDURE SetFontLock(lockFlag: BOOLEAN).
    // Pops one 2-byte BOOLEAN argument; no function-result slot.
    #[test]
    fn setfontlock_true_pops_two_byte_boolean_argument_frame() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_word(sp, 0x0100); // TRUE in high byte
        bus.write_word(sp + 2, 0xCAFE); // sentinel above pop window
        bus.write_word(sp + 4, 0xF00D); // additional sentinel
        let result = disp.dispatch_toolbox(true, 0x103, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
        // Sentinels above the pop window survive — trap doesn't write
        // past the 2-byte argument slot.
        assert_eq!(bus.read_word(sp + 2), 0xCAFE);
        assert_eq!(bus.read_word(sp + 4), 0xF00D);
    }

    #[test]
    fn setfontlock_false_pops_two_byte_boolean_argument_frame() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_word(sp, 0x0000); // FALSE
        bus.write_word(sp + 2, 0xBABE); // sentinel above pop window
        bus.write_word(sp + 4, 0xBEEF); // additional sentinel
        let result = disp.dispatch_toolbox(true, 0x103, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
        // FALSE branch pops the same 2 bytes as TRUE (Pascal PROCEDURE
        // calling convention is value-independent).
        assert_eq!(bus.read_word(sp + 2), 0xBABE);
        assert_eq!(bus.read_word(sp + 4), 0xBEEF);
    }

    #[test]
    fn setfontlock_alternating_calls_have_net_sp_delta_zero() {
        // Eight alternating TRUE/FALSE calls — each pops exactly 2
        // bytes; net SP delta after the last pop equals starting SP
        // minus the cumulative argument bytes (verifies no value-
        // dependent per-call drift).
        let (mut disp, mut cpu, mut bus) = setup();
        let base = TEST_SP;
        let mut sp = base;
        for i in 0..8u32 {
            let value: u16 = if i & 1 == 0 { 0x0100 } else { 0x0000 };
            bus.write_word(sp, value);
            cpu.write_reg(Register::A7, sp);
            let result = disp.dispatch_toolbox(true, 0x103, &mut cpu, &mut bus);
            assert!(result.is_some());
            assert!(result.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), sp + 2);
            sp = cpu.read_reg(Register::A7);
        }
        assert_eq!(sp - base, 16);
    }

    // Fix2Frac ($A841)
    // Operating System Utilities 1994, p. 3-44; IM IV 1986, p. IV-65.
    #[test]
    fn fix2frac_returns_equivalent_fract_and_saturates_out_of_range_inputs() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        // IM IV-65 example: Fix2Frac(X2Fix(1.75)) = $70000000.
        bus.write_long(sp, 0x0001_C000);
        bus.write_long(sp + 4, 0);
        let first = disp.dispatch_toolbox(true, 0x041, &mut cpu, &mut bus);
        assert!(first.is_some());
        assert!(first.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 4), 0x7000_0000);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);

        // OS Utils 3-44: values above Fract max saturate to $7FFFFFFF.
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0x0002_0000); // +2.0 Fixed
        bus.write_long(sp + 4, 0);
        let high = disp.dispatch_toolbox(true, 0x041, &mut cpu, &mut bus);
        assert!(high.is_some());
        assert!(high.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 4), 0x7FFF_FFFF);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);

        // OS Utils 3-44: values below Fract min saturate to $80000000.
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, (-0x0002_0000i32) as u32); // -2.0 Fixed
        bus.write_long(sp + 4, 0);
        let low = disp.dispatch_toolbox(true, 0x041, &mut cpu, &mut bus);
        assert!(low.is_some());
        assert!(low.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 4), 0x8000_0000);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
    }

    // Frac2Fix ($A842)
    // Operating System Utilities 1994, p. 3-44; IM IV 1986, p. IV-65.
    #[test]
    fn frac2fix_matches_documented_positive_and_negative_examples() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        // IM IV-65 example: Frac2Fix(X2Frac(1.75)) = $0001C000.
        bus.write_long(sp, 0x7000_0000);
        bus.write_long(sp + 4, 0);
        let pos = disp.dispatch_toolbox(true, 0x042, &mut cpu, &mut bus);
        assert!(pos.is_some());
        assert!(pos.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 4), 0x0001_C000);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);

        // IM IV-65 example: Frac2Fix(X2Frac(-1.75)) = $FFFE4000.
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0x9000_0000);
        bus.write_long(sp + 4, 0);
        let neg = disp.dispatch_toolbox(true, 0x042, &mut cpu, &mut bus);
        assert!(neg.is_some());
        assert!(neg.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 4), 0xFFFE_4000);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
    }

    // Fix2X ($A843)
    // Operating System Utilities 1994, p. 3-45.
    #[test]
    fn fix2x_returns_extended_equivalent_and_pops_fixed_argument() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_long(sp, 0x0001_C000); // 1.75 Fixed
        for i in 0..10 {
            bus.write_byte(sp + 4 + i, 0);
        }

        let result = disp.dispatch_toolbox(true, 0x043, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        let ext = Extended80::read_from_bus(&bus, sp + 4);
        assert!((f64::from(ext) - 1.75).abs() < 1e-12);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
    }

    // X2Fix ($A844)
    // Operating System Utilities 1994, p. 3-45.
    #[test]
    fn x2fix_returns_best_fixed_approximation_and_saturates_out_of_range() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        Extended80::from(1.75).write_to_bus(&mut bus, sp);
        bus.write_long(sp + 10, 0);
        let exact = disp.dispatch_toolbox(true, 0x044, &mut cpu, &mut bus);
        assert!(exact.is_some());
        assert!(exact.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 10), 0x0001_C000);
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);

        cpu.write_reg(Register::A7, sp);
        Extended80::from(40000.0).write_to_bus(&mut bus, sp);
        bus.write_long(sp + 10, 0);
        let high = disp.dispatch_toolbox(true, 0x044, &mut cpu, &mut bus);
        assert!(high.is_some());
        assert!(high.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 10), 0x7FFF_FFFF);
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);

        cpu.write_reg(Register::A7, sp);
        Extended80::from(-40000.0).write_to_bus(&mut bus, sp);
        bus.write_long(sp + 10, 0);
        let low = disp.dispatch_toolbox(true, 0x044, &mut cpu, &mut bus);
        assert!(low.is_some());
        assert!(low.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 10), 0x8000_0000);
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
    }

    // Frac2X ($A845)
    // Operating System Utilities 1994, p. 3-46.
    #[test]
    fn frac2x_returns_extended_equivalent_and_pops_fract_argument() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_long(sp, 0x7000_0000); // 1.75 Fract
        for i in 0..10 {
            bus.write_byte(sp + 4 + i, 0);
        }

        let result = disp.dispatch_toolbox(true, 0x045, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        let ext = Extended80::read_from_bus(&bus, sp + 4);
        assert!((f64::from(ext) - 1.75).abs() < 1e-12);
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
    }

    // X2Frac ($A846)
    // Operating System Utilities 1994, p. 3-46.
    #[test]
    fn x2frac_returns_best_fract_approximation_and_saturates_out_of_range() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        Extended80::from(1.75).write_to_bus(&mut bus, sp);
        bus.write_long(sp + 10, 0);
        let exact = disp.dispatch_toolbox(true, 0x046, &mut cpu, &mut bus);
        assert!(exact.is_some());
        assert!(exact.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 10), 0x7000_0000);
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);

        cpu.write_reg(Register::A7, sp);
        Extended80::from(3.0).write_to_bus(&mut bus, sp);
        bus.write_long(sp + 10, 0);
        let high = disp.dispatch_toolbox(true, 0x046, &mut cpu, &mut bus);
        assert!(high.is_some());
        assert!(high.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 10), 0x7FFF_FFFF);
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);

        cpu.write_reg(Register::A7, sp);
        Extended80::from(-3.0).write_to_bus(&mut bus, sp);
        bus.write_long(sp + 10, 0);
        let low = disp.dispatch_toolbox(true, 0x046, &mut cpu, &mut bus);
        assert!(low.is_some());
        assert!(low.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 10), 0x8000_0000);
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
    }

    // FracCos ($A847)
    // Inside Macintosh IV (1986), p. IV-64; OS Utils (1994), p. 3-42.
    #[test]
    fn fraccos_zero_radians_returns_plus_one_fract() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_long(sp, 0x0000_0000); // 0.0 radians in Fixed
        bus.write_long(sp + 4, 0);
        let result = disp.dispatch_toolbox(true, 0x047, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 4), 0x4000_0000);
    }

    #[test]
    fn fraccos_consumes_fixed_argument_and_writes_result_slot() {
        // FUNCTION FracCos(x: Fixed): Fract.
        // One 4-byte Fixed argument consumed; 4-byte Fract result at post-pop [SP].
        // Inside Macintosh IV (1986), p. IV-64; OS Utils (1994), p. 3-42.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_long(sp, 0x0001_0000); // 1.0 radians in Fixed
        bus.write_long(sp + 4, 0xDEAD_BEEF);
        let result = disp.dispatch_toolbox(true, 0x047, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
        assert_ne!(
            bus.read_long(sp + 4),
            0xDEAD_BEEF,
            "FracCos should write a Fract result to the function-result slot"
        );
    }

    // FracSin ($A848)
    // Inside Macintosh IV (1986), p. IV-64; OS Utils (1994), p. 3-42.
    #[test]
    fn fracsin_zero_radians_returns_zero_fract() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_long(sp, 0x0000_0000); // 0.0 radians in Fixed
        bus.write_long(sp + 4, 0xDEAD_BEEF);
        let result = disp.dispatch_toolbox(true, 0x048, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 4), 0x0000_0000);
    }

    #[test]
    fn fracsin_consumes_fixed_argument_and_writes_result_slot() {
        // FUNCTION FracSin(x: Fixed): Fract.
        // One 4-byte Fixed argument consumed; 4-byte Fract result at post-pop [SP].
        // Inside Macintosh IV (1986), p. IV-64; OS Utils (1994), p. 3-42.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_long(sp, 0x0001_0000); // 1.0 radians in Fixed
        bus.write_long(sp + 4, 0xDEAD_BEEF);
        let result = disp.dispatch_toolbox(true, 0x048, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
        assert_ne!(
            bus.read_long(sp + 4),
            0xDEAD_BEEF,
            "FracSin should write a Fract result to the function-result slot"
        );
    }

    // FracSqrt ($A849)
    // Inside Macintosh IV (1986), pp. IV-64..IV-65; OS Utils (1994), p. 3-41.
    #[test]
    fn fracsqrt_matches_documented_iv65_example_value() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        // IM:IV IV-65 example: FracSqrt(X2Frac(1.96)) = $5999999A.
        // X2Frac(1.96) = $7D70A3D7.
        bus.write_long(sp, 0x7D70_A3D7);
        bus.write_long(sp + 4, 0);
        let result = disp.dispatch_toolbox(true, 0x049, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 4), 0x5999_999A);
    }

    #[test]
    fn fracsqrt_interprets_input_as_unsigned_fract() {
        // IM:IV IV-64 and OS Utils 3-41: FracSqrt interprets x as unsigned
        // 0..4-2^-30, so bit 31 carries weight +2 instead of -2.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        // $C0000000 is -1.0 as signed Fract but 3.0 in unsigned Fract domain.
        // sqrt(3.0) in Fract rounds to $6ED9EBA1.
        bus.write_long(sp, 0xC000_0000);
        bus.write_long(sp + 4, 0);
        let result = disp.dispatch_toolbox(true, 0x049, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 4), 0x6ED9_EBA1);
    }

    #[test]
    fn fracsqrt_consumes_fract_argument_and_writes_result_slot() {
        // FUNCTION FracSqrt(x: Fract): Fract.
        // One 4-byte Fract argument consumed; 4-byte Fract result at post-pop [SP].
        // Inside Macintosh IV (1986), p. IV-64; OS Utils (1994), p. 3-41.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_long(sp, 0x4000_0000); // 1.0 Fract
        bus.write_long(sp + 4, 0xDEAD_BEEF);
        let result = disp.dispatch_toolbox(true, 0x049, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
        assert_ne!(
            bus.read_long(sp + 4),
            0xDEAD_BEEF,
            "FracSqrt should write a Fract result to the function-result slot"
        );
    }

    // FracMul ($A84A)
    // Operating System Utilities 1994, p. 3-40; IM IV 1986, p. IV-65.
    #[test]
    fn fracmul_matches_documented_examples_and_writes_result_slot() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        // IM IV-65: FracMul(X2Frac(1.50), X2Frac(1.30)) = $7CCCCCCD.
        // Stack at entry: SP+0=b, SP+4=a, SP+8=result slot.
        bus.write_long(sp, 0x5333_3333); // X2Frac(1.30)
        bus.write_long(sp + 4, 0x6000_0000); // X2Frac(1.50)
        bus.write_long(sp + 8, 0);
        let pos = disp.dispatch_toolbox(true, 0x04A, &mut cpu, &mut bus);
        assert!(pos.is_some());
        assert!(pos.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 8), 0x7CCC_CCCD);
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);

        // IM IV-65: FracMul(X2Frac(-1.50), X2Frac(1.30)) = $83333333.
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0x5333_3333); // X2Frac(1.30)
        bus.write_long(sp + 4, 0xA000_0000); // X2Frac(-1.50)
        bus.write_long(sp + 8, 0);
        let neg = disp.dispatch_toolbox(true, 0x04A, &mut cpu, &mut bus);
        assert!(neg.is_some());
        assert!(neg.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 8), 0x8333_3333);
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
    }

    // FracDiv ($A84B)
    // Operating System Utilities 1994, pp. 3-40 to 3-41; IM IV 1986, p. IV-65.
    #[test]
    fn fracdiv_matches_documented_examples_and_writes_result_slot() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        // IM IV-65: FracDiv(X2Frac(1.95), X2Frac(1.30)) = $60000000.
        // Stack at entry: SP+0=b (denominator), SP+4=a (numerator), SP+8=result slot.
        bus.write_long(sp, 0x5333_3333); // X2Frac(1.30)
        bus.write_long(sp + 4, 0x7CCC_CCCD); // X2Frac(1.95)
        bus.write_long(sp + 8, 0);
        let pos = disp.dispatch_toolbox(true, 0x04B, &mut cpu, &mut bus);
        assert!(pos.is_some());
        assert!(pos.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 8), 0x6000_0000);
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);

        // IM IV-65: FracDiv(X2Frac(-1.95), X2Frac(1.30)) = $A0000000.
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0x5333_3333); // X2Frac(1.30)
        bus.write_long(sp + 4, 0x8333_3333); // X2Frac(-1.95)
        bus.write_long(sp + 8, 0);
        let neg = disp.dispatch_toolbox(true, 0x04B, &mut cpu, &mut bus);
        assert!(neg.is_some());
        assert!(neg.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 8), 0xA000_0000);
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
    }

    // FracDiv ($A84B) divide-by-zero saturation.
    // Operating System Utilities 1994, p. 3-41: when b==0, return
    // $80000000 if a is negative, else $7FFFFFFF (including 0/0).
    #[test]
    fn fracdiv_divide_by_zero_saturates_with_dividend_sign() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        bus.write_long(sp, 0); // b = 0
        bus.write_long(sp + 4, 0x4000_0000); // a = +1.0
        bus.write_long(sp + 8, 0);
        let pos = disp.dispatch_toolbox(true, 0x04B, &mut cpu, &mut bus);
        assert!(pos.is_some());
        assert!(pos.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 8), 0x7FFF_FFFF);
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0); // b = 0
        bus.write_long(sp + 4, 0xC000_0000); // a = -1.0
        bus.write_long(sp + 8, 0);
        let neg = disp.dispatch_toolbox(true, 0x04B, &mut cpu, &mut bus);
        assert!(neg.is_some());
        assert!(neg.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 8), 0x8000_0000);
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);

        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0); // b = 0
        bus.write_long(sp + 4, 0); // a = 0
        bus.write_long(sp + 8, 0);
        let zero = disp.dispatch_toolbox(true, 0x04B, &mut cpu, &mut bus);
        assert!(zero.is_some());
        assert!(zero.unwrap().is_ok());
        assert_eq!(bus.read_long(sp + 8), 0x7FFF_FFFF);
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
    }

    // CursorDeviceDispatch ($AADB) — selector in D0, no stack args
    #[test]
    fn test_extended_dispatch() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x1234_5678);

        let result = disp.dispatch_toolbox(true, 0x2DB, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
        assert_eq!(bus.read_word(sp + 2), 0);
    }

    // Movie Toolbox Dispatch ($AAAA)
    #[test]
    fn movietoolboxdispatch_selector_in_d0_returns_noerr_and_preserves_stack() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x0000_0001);
        cpu.write_reg(Register::D1, 0x1111_2222);
        cpu.write_reg(Register::A0, 0x3333_4444);
        cpu.write_reg(Register::A1, 0x5555_6666);
        bus.write_word(sp, 0xCAFE);
        bus.write_word(sp + 2, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(result.is_some(), "MovieToolboxDispatch should be handled");
        assert!(
            result.unwrap().is_ok(),
            "MovieToolboxDispatch should return"
        );
        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert_eq!(cpu.read_reg(Register::D1), 0x1111_2222);
        assert_eq!(cpu.read_reg(Register::A0), 0x3333_4444);
        assert_eq!(cpu.read_reg(Register::A1), 0x5555_6666);
        assert_eq!(cpu.read_reg(Register::A7), sp);
        assert_eq!(bus.read_word(sp), 0);
        assert_eq!(bus.read_word(sp + 2), 0xBEEF);
    }

    #[test]
    fn movietoolboxdispatch_exit_movies_clears_movie_state_and_preserves_stack() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x0187); // NewMovie
        bus.write_long(sp, 0);
        bus.write_long(sp + 4, 0);
        let create = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(create.is_some(), "NewMovie should be handled");
        assert!(create.unwrap().is_ok(), "NewMovie should return");
        let movie = bus.read_long(sp + 4);
        assert!(disp.movie_states.contains_key(&movie));
        disp.movie_error = -2010;
        disp.movie_sticky_error = -2010;

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x0002); // ExitMovies
        bus.write_word(sp, 0xCAFE);
        let result = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(result.is_some(), "ExitMovies should be handled");
        assert!(result.unwrap().is_ok(), "ExitMovies should return");
        assert_eq!(cpu.read_reg(Register::A7), sp);
        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert_eq!(bus.read_word(sp), 0xCAFE);
        assert!(disp.movie_states.is_empty());
        assert_eq!(disp.movie_error, 0);
        assert_eq!(disp.movie_sticky_error, 0);
    }

    #[test]
    fn movietoolboxdispatch_new_movie_returns_empty_movie_with_current_gworld() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        disp.current_port = 0x0033_0000;
        disp.current_gdevice = 0x0044_0000;

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x0187); // NewMovie
        bus.write_long(sp, 0x0000_0001);
        bus.write_long(sp + 4, 0xDEAD_BEEF);
        let result = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(result.is_some(), "NewMovie should be handled");
        assert!(result.unwrap().is_ok(), "NewMovie should return");
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
        assert_eq!(cpu.read_reg(Register::D0), 0);

        let movie = bus.read_long(sp + 4);
        assert_ne!(movie, 0);
        assert_eq!(bus.read_long(movie), u32::from_be_bytes(*b"MooV"));
        assert_eq!(bus.read_long(movie + 4), 0x0000_0001);
        assert_eq!(bus.read_long(movie + 8), 0x0033_0000);
        assert_eq!(bus.read_long(movie + 12), 0x0044_0000);
        let state = disp
            .movie_states
            .get(&movie)
            .expect("NewMovie should register MovieState");
        assert_eq!(state.box_rect, (0, 0, 120, 160));
        assert_eq!(state.gworld_port, 0x0033_0000);
        assert_eq!(state.gworld_gdh, 0x0044_0000);
        assert!(!state.active);
        assert_eq!(state.duration, 1);
    }

    #[test]
    fn movietoolboxdispatch_update_movie_pops_and_reports_validity() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x0187); // NewMovie
        bus.write_long(sp, 0);
        bus.write_long(sp + 4, 0);
        let create = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(create.is_some(), "NewMovie should be handled");
        assert!(create.unwrap().is_ok(), "NewMovie should return");
        let movie = bus.read_long(sp + 4);

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x001F); // UpdateMovie
        bus.write_long(sp, movie);
        bus.write_word(sp + 4, 0xBEEF);
        let valid = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(valid.is_some(), "UpdateMovie should be handled");
        assert!(valid.unwrap().is_ok(), "UpdateMovie should return");
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert_eq!(bus.read_word(sp + 4), 0);
        assert_eq!(disp.movie_error, 0);

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x001F); // UpdateMovie
        bus.write_long(sp, 0x00FF_EE00);
        bus.write_word(sp + 4, 0xBEEF);
        let invalid = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(invalid.is_some(), "UpdateMovie should be handled");
        assert!(invalid.unwrap().is_ok(), "UpdateMovie should return");
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
        assert_eq!(cpu.read_reg(Register::D0), (-2010i16) as u32);
        assert_eq!(bus.read_word(sp + 4), (-2010i16) as u16);
        assert_eq!(disp.movie_error, -2010);
        assert_eq!(disp.movie_sticky_error, -2010);
    }

    #[test]
    fn movietoolboxdispatch_open_movie_file_opens_vfs_movie_and_writes_refnum() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let spec_ptr = 0x300000;
        let ref_num_ptr = 0x300100;
        let movies_dir = disp.ensure_vfs_directory("AmoebArena/Movies");
        disp.vfs.insert(
            "AmoebArena/Movies/C&G".to_string(),
            vec![0x6D, 0x6F, 0x6F, 0x76],
        );
        disp.vfs_rsrc
            .insert("AmoebArena/Movies/C&G".to_string(), Vec::new());
        disp.ensure_vfs_catalog();
        write_test_fsspec(&mut bus, spec_ptr, -1, movies_dir, b"C&G");
        bus.write_word(ref_num_ptr, 0xCAFE);

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x0192);
        bus.write_word(sp, 0x0100); // fsRdPerm as SignedByte in the high byte.
        bus.write_long(sp + 2, ref_num_ptr);
        bus.write_long(sp + 6, spec_ptr);
        bus.write_word(sp + 10, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(result.is_some(), "MovieToolboxDispatch should be handled");
        assert!(result.unwrap().is_ok(), "OpenMovieFile should return");
        assert_eq!(cpu.read_reg(Register::A7), sp + 10);
        assert_eq!(bus.read_word(sp + 10), 0);
        assert_eq!(cpu.read_reg(Register::D0), 0);
        let refnum = bus.read_word(ref_num_ptr);
        assert_ne!(refnum, 0);
        assert_ne!(refnum, 0xCAFE);
        assert_eq!(
            disp.resource_file_name(refnum).map(str::to_owned),
            Some("AmoebArena/Movies/C&G".to_string())
        );
        assert_eq!(disp.current_resource_refnum(), refnum);
    }

    #[test]
    fn movietoolboxdispatch_new_movie_from_file_returns_movie_and_resource_id() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let movie_ptr = 0x300200;
        let res_id_ptr = 0x300204;
        let res_name_ptr = 0x300208;
        let changed_ptr = 0x300310;
        let refnum = 123;

        disp.register_empty_resource_file(refnum);
        disp.set_resource_file_name(refnum, "Movies/Intro");
        disp.install_named_test_resource_in_file(&mut bus, refnum, *b"moov", 128, "Intro", b"moov");
        bus.write_long(movie_ptr, 0);
        bus.write_word(res_id_ptr, 0);
        bus.write_byte(res_name_ptr, 0xEE);
        bus.write_byte(changed_ptr, 0xEE);

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x00F0);
        bus.write_long(sp, changed_ptr);
        bus.write_word(sp + 4, 1); // newMovieActive
        bus.write_long(sp + 6, res_name_ptr);
        bus.write_long(sp + 10, res_id_ptr);
        bus.write_word(sp + 14, refnum);
        bus.write_long(sp + 16, movie_ptr);
        bus.write_word(sp + 20, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(result.is_some(), "MovieToolboxDispatch should be handled");
        assert!(result.unwrap().is_ok(), "NewMovieFromFile should return");
        assert_eq!(cpu.read_reg(Register::A7), sp + 20);
        assert_eq!(bus.read_word(sp + 20), 0);
        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert_ne!(bus.read_long(movie_ptr), 0);
        assert_eq!(bus.read_word(res_id_ptr), 128);
        assert_eq!(bus.read_pstring(res_name_ptr), b"Intro");
        assert_eq!(bus.read_byte(changed_ptr), 0);
    }

    fn quicktime_atom(atom_type: [u8; 4], payload: &[u8]) -> Vec<u8> {
        let mut atom = Vec::with_capacity(8 + payload.len());
        atom.extend_from_slice(&((payload.len() + 8) as u32).to_be_bytes());
        atom.extend_from_slice(&atom_type);
        atom.extend_from_slice(payload);
        atom
    }

    #[test]
    fn quicktime_movie_metadata_reads_mvhd_duration_and_tkhd_bounds() {
        let mut mvhd = vec![0; 20];
        mvhd[16..20].copy_from_slice(&240u32.to_be_bytes());

        let mut tkhd = vec![0; 84];
        tkhd[76..80].copy_from_slice(&(320u32 << 16).to_be_bytes());
        tkhd[80..84].copy_from_slice(&(200u32 << 16).to_be_bytes());

        let trak = quicktime_atom(*b"trak", &quicktime_atom(*b"tkhd", &tkhd));
        let mut moov_payload = quicktime_atom(*b"mvhd", &mvhd);
        moov_payload.extend_from_slice(&trak);
        let moov = quicktime_atom(*b"moov", &moov_payload);

        let (bounds, duration) = quicktime_movie_metadata(&moov);
        assert_eq!(bounds, (0, 0, 200, 320));
        assert_eq!(duration, 240);
    }

    fn create_test_movie_from_file(
        disp: &mut TrapDispatcher,
        cpu: &mut MockCpu,
        bus: &mut MacMemoryBus,
        sp: u32,
    ) -> u32 {
        let movie_ptr = 0x300400;
        let res_id_ptr = 0x300404;
        let res_name_ptr = 0x300408;
        let changed_ptr = 0x300510;
        let refnum = 124;

        disp.register_empty_resource_file(refnum);
        disp.set_resource_file_name(refnum, "Movies/Test");
        disp.install_named_test_resource_in_file(bus, refnum, *b"moov", 128, "Test", b"moov");
        bus.write_long(movie_ptr, 0);
        bus.write_word(res_id_ptr, 0);
        bus.write_byte(res_name_ptr, 0);
        bus.write_byte(changed_ptr, 0);

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x00F0);
        bus.write_long(sp, changed_ptr);
        bus.write_word(sp + 4, 1);
        bus.write_long(sp + 6, res_name_ptr);
        bus.write_long(sp + 10, res_id_ptr);
        bus.write_word(sp + 14, refnum);
        bus.write_long(sp + 16, movie_ptr);
        bus.write_word(sp + 20, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x2AA, cpu, bus);
        assert!(result.is_some(), "NewMovieFromFile should be handled");
        assert!(result.unwrap().is_ok(), "NewMovieFromFile should return");
        let movie = bus.read_long(movie_ptr);
        assert_ne!(movie, 0, "NewMovieFromFile should create a movie token");
        movie
    }

    #[test]
    fn movietoolboxdispatch_movie_spatial_selectors_pop_and_round_trip_box() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let movie = create_test_movie_from_file(&mut disp, &mut cpu, &mut bus, sp);
        let box_ptr = 0x300600;
        let out_box_ptr = 0x300620;

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x0016); // SetMovieGWorld
        bus.write_long(sp, 0); // gdh
        bus.write_long(sp + 4, 0x0022_0000); // port
        bus.write_long(sp + 8, movie);
        let result = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(result.is_some(), "SetMovieGWorld should be handled");
        assert!(result.unwrap().is_ok(), "SetMovieGWorld should return");
        assert_eq!(cpu.read_reg(Register::A7), sp + 12);

        bus.write_word(box_ptr, 7);
        bus.write_word(box_ptr + 2, 11);
        bus.write_word(box_ptr + 4, 107);
        bus.write_word(box_ptr + 6, 211);
        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x00FA); // SetMovieBox
        bus.write_long(sp, box_ptr);
        bus.write_long(sp + 4, movie);
        let result = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(result.is_some(), "SetMovieBox should be handled");
        assert!(result.unwrap().is_ok(), "SetMovieBox should return");
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);

        for offset in (0..8).step_by(2) {
            bus.write_word(out_box_ptr + offset, 0xEEEE);
        }
        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x00F9); // GetMovieBox
        bus.write_long(sp, out_box_ptr);
        bus.write_long(sp + 4, movie);
        let result = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(result.is_some(), "GetMovieBox should be handled");
        assert!(result.unwrap().is_ok(), "GetMovieBox should return");
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
        assert_eq!(bus.read_word(out_box_ptr), 7);
        assert_eq!(bus.read_word(out_box_ptr + 2), 11);
        assert_eq!(bus.read_word(out_box_ptr + 4), 107);
        assert_eq!(bus.read_word(out_box_ptr + 6), 211);
    }

    #[test]
    fn movietoolboxdispatch_movie_playback_selectors_pop_and_report_done() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let movie = create_test_movie_from_file(&mut disp, &mut cpu, &mut bus, sp);

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x000B); // StartMovie
        bus.write_long(sp, movie);
        let result = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(result.is_some(), "StartMovie should be handled");
        assert!(result.unwrap().is_ok(), "StartMovie should return");
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x0005); // MoviesTask
        bus.write_long(sp, 0); // maxMilliSecToUse: service once
        bus.write_long(sp + 4, movie);
        let result = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(result.is_some(), "MoviesTask should be handled");
        assert!(result.unwrap().is_ok(), "MoviesTask should return");
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x00DD); // IsMovieDone
        bus.write_long(sp, movie);
        bus.write_word(sp + 4, 0xBEEF);
        let result = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(result.is_some(), "IsMovieDone should be handled");
        assert!(result.unwrap().is_ok(), "IsMovieDone should return");
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
        assert_eq!(bus.read_word(sp + 4), 0xFFFF);
    }

    #[test]
    fn movietoolboxdispatch_movie_preload_duration_and_volume_selectors_pop() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let movie = create_test_movie_from_file(&mut disp, &mut cpu, &mut bus, sp);

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x002B); // GetMovieDuration
        bus.write_long(sp, movie);
        bus.write_long(sp + 4, 0xDEAD_BEEF);
        let result = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(result.is_some(), "GetMovieDuration should be handled");
        assert!(result.unwrap().is_ok(), "GetMovieDuration should return");
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
        assert_ne!(bus.read_long(sp + 4), 0xDEAD_BEEF);

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x00F4); // SetMoviePreferredRate
        bus.write_long(sp, 0x0002_0000);
        bus.write_long(sp + 4, movie);
        let result = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(result.is_some(), "SetMoviePreferredRate should be handled");
        assert!(
            result.unwrap().is_ok(),
            "SetMoviePreferredRate should return"
        );
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x00F3); // GetMoviePreferredRate
        bus.write_long(sp, movie);
        bus.write_long(sp + 4, 0xDEAD_BEEF);
        let result = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(result.is_some(), "GetMoviePreferredRate should be handled");
        assert!(
            result.unwrap().is_ok(),
            "GetMoviePreferredRate should return"
        );
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
        assert_eq!(bus.read_long(sp + 4), 0x0002_0000);

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x002F); // SetMovieVolume
        bus.write_word(sp, 0x0100);
        bus.write_long(sp + 2, movie);
        let result = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(result.is_some(), "SetMovieVolume should be handled");
        assert!(result.unwrap().is_ok(), "SetMovieVolume should return");
        assert_eq!(cpu.read_reg(Register::A7), sp + 6);

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x00F6); // SetMoviePreferredVolume
        bus.write_word(sp, 0x0080);
        bus.write_long(sp + 2, movie);
        let result = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(
            result.is_some(),
            "SetMoviePreferredVolume should be handled"
        );
        assert!(
            result.unwrap().is_ok(),
            "SetMoviePreferredVolume should return"
        );
        assert_eq!(cpu.read_reg(Register::A7), sp + 6);

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x00F5); // GetMoviePreferredVolume
        bus.write_long(sp, movie);
        bus.write_word(sp + 4, 0xBEEF);
        let result = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(
            result.is_some(),
            "GetMoviePreferredVolume should be handled"
        );
        assert!(
            result.unwrap().is_ok(),
            "GetMoviePreferredVolume should return"
        );
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
        assert_eq!(bus.read_word(sp + 4), 0x0080);

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x0006); // PrerollMovie
        bus.write_long(sp, 0x0001_0000); // rate
        bus.write_long(sp + 4, 0); // time
        bus.write_long(sp + 8, movie);
        bus.write_word(sp + 12, 0xBEEF);
        let result = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(result.is_some(), "PrerollMovie should be handled");
        assert!(result.unwrap().is_ok(), "PrerollMovie should return");
        assert_eq!(cpu.read_reg(Register::A7), sp + 12);
        assert_eq!(bus.read_word(sp + 12), 0);

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x0007); // LoadMovieIntoRam
        bus.write_long(sp, 0); // flags
        bus.write_long(sp + 4, 1); // duration
        bus.write_long(sp + 8, 0); // time
        bus.write_long(sp + 12, movie);
        bus.write_word(sp + 16, 0xBEEF);
        let result = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(result.is_some(), "LoadMovieIntoRam should be handled");
        assert!(result.unwrap().is_ok(), "LoadMovieIntoRam should return");
        assert_eq!(cpu.read_reg(Register::A7), sp + 16);
        assert_eq!(bus.read_word(sp + 16), 0);
    }

    #[test]
    fn movietoolboxdispatch_dispose_movie_pops_and_releases_movie_state() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let movie = create_test_movie_from_file(&mut disp, &mut cpu, &mut bus, sp);
        assert!(disp.movie_states.contains_key(&movie));

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x0023); // DisposeMovie
        bus.write_long(sp, movie);
        let result = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(result.is_some(), "DisposeMovie should be handled");
        assert!(result.unwrap().is_ok(), "DisposeMovie should return");
        assert_eq!(cpu.read_reg(Register::A7), sp + 4);
        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert!(!disp.movie_states.contains_key(&movie));
    }

    #[test]
    fn movietoolboxdispatch_close_movie_file_closes_data_fork_and_writes_noerr() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        let spec_ptr = 0x300800;
        let ref_num_ptr = 0x300900;
        let movies_dir = disp.ensure_vfs_directory("AmoebArena/Movies");
        disp.vfs.insert(
            "AmoebArena/Movies/C&G".to_string(),
            vec![0x6D, 0x6F, 0x6F, 0x76],
        );
        disp.ensure_vfs_catalog();
        write_test_fsspec(&mut bus, spec_ptr, -1, movies_dir, b"C&G");

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x0192); // OpenMovieFile
        bus.write_word(sp, 0x0100);
        bus.write_long(sp + 2, ref_num_ptr);
        bus.write_long(sp + 6, spec_ptr);
        bus.write_word(sp + 10, 0xBEEF);
        let open = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(open.is_some(), "OpenMovieFile should be handled");
        assert!(open.unwrap().is_ok(), "OpenMovieFile should return");
        let refnum = bus.read_word(ref_num_ptr);
        assert!(disp.open_files.contains_key(&refnum));
        assert_eq!(
            disp.resource_file_name(refnum).map(str::to_owned),
            Some("AmoebArena/Movies/C&G".to_string())
        );
        let data_ptr = bus.alloc(8);
        bus.write_bytes(data_ptr, &[0xC0; 8]);
        let handle = bus.alloc(4);
        bus.write_long(handle, data_ptr);
        disp.resources
            .as_mut()
            .unwrap()
            .files
            .get_mut(&refnum)
            .unwrap()
            .loaded
            .insert((*b"PICT", 23002), data_ptr);
        disp.loaded_handles
            .insert(handle, (data_ptr, *b"PICT", 23002));
        disp.resource_handle_files.insert(handle, refnum);
        disp.ptr_to_handle.insert(data_ptr, handle);

        cpu.write_reg(Register::A7, sp);
        cpu.write_reg(Register::D0, 0x00D5); // CloseMovieFile
        bus.write_word(sp, refnum);
        bus.write_word(sp + 2, 0xBEEF);
        let close = disp.dispatch_toolbox(true, 0x2AA, &mut cpu, &mut bus);
        assert!(close.is_some(), "CloseMovieFile should be handled");
        assert!(close.unwrap().is_ok(), "CloseMovieFile should return");
        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
        assert_eq!(bus.read_word(sp + 2), 0);
        assert_eq!(cpu.read_reg(Register::D0), 0);
        assert!(!disp.open_files.contains_key(&refnum));
        assert!(disp.resource_file_name(refnum).is_none());
        assert_eq!(bus.get_alloc_size(data_ptr), None);
        assert_eq!(bus.get_alloc_size(handle), None);
        assert!(!disp.loaded_handles.contains_key(&handle));
        assert!(!disp.resource_handle_files.contains_key(&handle));
        assert_eq!(disp.ptr_to_handle.get(&data_ptr), None);
    }

    // Unhandled trap returns None
    #[test]
    fn test_unhandled_trap_returns_none() {
        let (mut disp, mut cpu, mut bus) = setup();

        let result = disp.dispatch_toolbox(true, 0xFFF, &mut cpu, &mut bus);
        assert!(result.is_none(), "Unhandled trap should return None");
    }

    // FixATan2 ($A818)
    // FUNCTION FixATan2(x, y: LongInt): Fixed;
    // Inside Macintosh Volume IV (1986), p. IV-65.
    //
    // Witnesses the IM:IV IV-65 documented value bit-exactly against the
    // Systemless HLE: FixATan2(X2Fix(1.0), X2Fix(1.0)) = 0x0000C910. Pascal
    // LTR push: x first (lands at SP+4), y last (lands at SP+0). Trap pops
    // 8 arg bytes and writes the 4-byte Fixed result into the slot at
    // former SP+8.
    #[test]
    fn fixatan2_returns_im_documented_pi_over_four_for_one_one() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        // Pascal LTR push: x first, y last → y at SP+0, x at SP+4.
        bus.write_long(sp, 0x0001_0000); // y = X2Fix(1.0)
        bus.write_long(sp + 4, 0x0001_0000); // x = X2Fix(1.0)
        bus.write_long(sp + 8, 0xDEAD_BEEF); // result-slot poison

        let result = disp.dispatch_toolbox(true, 0x018, &mut cpu, &mut bus);
        assert!(result.is_some());
        assert!(result.unwrap().is_ok());

        // IM:IV IV-65: FixATan2(X2Fix(1.00), X2Fix(1.00)) = $0000C910
        assert_eq!(bus.read_long(sp + 8), 0x0000_C910);
        // 8 arg bytes consumed.
        assert_eq!(cpu.read_reg(Register::A7), sp + 8);
    }

    // FixATan2 ($A818): scale invariance — only y/x ratio matters.
    // Per IM:IV IV-65 "arctan(type/type) -> Fixed" note.
    #[test]
    fn fixatan2_only_ratio_matters_scale_invariance() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;

        // First call: raw LONGINT 1:1 ratio (y=2, x=2).
        bus.write_long(sp, 2);
        bus.write_long(sp + 4, 2);
        let _ = disp
            .dispatch_toolbox(true, 0x018, &mut cpu, &mut bus)
            .unwrap();
        let raw_result = bus.read_long(sp + 8);

        // Second call: Fixed 1:1 ratio (y=X2Fix(1), x=X2Fix(1)).
        cpu.write_reg(Register::A7, sp);
        bus.write_long(sp, 0x0001_0000);
        bus.write_long(sp + 4, 0x0001_0000);
        let _ = disp
            .dispatch_toolbox(true, 0x018, &mut cpu, &mut bus)
            .unwrap();
        let fixed_result = bus.read_long(sp + 8);

        // Same ratio → same Fixed result; both equal IM:IV IV-65 documented value.
        assert_eq!(raw_result, fixed_result);
        assert_eq!(raw_result, 0x0000_C910);
    }

    // SysError ($A9C9) must halt the runner so the halt PC reports the
    // originating SysError call site instead of letting the game execute
    // past it into invalid territory.
    // PROCEDURE SysError(errorCode: INTEGER);
    // Inside Macintosh Volume II (1985), pp. II-358 to II-359;
    // Inside Macintosh: Operating System Utilities (1994), pp. 2-13 to 2-14.
    #[test]
    fn test_syserror_writes_ds_err_code_and_halts_runner() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        // Push errorCode (INTEGER, 16-bit) at SP.
        bus.write_word(sp, 0x002A); // dsCoreErr-style code; value irrelevant
        bus.write_word(crate::memory::globals::addr::DS_ERR_CODE, 0xBEEF);

        let result = disp.dispatch_toolbox(true, 0x1C9, &mut cpu, &mut bus);
        // Some(Err(Halted)) — handler matched AND signalled halt.
        let inner = result.expect("SysError must be a handled trap");
        assert!(
            matches!(inner, Err(crate::Error::Halted)),
            "SysError must return Err(Halted), got {:?}",
            inner
        );
        // Stack-discipline: errorCode (2 bytes) consumed, A7 advanced.
        assert_eq!(cpu.read_reg(Register::A7), sp + 2);
        assert_eq!(
            bus.read_word(crate::memory::globals::addr::DS_ERR_CODE),
            0x002A
        );
    }

    #[test]
    fn initresources_returns_minus_one_for_nominal_call() {
        let (mut disp, mut cpu, mut bus) = setup();

        cpu.write_reg(Register::A7, TEST_SP);
        let init = disp.dispatch_toolbox(true, 0x195, &mut cpu, &mut bus);
        assert!(init.is_some(), "InitResources should be handled");
        assert!(
            init.unwrap().is_ok(),
            "InitResources should return normally"
        );
        assert_eq!(bus.read_word(TEST_SP) as i16, -1);
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
    }

    #[test]
    fn rsrczoneinit_preserves_stack_pointer() {
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);

        let result = disp.dispatch_toolbox(true, 0x196, &mut cpu, &mut bus);
        assert!(result.is_some(), "RsrcZoneInit should be handled");
        assert!(
            result.unwrap().is_ok(),
            "RsrcZoneInit should return normally"
        );
        assert_eq!(cpu.read_reg(Register::A7), sp_before);
    }
}
