//! Dialog Manager, Cursor Manager, and misc stub trap handlers.

use super::dispatch::{
    DialogItem, DialogPopupDraw, DialogPopupTrackingState, DialogTrackingState,
    DialogUserItemTrackingState, PendingDialogPopupMenu, PersistentDialogSnapshot, QueuedEvent,
    RetainedModalDialogClickState,
};
use super::types::{decode_mac_roman_for_render, Rect, ShapeOp};
use crate::cpu::{CpuOps, Register};
use crate::display::CursorImage;
use crate::memory::{MacMemoryBus, MemoryBus};
use crate::quickdraw::fonts::get_font_face_scaled;
use crate::quickdraw::text::get_font_metrics;
use crate::ui_theme::{ControlKind, UiThemeId};
use crate::Result;
use std::collections::VecDeque;
use std::sync::OnceLock;

static TRACE_DIALOG_PROCS: OnceLock<bool> = OnceLock::new();
static TRACE_DIALOG_FILTER: OnceLock<bool> = OnceLock::new();
static TRACE_TEXTEDIT: OnceLock<bool> = OnceLock::new();
static TRACE_DIALOG_ITEMS: OnceLock<bool> = OnceLock::new();
static TRACE_DIALOG_TEXT_INLINE: OnceLock<bool> = OnceLock::new();
const MANAGER_DIALOG_RECORD_ALIGNMENT: u32 = 256;

struct DialogCIconLayout {
    width: i16,
    height: i16,
    pm_row_bytes: u32,
    mask_row_bytes: u32,
    bmap_row_bytes: u32,
    pixel_size: u16,
    mask_data_ptr: u32,
    bmap_data_ptr: u32,
    pixel_data_ptr: u32,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct TeResolvedStyle {
    font: i16,
    face: i16,
    size: i16,
    color: (u16, u16, u16),
    line_height: i16,
    ascent: i16,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct TeStyleRun {
    start: usize,
    style_index: usize,
    style: TeResolvedStyle,
}

fn trace_dialog_procs_enabled() -> bool {
    *TRACE_DIALOG_PROCS.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_DIALOG_PROCS").is_some())
}

fn trace_dialog_filter_enabled() -> bool {
    *TRACE_DIALOG_FILTER
        .get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_DIALOG_FILTER").is_some())
}

fn trace_textedit_enabled() -> bool {
    *TRACE_TEXTEDIT.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_TEXTEDIT").is_some())
}

fn trace_dialog_items_enabled() -> bool {
    *TRACE_DIALOG_ITEMS.get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_DIALOG_ITEMS").is_some())
}

fn trace_dialog_text_inline_enabled() -> bool {
    *TRACE_DIALOG_TEXT_INLINE
        .get_or_init(|| std::env::var_os("SYSTEMLESS_TRACE_DIALOG_TEXT").is_some())
}

fn input_trace_event_name(what: u16) -> &'static str {
    match what {
        0 => "nullEvent",
        1 => "mouseDown",
        2 => "mouseUp",
        3 => "keyDown",
        4 => "keyUp",
        5 => "autoKey",
        6 => "updateEvt",
        7 => "diskEvt",
        8 => "activateEvt",
        _ => "unknownEvent",
    }
}

fn input_trace_nonzero(value: u32) -> String {
    if value == 0 {
        "$00000000".to_string()
    } else {
        "$NONZERO".to_string()
    }
}

fn input_trace_event_message(what: u16, message: u32) -> String {
    match what {
        6 | 8 => input_trace_nonzero(message),
        _ => format!("${message:08X}"),
    }
}

fn input_trace_text_bytes(value: &str) -> String {
    if value.is_empty() {
        return "empty".to_string();
    }
    let mut out = String::from("hex:");
    for byte in value.as_bytes() {
        out.push_str(&format!("{byte:02X}"));
    }
    out
}

impl super::TrapDispatcher {
    fn record_dialog_input_trace(
        &mut self,
        trap: &str,
        event_ptr: u32,
        what: u16,
        message: u32,
        where_v: i16,
        where_h: i16,
        modifiers: u16,
        active_dialog: Option<u32>,
        result: bool,
        detail: &str,
    ) {
        if !self.input_trace_enabled {
            return;
        }
        let active_dialog = active_dialog
            .map(input_trace_nonzero)
            .unwrap_or_else(|| "none".to_string());
        let mut line = format!(
            "{trap} action=guest_event:{} event_ptr={} delivered=EventRecord{{what={}({}), message={}, where=({where_v},{where_h}), modifiers=${modifiers:04X}}} {} active_dialog={} result={}",
            input_trace_event_name(what),
            input_trace_nonzero(event_ptr),
            input_trace_event_name(what),
            what,
            input_trace_event_message(what, message),
            self.input_trace_state_fields(),
            active_dialog,
            if result { "true" } else { "false" },
        );
        if !detail.is_empty() {
            line.push(' ');
            line.push_str(detail);
        }
        self.record_input_trace_line(line);
    }

    fn record_modal_dialog_input_trace(
        &mut self,
        action: &str,
        dialog_ptr: u32,
        bounds: (i16, i16, i16, i16),
        item_hit: i16,
        item_type: Option<u8>,
        highlighted: Option<bool>,
        result: &str,
        outcome: &str,
    ) {
        if !self.input_trace_enabled {
            return;
        }
        let item_type = item_type
            .map(|value| format!("${value:02X}"))
            .unwrap_or_else(|| "none".to_string());
        let highlighted = highlighted
            .map(|value| if value { "true" } else { "false" }.to_string())
            .unwrap_or_else(|| "none".to_string());
        self.record_input_trace_line(format!(
            "A991 action={} live_mouse=({},{}) {} dialog={} bounds=({},{},{},{}) item_hit={} item_type={} highlighted={} result={} outcome={}",
            action,
            self.mouse_pos.0,
            self.mouse_pos.1,
            self.input_trace_state_fields(),
            input_trace_nonzero(dialog_ptr),
            bounds.0,
            bounds.1,
            bounds.2,
            bounds.3,
            item_hit,
            item_type,
            highlighted,
            result,
            outcome,
        ));
    }

    fn record_modal_dialog_text_input_trace(
        &mut self,
        action: &str,
        dialog_ptr: u32,
        bounds: (i16, i16, i16, i16),
        edit_item: i16,
        item_type: Option<u8>,
        key_code: u8,
        char_code: u8,
        text_before: &str,
        text_after: &str,
        result: &str,
        outcome: &str,
    ) {
        if !self.input_trace_enabled {
            return;
        }
        let item_type = item_type
            .map(|value| format!("${value:02X}"))
            .unwrap_or_else(|| "none".to_string());
        self.record_input_trace_line(format!(
            "A991 action={} live_mouse=({},{}) {} dialog={} bounds=({},{},{},{}) edit_item={} item_type={} key_code=${:02X} char_code=${:02X} text_before={} text_after={} result={} outcome={}",
            action,
            self.mouse_pos.0,
            self.mouse_pos.1,
            self.input_trace_state_fields(),
            input_trace_nonzero(dialog_ptr),
            bounds.0,
            bounds.1,
            bounds.2,
            bounds.3,
            edit_item,
            item_type,
            key_code,
            char_code,
            input_trace_text_bytes(text_before),
            input_trace_text_bytes(text_after),
            result,
            outcome,
        ));
    }

    fn record_modal_dialog_filter_input_trace(
        &mut self,
        action: &str,
        dialog_ptr: u32,
        bounds: (i16, i16, i16, i16),
        filter_proc: u32,
        event: Option<&QueuedEvent>,
        item_hit: i16,
        item_type: Option<u8>,
        handled_mouse_down: bool,
        dialog_retained: bool,
        result: &str,
        outcome: &str,
    ) {
        if !self.input_trace_enabled {
            return;
        }
        let item_type = item_type
            .map(|value| format!("${value:02X}"))
            .unwrap_or_else(|| "none".to_string());
        let (event_name, message, where_text, modifiers) = if let Some(event) = event {
            (
                format!("{}({})", input_trace_event_name(event.what), event.what),
                input_trace_event_message(event.what, event.message),
                format!("({},{})", event.where_v, event.where_h),
                format!("${:04X}", event.modifiers),
            )
        } else {
            (
                "none".to_string(),
                "none".to_string(),
                "none".to_string(),
                "none".to_string(),
            )
        };
        self.record_input_trace_line(format!(
            "A991 action={} live_mouse=({},{}) {} dialog={} bounds=({},{},{},{}) filter_proc={} event={} message={} where={} modifiers={} item_hit={} item_type={} handled_mouse_down={} dialog_retained={} result={} outcome={}",
            action,
            self.mouse_pos.0,
            self.mouse_pos.1,
            self.input_trace_state_fields(),
            input_trace_nonzero(dialog_ptr),
            bounds.0,
            bounds.1,
            bounds.2,
            bounds.3,
            input_trace_nonzero(filter_proc),
            event_name,
            message,
            where_text,
            modifiers,
            item_hit,
            item_type,
            handled_mouse_down,
            dialog_retained,
            result,
            outcome,
        ));
    }

    fn dialog_template_res_err(&self, dialog_id: i16) -> i16 {
        if self.find_resource_any(*b"DLOG", dialog_id).is_some() {
            0
        } else {
            0
        }
    }

    fn loaded_resource_handle_in_file(
        &self,
        res_type: [u8; 4],
        res_id: i16,
        ptr: u32,
        refnum: u16,
    ) -> Option<u32> {
        self.loaded_handles
            .iter()
            .find_map(|(&handle, &(loaded_ptr, loaded_type, loaded_id))| {
                if loaded_ptr == ptr
                    && loaded_type == res_type
                    && loaded_id == res_id
                    && self.resource_handle_files.get(&handle).copied() == Some(refnum)
                {
                    Some(handle)
                } else {
                    None
                }
            })
    }

    fn set_handle_purgeable(&mut self, handle: u32, purgeable: bool) {
        if handle == 0 {
            return;
        }

        if purgeable {
            *self.handle_state_bits.entry(handle).or_insert(0) |= 0x40;
        } else if let Some(bits) = self.handle_state_bits.get_mut(&handle) {
            *bits &= !0x40;
            if *bits == 0 {
                self.handle_state_bits.remove(&handle);
            }
        }
    }

    fn prepare_dialog_resource_handle(
        &mut self,
        bus: &mut MacMemoryBus,
        res_type: [u8; 4],
        res_id: i16,
        purgeable: bool,
        load_if_missing: bool,
    ) -> Option<(u32, u32)> {
        let (refnum, ptr) = self.find_resource_any(res_type, res_id)?;
        let handle = if load_if_missing {
            let handle =
                self.get_or_create_resource_handle_in_file(bus, res_type, res_id, ptr, refnum);
            if ptr != 0 && bus.read_long(handle) == 0 {
                bus.write_long(handle, ptr);
                self.ptr_to_handle.insert(ptr, handle);
            }
            handle
        } else {
            self.loaded_resource_handle_in_file(res_type, res_id, ptr, refnum)?
        };

        self.set_handle_purgeable(handle, purgeable);
        Some((handle, ptr))
    }

    fn dialog_item_resource_type(item_type: u8) -> Option<[u8; 4]> {
        match item_type & 0x7F {
            7 => Some(*b"CNTL"),
            32 => Some(*b"ICON"),
            64 => Some(*b"PICT"),
            _ => None,
        }
    }

    fn cascade_dialog_resource_purgeability(
        &mut self,
        bus: &mut MacMemoryBus,
        template_type: [u8; 4],
        template_id: i16,
        purgeable: bool,
        load_if_missing: bool,
    ) {
        let Some((_, template_ptr)) = self.prepare_dialog_resource_handle(
            bus,
            template_type,
            template_id,
            purgeable,
            load_if_missing,
        ) else {
            return;
        };

        let items_id = match template_type {
            // IM:I I-437: DLOG item-list ID is at offset 18.
            [b'D', b'L', b'O', b'G'] if bus.get_alloc_size(template_ptr).unwrap_or(0) >= 20 => {
                bus.read_word(template_ptr + 18) as i16
            }
            // IM:I I-425..I-426: ALRT item-list ID is at offset 8.
            [b'A', b'L', b'R', b'T'] if bus.get_alloc_size(template_ptr).unwrap_or(0) >= 10 => {
                bus.read_word(template_ptr + 8) as i16
            }
            _ => return,
        };

        let Some((_, ditl_ptr)) = self.prepare_dialog_resource_handle(
            bus,
            *b"DITL",
            items_id,
            purgeable,
            load_if_missing,
        ) else {
            return;
        };

        let ditl_len = bus.get_alloc_size(ditl_ptr).unwrap_or(0);
        let items = Self::parse_ditl(bus, ditl_ptr, ditl_len);
        for item in items {
            if let Some(res_type) = Self::dialog_item_resource_type(item.item_type) {
                self.prepare_dialog_resource_handle(
                    bus,
                    res_type,
                    item.resource_id,
                    purgeable,
                    load_if_missing,
                );
            }
        }
    }

    const TE_DEST_RECT_OFFSET: u32 = 0x00;
    const TE_VIEW_RECT_OFFSET: u32 = 0x08;
    const TE_SEL_RECT_OFFSET: u32 = 0x10;
    const TE_LINE_HEIGHT_OFFSET: u32 = 0x18;
    const TE_FONT_ASCENT_OFFSET: u32 = 0x1A;
    const TE_SEL_POINT_OFFSET: u32 = 0x1C;
    const TE_SEL_START_OFFSET: u32 = 0x20;
    const TE_SEL_END_OFFSET: u32 = 0x22;
    const TE_ACTIVE_OFFSET: u32 = 0x24;
    const TE_CARET_TIME_OFFSET: u32 = 0x34;
    const TE_CARET_STATE_OFFSET: u32 = 0x38;
    #[allow(dead_code)]
    const TE_CR_ONLY_OFFSET: u32 = 0x48;
    const TE_JUST_OFFSET: u32 = 0x3A;
    const TE_LENGTH_OFFSET: u32 = 0x3C;
    const TE_HTEXT_OFFSET: u32 = 0x3E;
    const TE_TX_FONT_OFFSET: u32 = 0x4A;
    const TE_TX_FACE_OFFSET: u32 = 0x4C;
    const TE_TX_MODE_OFFSET: u32 = 0x4E;
    const TE_TX_SIZE_OFFSET: u32 = 0x50;
    const TE_IN_PORT_OFFSET: u32 = 0x52;
    const TE_N_LINES_OFFSET: u32 = 0x5E;
    const TE_LINE_STARTS_OFFSET: u32 = 0x60;
    const TE_REC_MIN_SIZE: u32 = 128;
    const TE_CARET_BLINK_TICKS: u32 = 32;
    // TextEdit draws inside the destination rectangle (IM:I I-373 to I-374);
    // BasiliskII/System 7.5.3 `dialog_visual_textedit_smoke` pins the ROM's
    // flush-left glyph origin one pixel in from destRect.left.
    const TE_LINE_LEFT_INSET: i16 = 1;

    const DBOX_FRAME_MARGIN: i16 = 8;
    const EDIT_TEXT_FRAME_OUTSET: i16 = 3;
    const STANDARD_CONTROL_MARK_SIZE: i16 = 12;
    const STANDARD_CONTROL_MARK_LEFT_INSET: i16 = 2;
    const STANDARD_CONTROL_TITLE_GAP: i16 = 4;
    const INACTIVE_STANDARD_CONTROL_TITLE_RGB: [u16; 3] = [0xA1A1, 0xA1A1, 0xA1A1];
    const CLASSIC_RADIO_OUTLINE_12: [&'static str; 12] = [
        "....####....",
        "..##....##..",
        ".#........#.",
        ".#........#.",
        "#..........#",
        "#..........#",
        "#..........#",
        "#..........#",
        ".#........#.",
        ".#........#.",
        "..##....##..",
        "....####....",
    ];
    const CLASSIC_RADIO_DOT_12: [&'static str; 12] = [
        "............",
        "............",
        "............",
        "....####....",
        "...######...",
        "...######...",
        "...######...",
        "...######...",
        "....####....",
        "............",
        "............",
        "............",
    ];

    const TE_STYLE_N_RUNS_OFFSET: u32 = 0x00;
    const TE_STYLE_N_STYLES_OFFSET: u32 = 0x02;
    const TE_STYLE_STYLE_TABLE_OFFSET: u32 = 0x04;
    const TE_STYLE_LH_TABLE_OFFSET: u32 = 0x08;
    const TE_STYLE_NULL_STYLE_OFFSET: u32 = 0x10;
    const TE_STYLE_RUNS_OFFSET: u32 = 0x14;

    const ST_ELEMENT_REFCOUNT_OFFSET: u32 = 0x00;
    const ST_ELEMENT_HEIGHT_OFFSET: u32 = 0x02;
    const ST_ELEMENT_ASCENT_OFFSET: u32 = 0x04;
    const ST_ELEMENT_FONT_OFFSET: u32 = 0x06;
    const ST_ELEMENT_FACE_OFFSET: u32 = 0x08;
    const ST_ELEMENT_SIZE_OFFSET: u32 = 0x0A;
    const ST_ELEMENT_COLOR_OFFSET: u32 = 0x0C;
    const ST_ELEMENT_SIZE: u32 = 0x12;

    const LH_ELEMENT_HEIGHT_OFFSET: u32 = 0x00;
    const LH_ELEMENT_ASCENT_OFFSET: u32 = 0x02;
    const LH_ELEMENT_SIZE: u32 = 0x04;

    const NULL_STYLE_SCRAP_OFFSET: u32 = 0x04;
    const NULL_STYLE_REC_SIZE: u32 = 0x08;

    const SCRAP_N_STYLES_OFFSET: u32 = 0x00;
    const SCRAP_STYLE_TAB_OFFSET: u32 = 0x0C;
    const SCRAP_STYLE_START_CHAR_OFFSET: u32 = 0x00;
    const SCRAP_STYLE_HEIGHT_OFFSET: u32 = 0x04;
    const SCRAP_STYLE_ASCENT_OFFSET: u32 = 0x06;
    const SCRAP_STYLE_FONT_OFFSET: u32 = 0x08;
    const SCRAP_STYLE_FACE_OFFSET: u32 = 0x0A;
    const SCRAP_STYLE_SIZE_OFFSET: u32 = 0x0C;
    const SCRAP_STYLE_COLOR_OFFSET: u32 = 0x0E;
    const SCRAP_STYLE_ELEMENT_SIZE: u32 = 0x14;
    const STYLE_SCRAP_REC_SIZE: u32 = 0x20;

    const TE_FEATURE_AUTO_SCROLL: u16 = 0;
    const TE_FEATURE_TEXT_BUFFERING: u16 = 1;
    const TE_FEATURE_OUTLINE_HILITE: u16 = 2;
    const TE_FEATURE_INLINE_INPUT: u16 = 3;
    const TE_FEATURE_USE_TEXT_SERVICES: u16 = 4;

    const TE_BIT_CLEAR: i16 = 0;
    const TE_BIT_SET: i16 = 1;
    const TE_BIT_TEST: i16 = -1;

    fn dialog_window_proc_id(&self, bus: &MacMemoryBus, dialog_ptr: u32) -> i16 {
        self.window_proc_ids
            .get(&dialog_ptr)
            .copied()
            // Older tests seed pre-side-table dialogs by writing the WDEF
            // procID at +108. Real DialogRecords use +108 as windowKind.
            .unwrap_or_else(|| bus.read_word(dialog_ptr + 108) as i16)
    }

    fn dialog_window_title(bus: &MacMemoryBus, dialog_ptr: u32) -> String {
        if dialog_ptr == 0 {
            return String::new();
        }
        // WindowRecord.titleHandle is a StringHandle at +134; GetWTitle uses
        // the same documented field (IM:I I-284).
        let title_handle = bus.read_long(dialog_ptr + 134);
        if title_handle == 0 {
            return String::new();
        }
        let title_ptr = bus.read_long(title_handle);
        if title_ptr == 0 {
            return String::new();
        }
        decode_mac_roman_for_render(&bus.read_pstring(title_ptr))
    }

    fn ensure_text_handle_size(bus: &mut MacMemoryBus, item_handle: u32, size: usize) -> u32 {
        if item_handle == 0 {
            return 0;
        }

        let current_ptr = bus.read_long(item_handle);
        if size == 0 {
            if current_ptr != 0 {
                bus.free(current_ptr);
                bus.write_long(item_handle, 0);
            }
            return 0;
        }

        let required = size as u32;
        let current_size = if current_ptr != 0 {
            bus.get_alloc_size(current_ptr).unwrap_or(0)
        } else {
            0
        };

        if current_ptr != 0 && current_size == required {
            return current_ptr;
        }

        let new_ptr = bus.alloc(required);
        if new_ptr == 0 {
            return current_ptr;
        }

        if current_ptr != 0 {
            bus.free(current_ptr);
        }
        bus.write_long(item_handle, new_ptr);
        new_ptr
    }

    fn text_item_string_from_handle(bus: &MacMemoryBus, item_handle: u32) -> String {
        Self::text_item_string_from_handle_if_present(bus, item_handle).unwrap_or_default()
    }

    fn text_item_string_from_handle_if_present(
        bus: &MacMemoryBus,
        item_handle: u32,
    ) -> Option<String> {
        Self::text_item_bytes_from_handle_if_present(bus, item_handle)
            .map(|bytes| decode_mac_roman_for_render(&bytes))
    }

    fn text_item_bytes_from_handle_if_present(
        bus: &MacMemoryBus,
        item_handle: u32,
    ) -> Option<Vec<u8>> {
        if item_handle == 0 {
            return None;
        }

        let data_ptr = bus.read_long(item_handle);
        if data_ptr == 0 {
            return Some(Vec::new());
        }

        let len = bus.get_alloc_size(data_ptr).unwrap_or(0) as usize;
        Some(bus.read_bytes(data_ptr, len))
    }

    fn dialog_item_handle_addr(bus: &MacMemoryBus, dialog_ptr: u32, item_no: i16) -> Option<u32> {
        if item_no <= 0 {
            return None;
        }

        let items_handle = bus.read_long(dialog_ptr + 156);
        if items_handle == 0 {
            return None;
        }
        let ditl_ptr = bus.read_long(items_handle);
        if ditl_ptr == 0 {
            return None;
        }

        let max_index = bus.read_word(ditl_ptr) as i16;
        if item_no > max_index + 1 {
            return None;
        }

        let ditl_len = bus.get_alloc_size(ditl_ptr).unwrap_or(u32::MAX);
        let mut offset = 2u32;
        for current_item in 1..=max_index + 1 {
            let item_handle_addr = ditl_ptr + offset;
            offset += 4; // itmhand
            offset += 8; // itmr
            let item_type = bus.read_byte(ditl_ptr + offset);
            let data_len_byte = bus.read_byte(ditl_ptr + offset + 1);
            offset += 2; // itmtype + itmlen
            let base_type = item_type & 0x7F;
            let remaining = ditl_len.saturating_sub(offset);
            let payload_len = Self::ditl_item_payload_len(base_type, data_len_byte, remaining)?;
            let padded = (payload_len + 1) & !1;
            if current_item == item_no {
                return Some(item_handle_addr);
            }
            offset += padded;
        }

        None
    }

    fn dialog_item_handle(bus: &MacMemoryBus, dialog_ptr: u32, item_no: i16) -> u32 {
        Self::dialog_item_handle_addr(bus, dialog_ptr, item_no)
            .map(|addr| bus.read_long(addr))
            .unwrap_or(0)
    }

    fn set_dialog_item_handle(bus: &mut MacMemoryBus, dialog_ptr: u32, item_no: i16, handle: u32) {
        if let Some(addr) = Self::dialog_item_handle_addr(bus, dialog_ptr, item_no) {
            bus.write_long(addr, handle);
        }
    }

    pub(crate) fn front_dialog_for_user_item_proc(&self, proc_ptr: u32) -> Option<u32> {
        if proc_ptr == 0 {
            return None;
        }
        let front = self.front_window;
        if front == 0 {
            return None;
        }
        let Some(items) = self.dialog_items.get(&front) else {
            return None;
        };
        if items
            .iter()
            .any(|item| (item.item_type & 0x7F) == 0 && item.proc_ptr == proc_ptr)
        {
            Some(front)
        } else {
            None
        }
    }

    pub(crate) fn front_dialog_has_user_item_proc(&self, proc_ptr: u32) -> bool {
        self.front_dialog_for_user_item_proc(proc_ptr).is_some()
    }

    pub(crate) fn redraw_dialog_window_contents(
        &mut self,
        bus: &mut MacMemoryBus,
        dialog_ptr: u32,
    ) {
        if let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() {
            Self::refresh_ditl_proc_ptrs(bus, dialog_ptr, &mut items);
            let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
            let proc_id = self.dialog_window_proc_id(bus, dialog_ptr);
            let (edit_text, edit_item, default_item) =
                Self::dialog_edit_state(bus, dialog_ptr, &items);
            self.draw_dialog(
                bus,
                bounds,
                proc_id,
                "",
                &items,
                default_item,
                &edit_text,
                edit_item,
                false,
                dialog_ptr,
            );
            if Self::dialog_is_game_managed(bounds, &items) {
                self.dialog_visible_snapshots.remove(&dialog_ptr);
            } else {
                let pixels = self.save_dialog_pixels(bus, bounds);
                self.dialog_visible_snapshots
                    .insert(dialog_ptr, PersistentDialogSnapshot { bounds, pixels });
            }
            self.dialog_items.insert(dialog_ptr, items);
        }
    }

    pub(crate) fn queue_modeless_dialog_draw_procs(
        &mut self,
        bus: &mut MacMemoryBus,
        dialog_ptr: u32,
    ) {
        self.queue_modeless_dialog_draw_procs_intersecting(bus, dialog_ptr, None);
    }

    fn update_dialog_window_contents<C: CpuOps>(
        &mut self,
        bus: &mut MacMemoryBus,
        cpu: &mut C,
        dialog_ptr: u32,
        update_rect: Option<(i16, i16, i16, i16)>,
    ) -> bool {
        let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() else {
            return false;
        };
        Self::refresh_ditl_proc_ptrs(bus, dialog_ptr, &mut items);
        let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
        let update_screen_rect =
            update_rect.and_then(|rect| Self::dialog_update_screen_rect(bounds, rect));
        let proc_id = self.dialog_window_proc_id(bus, dialog_ptr);
        let (edit_text, edit_item, default_item) = Self::dialog_edit_state(bus, dialog_ptr, &items);
        // MTE 1992 p. 6-141: DialogSelect handles update events by calling
        // DrawDialog. MTE 1992 p. 6-143: UpdateDialog uses SetPort before
        // redrawing the update region. Keep both dialog update paths on the
        // dialog port before any standard-item drawing or userItem callbacks.
        self.set_current_port_state(bus, cpu, dialog_ptr, None);
        let Some(update_screen_rect) = update_screen_rect else {
            return false;
        };
        let before_pixels = self.save_dialog_pixels(bus, bounds);
        self.dialog_initial_draw_deferred.remove(&dialog_ptr);
        self.draw_dialog(
            bus,
            bounds,
            proc_id,
            "",
            &items,
            default_item,
            &edit_text,
            edit_item,
            false,
            dialog_ptr,
        );
        // MTE 1992 p. 6-143: UpdateDialog redraws only the supplied update
        // region. Most Dialog Manager HLE drawing helpers write directly to the
        // framebuffer, so clip the full redraw by restoring pre-update pixels
        // outside the update region after standard items have been refreshed.
        self.restore_dialog_pixels_outside_rect(bus, bounds, &before_pixels, update_screen_rect);
        self.dialog_items.insert(dialog_ptr, items);
        // Application-defined userItems are guest-owned. IM:I I-405 says
        // their item handle is a draw procedure; queue only the callbacks
        // whose display rectangles intersect the active update region.
        self.queue_modeless_dialog_draw_procs_intersecting(bus, dialog_ptr, update_rect);
        true
    }

    fn dialog_update_screen_rect(
        bounds: (i16, i16, i16, i16),
        rect: (i16, i16, i16, i16),
    ) -> Option<(i16, i16, i16, i16)> {
        let screen_rect = if Self::rects_intersect(rect, bounds) {
            rect
        } else {
            (
                bounds.0.saturating_add(rect.0),
                bounds.1.saturating_add(rect.1),
                bounds.0.saturating_add(rect.2),
                bounds.1.saturating_add(rect.3),
            )
        };
        Self::rect_intersection(
            screen_rect,
            (
                bounds.0 - Self::DBOX_FRAME_MARGIN,
                bounds.1 - Self::DBOX_FRAME_MARGIN,
                bounds.2 + Self::DBOX_FRAME_MARGIN,
                bounds.3 + Self::DBOX_FRAME_MARGIN,
            ),
        )
    }

    fn queue_modeless_dialog_draw_procs_intersecting(
        &mut self,
        bus: &mut MacMemoryBus,
        dialog_ptr: u32,
        update_rect: Option<(i16, i16, i16, i16)>,
    ) {
        let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() else {
            return;
        };
        Self::refresh_ditl_proc_ptrs(bus, dialog_ptr, &mut items);
        let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
        let effective_update_rect = update_rect.map(|rect| {
            if Self::rects_intersect(rect, bounds) {
                rect
            } else {
                (
                    bounds.0.saturating_add(rect.0),
                    bounds.1.saturating_add(rect.1),
                    bounds.0.saturating_add(rect.2),
                    bounds.1.saturating_add(rect.3),
                )
            }
        });
        for (i, item) in items.iter().enumerate() {
            let item_rect = Self::dialog_item_screen_rect(bounds, item.rect);
            if (item.item_type & 0x7F) == 0
                && item.proc_ptr != 0
                && Self::rects_intersect(item_rect, bounds)
                && effective_update_rect
                    .map(|rect| Self::rects_intersect(item_rect, rect))
                    .unwrap_or(true)
            {
                self.modeless_dialog_draw_proc_queue.push_back((
                    dialog_ptr,
                    item.proc_ptr,
                    (i + 1) as i16,
                ));
            }
        }
        self.dialog_items.insert(dialog_ptr, items);
    }

    fn allocate_te_handle(bus: &mut MacMemoryBus) -> u32 {
        // TENew returns a handle to a zeroed TERec owned by the caller.
        // Text 1993, 2-85 to 2-86
        let te_ptr = bus.alloc(Self::TE_REC_MIN_SIZE);
        if te_ptr == 0 {
            return 0;
        }
        let handle = bus.alloc(4);
        if handle == 0 {
            return 0;
        }
        bus.write_long(handle, te_ptr);
        handle
    }

    fn allocate_handle_with_data(bus: &mut MacMemoryBus, size: u32) -> u32 {
        let handle = bus.alloc(4);
        if handle == 0 {
            return 0;
        }

        let data_ptr = if size == 0 { 0 } else { bus.alloc(size) };
        bus.write_long(handle, data_ptr);
        handle
    }

    fn ensure_handle_capacity(bus: &mut MacMemoryBus, handle: u32, min_size: u32) -> u32 {
        if handle == 0 {
            return 0;
        }

        let current_ptr = bus.read_long(handle);
        let current_size = if current_ptr != 0 {
            bus.get_alloc_size(current_ptr).unwrap_or(0)
        } else {
            0
        };
        if current_size >= min_size {
            return current_ptr;
        }

        let new_ptr = bus.alloc(min_size);
        if new_ptr == 0 {
            return current_ptr;
        }
        if current_ptr != 0 && current_size != 0 {
            let existing = bus.read_bytes(current_ptr, current_size as usize);
            bus.write_bytes(new_ptr, &existing);
            bus.free(current_ptr);
        }
        bus.write_long(handle, new_ptr);
        new_ptr
    }

    fn te_record_size_for_line_count(line_count: usize) -> u32 {
        let line_entries = line_count.saturating_add(4) as u32;
        Self::TE_REC_MIN_SIZE.max(Self::TE_LINE_STARTS_OFFSET + line_entries * 2)
    }

    fn ensure_te_record_line_capacity(
        bus: &mut MacMemoryBus,
        te_handle: u32,
        line_count: usize,
    ) -> u32 {
        Self::ensure_handle_capacity(
            bus,
            te_handle,
            Self::te_record_size_for_line_count(line_count),
        )
    }

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

    fn te_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 te_rect_is_plausible(rect: (i16, i16, i16, i16)) -> bool {
        let (top, left, bottom, right) = rect;
        top <= bottom
            && left <= right
            && top.abs() <= 4096
            && left.abs() <= 4096
            && bottom.abs() <= 4096
            && right.abs() <= 4096
            && (bottom - top).abs() <= 4096
            && (right - left).abs() <= 4096
    }

    fn te_rect_is_empty(rect: (i16, i16, i16, i16)) -> bool {
        rect.0 == 0 && rect.1 == 0 && rect.2 == 0 && rect.3 == 0
    }

    /// Decode TENew/TEStyleNew arguments off the Pascal stack, sniffing
    /// either the modern Universal Headers pointer convention (8 bytes,
    /// two `const Rect *` ptrs) or the legacy Inside Macintosh by-value
    /// convention (16 bytes, two Rects pushed by value).
    ///
    /// Pascal calling convention pushes left-to-right, so the first
    /// arg (destRect / destRect_ptr) lands DEEPER on the stack than the
    /// second arg (viewRect / viewRect_ptr). Concretely:
    ///   * Pointer convention (8 bytes, MPW Universal Headers):
    ///       sp+0..3  viewRect_ptr  (last pushed, shallowest)
    ///       sp+4..7  destRect_ptr  (first pushed, deepest)
    ///   * By-value convention (16 bytes, classic IM:I I-373):
    ///       sp+0..7   viewRect bytes
    ///       sp+8..15  destRect bytes
    ///
    /// Returns (destRect, viewRect, stack_pop).
    #[allow(clippy::type_complexity)] // (destRect, viewRect, stack_pop) — local return shape, no aliasing benefit
    fn te_new_rect_args(
        bus: &MacMemoryBus,
        sp: u32,
    ) -> ((i16, i16, i16, i16), (i16, i16, i16, i16), u32) {
        // Pascal first arg (destRect / destRect_ptr) is DEEPEST → sp+4.
        // Pascal second arg (viewRect / viewRect_ptr) is SHALLOWEST → sp+0.
        let view_ptr_word = bus.read_long(sp);
        let dest_ptr_word = bus.read_long(sp + 4);
        let view_via_ptr = if view_ptr_word != 0 {
            Some(Self::te_read_rect(bus, view_ptr_word))
        } else {
            None
        };
        let dest_via_ptr = if dest_ptr_word != 0 {
            Some(Self::te_read_rect(bus, dest_ptr_word))
        } else {
            None
        };
        let view_is_rect_ptr = view_via_ptr.is_some_and(Self::te_rect_is_plausible);
        let dest_is_rect_ptr = dest_via_ptr.is_some_and(Self::te_rect_is_plausible);
        if view_is_rect_ptr && dest_is_rect_ptr {
            // Modern MPW Universal Headers pointer convention (8 bytes).
            let dest = dest_via_ptr.unwrap();
            let view = view_via_ptr.unwrap();
            if Self::te_rect_is_empty(view) && !Self::te_rect_is_empty(dest) {
                (dest, dest, 8)
            } else if Self::te_rect_is_empty(dest) && !Self::te_rect_is_empty(view) {
                (view, view, 8)
            } else {
                (dest, view, 8)
            }
        } else {
            // Legacy by-value convention (16 bytes).
            (
                Self::te_read_rect(bus, sp + 8),
                Self::te_read_rect(bus, sp),
                16,
            )
        }
    }

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

    fn te_is_styled_record(bus: &MacMemoryBus, te_ptr: u32) -> bool {
        te_ptr != 0 && bus.read_word(te_ptr + Self::TE_TX_SIZE_OFFSET) == 0xFFFF
    }

    fn te_text_handle(bus: &MacMemoryBus, te_handle: u32) -> u32 {
        let te_ptr = Self::te_record_ptr(bus, te_handle);
        if te_ptr == 0 {
            0
        } else {
            bus.read_long(te_ptr + Self::TE_HTEXT_OFFSET)
        }
    }

    fn te_style_handle(bus: &MacMemoryBus, te_handle: u32) -> u32 {
        let te_ptr = Self::te_record_ptr(bus, te_handle);
        if Self::te_is_styled_record(bus, te_ptr) {
            bus.read_long(te_ptr + Self::TE_TX_FONT_OFFSET)
        } else {
            0
        }
    }

    fn te_write_style_handle(bus: &mut MacMemoryBus, te_handle: u32, style_handle: u32) {
        let te_ptr = Self::te_record_ptr(bus, te_handle);
        if Self::te_is_styled_record(bus, te_ptr) {
            bus.write_long(te_ptr + Self::TE_TX_FONT_OFFSET, style_handle);
        }
    }

    fn te_line_starts(bus: &MacMemoryBus, te_handle: u32) -> Vec<usize> {
        let te_ptr = Self::te_record_ptr(bus, te_handle);
        if te_ptr == 0 {
            return vec![0];
        }

        let n_lines = bus.read_word(te_ptr + Self::TE_N_LINES_OFFSET) as usize;
        let mut starts = Vec::with_capacity(n_lines.saturating_add(1));
        for index in 0..=n_lines {
            starts.push(
                bus.read_word(te_ptr + Self::TE_LINE_STARTS_OFFSET + (index as u32 * 2)) as usize,
            );
        }
        if starts.is_empty() {
            starts.push(0);
        }
        starts
    }

    fn te_char_to_line_index(bus: &MacMemoryBus, te_handle: u32, offset: usize) -> usize {
        let starts = Self::te_line_starts(bus, te_handle);
        let n_lines = starts.len().saturating_sub(1);
        if n_lines <= 1 {
            return 0;
        }

        let mut low = 0usize;
        let mut high = n_lines;
        let mut current = (high + low) / 2;
        while low < high && starts[current] != offset {
            if starts[current] < offset {
                low = current + 1;
            } else {
                high = current.saturating_sub(1);
            }
            current = (high + low) / 2;
        }

        if starts[current] > offset || current == n_lines {
            current.saturating_sub(1)
        } else {
            current
        }
    }

    fn font_lookup_size(size: i16) -> i16 {
        // QuickDraw grafPorts initialize txSize to 0, which the Font Manager
        // interprets as the system font size. Preserve that sentinel for font
        // lookup; coercing it to 1 selects the wrong strike/fallback.
        // Inside Macintosh Volume I, I-163 and I-178.
        if size == 0 {
            0
        } else {
            size.max(1)
        }
    }

    fn te_char_to_point(&self, bus: &MacMemoryBus, te_handle: u32, offset: usize) -> (i16, i16) {
        let te_ptr = Self::te_record_ptr(bus, te_handle);
        if te_ptr == 0 {
            return (0, 0);
        }

        let dest_rect = Self::te_read_rect(bus, te_ptr + Self::TE_DEST_RECT_OFFSET);
        let just = bus.read_word(te_ptr + Self::TE_JUST_OFFSET) as i16;
        let text_bytes = Self::te_text_bytes(bus, te_handle);
        let text_len = text_bytes.len();
        let clamped = offset.min(text_len);
        let starts = Self::te_line_starts(bus, te_handle);
        let line_index = Self::te_char_to_line_index(bus, te_handle, clamped);
        let line_start = starts.get(line_index).copied().unwrap_or(0);
        let line_end = starts.get(line_index + 1).copied().unwrap_or(text_len);
        let on_break = clamped != 0
            && clamped == text_len
            && text_bytes.get(clamped - 1).copied() == Some(b'\r');
        let styled_runs = if Self::te_is_styled_record(bus, te_ptr) {
            self.te_style_runs(bus, te_handle, text_len)
        } else {
            Vec::new()
        };
        let uses_styled_runs = !styled_runs.is_empty() && Self::te_is_styled_record(bus, te_ptr);
        let (font, _, size, _, _, _) = self.te_primary_style(bus, te_handle);
        let rect_width = dest_rect.3.saturating_sub(dest_rect.1);
        // Per Inside Macintosh: Text 1993, lines 7320-7323:
        //   teJustLeft   =  0 (flush left — system default for LTR)
        //   teJustCenter =  1 (centered)
        //   teJustRight  = -1 (flush right)
        //   teForceLeft  = -2 (force flush left)
        let left_offset = match just {
            1 | -1 => {
                let line_width = if on_break {
                    rect_width.saturating_sub(1)
                } else {
                    let line_width = if uses_styled_runs {
                        self.te_measure_text_width_styled(
                            &styled_runs,
                            &text_bytes,
                            line_start,
                            line_end,
                        )
                    } else {
                        self.te_measure_text_width(
                            font,
                            Self::font_lookup_size(size),
                            &text_bytes,
                            line_start,
                            line_end,
                        )
                    };
                    rect_width.saturating_sub(line_width).saturating_sub(1)
                };
                if just == 1 {
                    line_width / 2
                } else {
                    line_width
                }
            }
            _ => Self::TE_LINE_LEFT_INSET,
        };
        let x = if on_break {
            dest_rect.1.saturating_add(left_offset)
        } else {
            dest_rect
                .1
                .saturating_add(left_offset)
                .saturating_add(if uses_styled_runs {
                    self.te_measure_text_width_styled(
                        &styled_runs,
                        &text_bytes,
                        line_start,
                        clamped,
                    )
                } else {
                    self.te_measure_text_width(
                        font,
                        Self::font_lookup_size(size),
                        &text_bytes,
                        line_start,
                        clamped,
                    )
                })
        };

        let mut top = dest_rect.0;
        for current_line in 0..line_index {
            top = top.saturating_add(Self::te_height_for_line(bus, te_handle, current_line));
        }
        if on_break {
            top = top.saturating_add(Self::te_height_for_line(bus, te_handle, line_index));
        }

        (top, x)
    }

    /// Inverse of `te_char_to_point`: locate the character offset whose
    /// glyph cell contains `point` (vertical, horizontal). Used by
    /// TEGetOffset ($A83C) per Inside Macintosh Volume V, V-172.
    fn te_point_to_char(&self, bus: &MacMemoryBus, te_handle: u32, point: (i16, i16)) -> i16 {
        let te_ptr = Self::te_record_ptr(bus, te_handle);
        if te_ptr == 0 {
            return 0;
        }

        let text_bytes = Self::te_text_bytes(bus, te_handle);
        let text_len = text_bytes.len();
        let starts = Self::te_line_starts(bus, te_handle);
        let n_lines = starts.len().saturating_sub(1);
        if text_len == 0 || n_lines == 0 {
            return 0;
        }

        let dest_rect = Self::te_read_rect(bus, te_ptr + Self::TE_DEST_RECT_OFFSET);
        let (point_v, point_h) = point;

        if point_v < dest_rect.0 {
            return 0;
        }

        let mut top = dest_rect.0;
        let mut line_index = n_lines.saturating_sub(1);
        let mut found_line = false;
        for current_line in 0..n_lines {
            let height = Self::te_height_for_line(bus, te_handle, current_line);
            let bottom = top.saturating_add(height);
            if point_v < bottom {
                line_index = current_line;
                found_line = true;
                break;
            }
            top = bottom;
        }
        if !found_line {
            return text_len.min(i16::MAX as usize) as i16;
        }

        let line_start = starts[line_index];
        let line_end = starts.get(line_index + 1).copied().unwrap_or(text_len);

        let just = bus.read_word(te_ptr + Self::TE_JUST_OFFSET) as i16;
        let styled_runs = if Self::te_is_styled_record(bus, te_ptr) {
            self.te_style_runs(bus, te_handle, text_len)
        } else {
            Vec::new()
        };
        let uses_styled_runs = !styled_runs.is_empty() && Self::te_is_styled_record(bus, te_ptr);
        let (font, _, size, _, _, _) = self.te_primary_style(bus, te_handle);
        let rect_width = dest_rect.3.saturating_sub(dest_rect.1);
        let left_offset = match just {
            1 | -1 => {
                let measured_width = if uses_styled_runs {
                    self.te_measure_text_width_styled(
                        &styled_runs,
                        &text_bytes,
                        line_start,
                        line_end,
                    )
                } else {
                    self.te_measure_text_width(
                        font,
                        Self::font_lookup_size(size),
                        &text_bytes,
                        line_start,
                        line_end,
                    )
                };
                let line_width = rect_width.saturating_sub(measured_width).saturating_sub(1);
                if just == 1 {
                    line_width / 2
                } else {
                    line_width
                }
            }
            _ => Self::TE_LINE_LEFT_INSET,
        };

        let mut x_cursor = dest_rect.1.saturating_add(left_offset);
        for (offset, byte) in text_bytes[line_start..line_end].iter().enumerate() {
            let index = line_start + offset;
            if matches!(*byte, b'\r' | b'\n') {
                return index.min(i16::MAX as usize) as i16;
            }
            let advance = if uses_styled_runs {
                let style = Self::te_style_at_offset(&styled_runs, index);
                self.te_char_width(style.font, style.size, *byte)
            } else {
                self.te_char_width(font, Self::font_lookup_size(size), *byte)
            };
            // Hit-test against the glyph midpoint so a click on the right
            // half of a character lands on the next offset, matching the
            // semantics described in Inside Macintosh Volume V, V-172.
            let mid = x_cursor.saturating_add(advance / 2);
            if point_h < mid {
                return index.min(i16::MAX as usize) as i16;
            }
            x_cursor = x_cursor.saturating_add(advance);
        }
        line_end.min(i16::MAX as usize) as i16
    }

    fn te_scroll_contents(
        &mut self,
        cpu: &mut impl CpuOps,
        bus: &mut MacMemoryBus,
        te_handle: u32,
        dh: i16,
        dv: i16,
    ) {
        if dh == 0 && dv == 0 {
            return;
        }

        let te_ptr = Self::te_record_ptr(bus, te_handle);
        if te_ptr == 0 {
            return;
        }

        let mut dest_rect = Self::te_read_rect(bus, te_ptr + Self::TE_DEST_RECT_OFFSET);
        dest_rect.0 = dest_rect.0.saturating_add(dv);
        dest_rect.1 = dest_rect.1.saturating_add(dh);
        dest_rect.2 = dest_rect.2.saturating_add(dv);
        dest_rect.3 = dest_rect.3.saturating_add(dh);
        Self::te_write_rect_words(bus, te_ptr + Self::TE_DEST_RECT_OFFSET, dest_rect);
        self.draw_te_contents(cpu, bus, te_handle);
        // Refresh rendered_pixels so redraw_chrome restores the scrolled state rather
        // than the pre-scroll snapshot captured at dialog creation.
        // Text 1993, 2-89 (TEScroll/TEPinScroll modify destRect and redraw).
        if let Some(ref tracking) = self.dialog_tracking {
            if !tracking.game_managed && tracking.rendered_pixels_final {
                let bounds = tracking.bounds;
                let new_pixels = self.save_dialog_pixels(bus, bounds);
                self.dialog_tracking.as_mut().unwrap().rendered_pixels = new_pixels;
            }
        }
    }

    fn te_getdelta(sel_start: i16, sel_stop: i16, view_start: i16, view_stop: i16) -> i16 {
        if sel_start < view_start {
            view_start.saturating_sub(sel_start)
        } else if sel_stop > view_stop {
            if sel_stop.saturating_sub(sel_start) > view_stop.saturating_sub(view_start) {
                view_start.saturating_sub(sel_start)
            } else {
                view_stop.saturating_sub(sel_stop)
            }
        } else {
            0
        }
    }

    fn te_height_for_line(bus: &MacMemoryBus, te_handle: u32, line_index: usize) -> i16 {
        let te_ptr = Self::te_record_ptr(bus, te_handle);
        if te_ptr == 0 {
            return 0;
        }

        if Self::te_is_styled_record(bus, te_ptr) {
            let style_handle = Self::te_style_handle(bus, te_handle);
            if style_handle != 0 {
                let style_ptr = bus.read_long(style_handle);
                if style_ptr != 0 {
                    let lh_handle = bus.read_long(style_ptr + Self::TE_STYLE_LH_TABLE_OFFSET);
                    let lh_ptr = if lh_handle != 0 {
                        bus.read_long(lh_handle)
                    } else {
                        0
                    };
                    if lh_ptr != 0 {
                        return bus.read_word(
                            lh_ptr
                                + (line_index as u32 * Self::LH_ELEMENT_SIZE)
                                + Self::LH_ELEMENT_HEIGHT_OFFSET,
                        ) as i16;
                    }
                }
            }
        }

        bus.read_word(te_ptr + Self::TE_LINE_HEIGHT_OFFSET) as i16
    }

    fn te_ascent_for_line(bus: &MacMemoryBus, te_handle: u32, line_index: usize) -> i16 {
        let te_ptr = Self::te_record_ptr(bus, te_handle);
        if te_ptr == 0 {
            return 0;
        }

        if Self::te_is_styled_record(bus, te_ptr) {
            let style_handle = Self::te_style_handle(bus, te_handle);
            if style_handle != 0 {
                let style_ptr = bus.read_long(style_handle);
                if style_ptr != 0 {
                    let lh_handle = bus.read_long(style_ptr + Self::TE_STYLE_LH_TABLE_OFFSET);
                    let lh_ptr = if lh_handle != 0 {
                        bus.read_long(lh_handle)
                    } else {
                        0
                    };
                    if lh_ptr != 0 {
                        return bus.read_word(
                            lh_ptr
                                + (line_index as u32 * Self::LH_ELEMENT_SIZE)
                                + Self::LH_ELEMENT_ASCENT_OFFSET,
                        ) as i16;
                    }
                }
            }
        }

        bus.read_word(te_ptr + Self::TE_FONT_ASCENT_OFFSET) as i16
    }

    fn te_primary_style(
        &self,
        bus: &MacMemoryBus,
        te_handle: u32,
    ) -> (i16, i16, i16, (u16, u16, u16), i16, i16) {
        let te_ptr = Self::te_record_ptr(bus, te_handle);
        if te_ptr == 0 {
            let size = Self::font_lookup_size(self.tx_size);
            let metrics = get_font_metrics(self.tx_font, size);
            return (
                self.tx_font,
                self.tx_face,
                size,
                self.fg_color,
                metrics.ascent + metrics.descent + metrics.leading,
                metrics.ascent,
            );
        }

        if Self::te_is_styled_record(bus, te_ptr) {
            let style_handle = bus.read_long(te_ptr + Self::TE_TX_FONT_OFFSET);
            let style_ptr = if style_handle != 0 {
                bus.read_long(style_handle)
            } else {
                0
            };
            if style_ptr != 0 {
                let style_table_handle =
                    bus.read_long(style_ptr + Self::TE_STYLE_STYLE_TABLE_OFFSET);
                let style_table_ptr = if style_table_handle != 0 {
                    bus.read_long(style_table_handle)
                } else {
                    0
                };
                if style_table_ptr != 0 {
                    return (
                        bus.read_word(style_table_ptr + Self::ST_ELEMENT_FONT_OFFSET) as i16,
                        bus.read_word(style_table_ptr + Self::ST_ELEMENT_FACE_OFFSET) as i16,
                        bus.read_word(style_table_ptr + Self::ST_ELEMENT_SIZE_OFFSET) as i16,
                        (
                            bus.read_word(style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET),
                            bus.read_word(style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 2),
                            bus.read_word(style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 4),
                        ),
                        bus.read_word(style_table_ptr + Self::ST_ELEMENT_HEIGHT_OFFSET) as i16,
                        bus.read_word(style_table_ptr + Self::ST_ELEMENT_ASCENT_OFFSET) as i16,
                    );
                }
            }
        }

        let tx_font = bus.read_word(te_ptr + Self::TE_TX_FONT_OFFSET) as i16;
        let tx_face = bus.read_word(te_ptr + Self::TE_TX_FACE_OFFSET) as i16;
        let tx_size =
            Self::font_lookup_size(bus.read_word(te_ptr + Self::TE_TX_SIZE_OFFSET) as i16);
        let metrics = get_font_metrics(tx_font, tx_size);
        (
            tx_font,
            tx_face,
            tx_size,
            self.fg_color,
            metrics.ascent + metrics.descent + metrics.leading,
            metrics.ascent,
        )
    }

    fn te_resolved_style_from_parts(
        font: i16,
        face: i16,
        size: i16,
        color: (u16, u16, u16),
        line_height: i16,
        ascent: i16,
    ) -> TeResolvedStyle {
        let resolved_size = Self::font_lookup_size(size);
        let metrics = get_font_metrics(font, resolved_size);
        let fallback_height = metrics.ascent + metrics.descent + metrics.leading;
        TeResolvedStyle {
            font,
            face,
            size: resolved_size,
            color,
            line_height: if line_height > 0 {
                line_height
            } else {
                fallback_height
            },
            ascent: if ascent > 0 { ascent } else { metrics.ascent },
        }
    }

    fn te_primary_resolved_style(&self, bus: &MacMemoryBus, te_handle: u32) -> TeResolvedStyle {
        let (font, face, size, color, line_height, ascent) = self.te_primary_style(bus, te_handle);
        Self::te_resolved_style_from_parts(font, face, size, color, line_height, ascent)
    }

    fn te_style_from_table_element(bus: &MacMemoryBus, style_ptr: u32) -> TeResolvedStyle {
        Self::te_resolved_style_from_parts(
            bus.read_word(style_ptr + Self::ST_ELEMENT_FONT_OFFSET) as i16,
            bus.read_word(style_ptr + Self::ST_ELEMENT_FACE_OFFSET) as i16,
            bus.read_word(style_ptr + Self::ST_ELEMENT_SIZE_OFFSET) as i16,
            (
                bus.read_word(style_ptr + Self::ST_ELEMENT_COLOR_OFFSET),
                bus.read_word(style_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 2),
                bus.read_word(style_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 4),
            ),
            bus.read_word(style_ptr + Self::ST_ELEMENT_HEIGHT_OFFSET) as i16,
            bus.read_word(style_ptr + Self::ST_ELEMENT_ASCENT_OFFSET) as i16,
        )
    }

    fn te_style_from_scrap_element(bus: &MacMemoryBus, scrap_style_ptr: u32) -> TeResolvedStyle {
        Self::te_resolved_style_from_parts(
            bus.read_word(scrap_style_ptr + Self::SCRAP_STYLE_FONT_OFFSET) as i16,
            bus.read_word(scrap_style_ptr + Self::SCRAP_STYLE_FACE_OFFSET) as i16,
            bus.read_word(scrap_style_ptr + Self::SCRAP_STYLE_SIZE_OFFSET) as i16,
            (
                bus.read_word(scrap_style_ptr + Self::SCRAP_STYLE_COLOR_OFFSET),
                bus.read_word(scrap_style_ptr + Self::SCRAP_STYLE_COLOR_OFFSET + 2),
                bus.read_word(scrap_style_ptr + Self::SCRAP_STYLE_COLOR_OFFSET + 4),
            ),
            bus.read_word(scrap_style_ptr + Self::SCRAP_STYLE_HEIGHT_OFFSET) as i16,
            bus.read_word(scrap_style_ptr + Self::SCRAP_STYLE_ASCENT_OFFSET) as i16,
        )
    }

    fn te_write_style_table_element(
        bus: &mut MacMemoryBus,
        style_ptr: u32,
        style: TeResolvedStyle,
    ) {
        bus.write_word(style_ptr + Self::ST_ELEMENT_REFCOUNT_OFFSET, 1);
        bus.write_word(
            style_ptr + Self::ST_ELEMENT_HEIGHT_OFFSET,
            style.line_height as u16,
        );
        bus.write_word(
            style_ptr + Self::ST_ELEMENT_ASCENT_OFFSET,
            style.ascent as u16,
        );
        bus.write_word(style_ptr + Self::ST_ELEMENT_FONT_OFFSET, style.font as u16);
        bus.write_word(style_ptr + Self::ST_ELEMENT_FACE_OFFSET, style.face as u16);
        bus.write_word(style_ptr + Self::ST_ELEMENT_SIZE_OFFSET, style.size as u16);
        bus.write_word(style_ptr + Self::ST_ELEMENT_COLOR_OFFSET, style.color.0);
        bus.write_word(style_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 2, style.color.1);
        bus.write_word(style_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 4, style.color.2);
    }

    fn te_style_runs(
        &self,
        bus: &MacMemoryBus,
        te_handle: u32,
        text_len: usize,
    ) -> Vec<TeStyleRun> {
        let fallback = self.te_primary_resolved_style(bus, te_handle);
        let te_ptr = Self::te_record_ptr(bus, te_handle);
        if !Self::te_is_styled_record(bus, te_ptr) {
            return vec![TeStyleRun {
                start: 0,
                style_index: 0,
                style: fallback,
            }];
        }

        let style_handle = Self::te_style_handle(bus, te_handle);
        let style_ptr = if style_handle != 0 {
            bus.read_long(style_handle)
        } else {
            0
        };
        if style_ptr == 0 {
            return vec![TeStyleRun {
                start: 0,
                style_index: 0,
                style: fallback,
            }];
        }

        let style_table_handle = bus.read_long(style_ptr + Self::TE_STYLE_STYLE_TABLE_OFFSET);
        let style_table_ptr = if style_table_handle != 0 {
            bus.read_long(style_table_handle)
        } else {
            0
        };
        if style_table_ptr == 0 {
            return vec![TeStyleRun {
                start: 0,
                style_index: 0,
                style: fallback,
            }];
        }

        let n_runs = bus.read_word(style_ptr + Self::TE_STYLE_N_RUNS_OFFSET) as usize;
        let n_styles = bus.read_word(style_ptr + Self::TE_STYLE_N_STYLES_OFFSET) as usize;
        let style_record_size = bus.get_alloc_size(style_ptr).unwrap_or(0);
        let style_table_size = bus.get_alloc_size(style_table_ptr).unwrap_or(0);
        let max_runs = style_record_size
            .saturating_sub(Self::TE_STYLE_RUNS_OFFSET)
            .checked_div(4)
            .unwrap_or(0) as usize;
        let max_styles = style_table_size
            .checked_div(Self::ST_ELEMENT_SIZE)
            .unwrap_or(0) as usize;
        let run_count = n_runs.min(max_runs);
        let style_count = n_styles.min(max_styles);
        if run_count == 0 || style_count == 0 {
            return vec![TeStyleRun {
                start: 0,
                style_index: 0,
                style: fallback,
            }];
        }

        let mut runs = Vec::with_capacity(run_count);
        for run_index in 0..run_count {
            let run_ptr = style_ptr + Self::TE_STYLE_RUNS_OFFSET + (run_index as u32 * 4);
            let style_index_word = bus.read_word(run_ptr + 2);
            if style_index_word == 0xFFFF {
                break;
            }
            let style_index = style_index_word as usize;
            if style_index >= style_count {
                continue;
            }
            let style_element_ptr = style_table_ptr + (style_index as u32 * Self::ST_ELEMENT_SIZE);
            runs.push(TeStyleRun {
                start: (bus.read_word(run_ptr) as usize).min(text_len),
                style_index,
                style: Self::te_style_from_table_element(bus, style_element_ptr),
            });
        }

        if runs.is_empty() {
            return vec![TeStyleRun {
                start: 0,
                style_index: 0,
                style: fallback,
            }];
        }

        runs.sort_by_key(|run| run.start);
        let mut folded: Vec<TeStyleRun> = Vec::with_capacity(runs.len() + 1);
        for run in runs {
            if let Some(last) = folded.last_mut() {
                if last.start == run.start {
                    *last = run;
                    continue;
                }
                if last.style == run.style {
                    continue;
                }
            }
            folded.push(run);
        }

        if folded.first().is_none_or(|run| run.start != 0) {
            folded.insert(
                0,
                TeStyleRun {
                    start: 0,
                    style_index: 0,
                    style: fallback,
                },
            );
        }
        folded
    }

    fn te_style_at_offset(runs: &[TeStyleRun], offset: usize) -> TeResolvedStyle {
        let mut style = runs
            .first()
            .map(|run| run.style)
            .unwrap_or_else(|| Self::te_resolved_style_from_parts(0, 0, 0, (0, 0, 0), 0, 0));
        for run in runs {
            if run.start > offset {
                break;
            }
            style = run.style;
        }
        style
    }

    fn te_measure_text_width_styled(
        &self,
        runs: &[TeStyleRun],
        text_bytes: &[u8],
        start: usize,
        end: usize,
    ) -> i16 {
        let start = start.min(text_bytes.len());
        let end = end.min(text_bytes.len());
        let mut width = 0i16;
        for (offset, &byte) in text_bytes[start..end].iter().enumerate() {
            let style = Self::te_style_at_offset(runs, start + offset);
            width = width.saturating_add(self.te_char_width(style.font, style.size, byte));
        }
        width
    }

    fn te_next_break_styled(
        &self,
        runs: &[TeStyleRun],
        text_bytes: &[u8],
        offset: usize,
        max_width: i16,
    ) -> usize {
        let len = text_bytes.len();
        if offset >= len {
            return len;
        }

        let mut width = 0i16;
        let mut index = offset;
        while width <= max_width && index < len && !matches!(text_bytes[index], b'\r' | b'\n') {
            let style = Self::te_style_at_offset(runs, index);
            width =
                width.saturating_add(self.te_char_width(style.font, style.size, text_bytes[index]));
            index += 1;
        }

        let mut next = if width > max_width {
            let max_index = index.saturating_sub(1);
            if text_bytes[max_index] == b' ' {
                let mut scan = index;
                while scan < len && text_bytes[scan] == b' ' {
                    scan += 1;
                }
                scan
            } else {
                let mut scan = max_index;
                while scan > offset && !Self::te_word_break_byte(text_bytes[scan - 1]) {
                    scan -= 1;
                }
                if scan == offset {
                    max_index
                } else {
                    scan
                }
            }
        } else if index == len {
            len
        } else {
            index + 1
        };

        if next == offset && offset < len {
            next += 1;
        }
        next
    }

    fn te_wrap_lines_styled(
        &self,
        runs: &[TeStyleRun],
        text_bytes: &[u8],
        box_width: i16,
    ) -> Vec<(usize, usize)> {
        let mut lines = Vec::new();
        let mut line_start = 0usize;
        while line_start < text_bytes.len() {
            let next = self.te_next_break_styled(runs, text_bytes, line_start, box_width);
            lines.push((line_start, next.min(text_bytes.len())));
            line_start = next;
        }
        lines
    }

    fn te_line_metrics_for_range(runs: &[TeStyleRun], start: usize, end: usize) -> (i16, i16) {
        if runs.is_empty() {
            return (0, 0);
        }

        let mut line_height = 0i16;
        let mut ascent = 0i16;
        if start >= end {
            let style = Self::te_style_at_offset(runs, start);
            return (style.line_height, style.ascent);
        }

        for offset in start..end {
            let style = Self::te_style_at_offset(runs, offset);
            line_height = line_height.max(style.line_height);
            ascent = ascent.max(style.ascent);
        }
        (line_height.max(1), ascent.max(0))
    }

    fn te_update_styled_run_sentinel(bus: &mut MacMemoryBus, te_handle: u32, text_len: usize) {
        let style_handle = Self::te_style_handle(bus, te_handle);
        if style_handle == 0 {
            return;
        }
        let style_ptr = bus.read_long(style_handle);
        if style_ptr == 0 {
            return;
        }
        let n_runs = bus.read_word(style_ptr + Self::TE_STYLE_N_RUNS_OFFSET) as usize;
        let needed = Self::TE_STYLE_RUNS_OFFSET + ((n_runs as u32 + 1) * 4);
        let style_ptr = Self::ensure_handle_capacity(bus, style_handle, needed);
        if style_ptr == 0 {
            return;
        }
        let sentinel = style_ptr + Self::TE_STYLE_RUNS_OFFSET + (n_runs as u32 * 4);
        bus.write_word(
            sentinel,
            text_len.saturating_add(1).min(u16::MAX as usize) as u16,
        );
        bus.write_word(sentinel + 2, 0xFFFF);
    }

    fn te_apply_style_scrap_to_range(
        &mut self,
        bus: &mut MacMemoryBus,
        te_handle: u32,
        style_scrap: u32,
        range_start: usize,
        range_len: usize,
    ) -> bool {
        let te_ptr = Self::te_record_ptr(bus, te_handle);
        if !Self::te_is_styled_record(bus, te_ptr) || style_scrap == 0 || range_len == 0 {
            return false;
        }

        let text_len = Self::te_text_length(bus, te_handle);
        let range_start = range_start.min(text_len);
        let range_end = range_start.saturating_add(range_len).min(text_len);
        if range_start >= range_end {
            return false;
        }

        let scrap_ptr = bus.read_long(style_scrap);
        if scrap_ptr == 0 {
            return false;
        }
        let scrap_size = bus.get_alloc_size(scrap_ptr).unwrap_or(0);
        if scrap_size < Self::SCRAP_STYLE_TAB_OFFSET {
            return false;
        }
        let style_count = (bus.read_word(scrap_ptr + Self::SCRAP_N_STYLES_OFFSET) as usize).min(
            scrap_size
                .saturating_sub(Self::SCRAP_STYLE_TAB_OFFSET)
                .checked_div(Self::SCRAP_STYLE_ELEMENT_SIZE)
                .unwrap_or(0) as usize,
        );
        if style_count == 0 {
            return false;
        }

        let existing_runs = self.te_style_runs(bus, te_handle, text_len);
        let mut candidates: Vec<(usize, u8, TeResolvedStyle)> =
            Vec::with_capacity(existing_runs.len() + style_count + 2);
        for run in &existing_runs {
            if run.start < range_start || run.start > range_end {
                candidates.push((run.start.min(text_len), 0, run.style));
            }
        }

        for style_index in 0..style_count {
            let scrap_style_ptr = scrap_ptr
                + Self::SCRAP_STYLE_TAB_OFFSET
                + (style_index as u32 * Self::SCRAP_STYLE_ELEMENT_SIZE);
            let local_start =
                bus.read_long(scrap_style_ptr + Self::SCRAP_STYLE_START_CHAR_OFFSET) as usize;
            let run_start = range_start.saturating_add(local_start.min(range_end - range_start));
            candidates.push((
                run_start,
                1,
                Self::te_style_from_scrap_element(bus, scrap_style_ptr),
            ));
        }

        if range_end < text_len {
            candidates.push((
                range_end,
                2,
                Self::te_style_at_offset(&existing_runs, range_end),
            ));
        }
        if !candidates.iter().any(|(start, _, _)| *start == 0) {
            candidates.push((0, 0, Self::te_style_at_offset(&existing_runs, 0)));
        }

        candidates.sort_by_key(|(start, order, _)| (*start, *order));
        let mut merged: Vec<(usize, TeResolvedStyle)> = Vec::with_capacity(candidates.len());
        for (start, _, style) in candidates {
            let start = start.min(text_len);
            if let Some((last_start, last_style)) = merged.last_mut() {
                if *last_start == start {
                    *last_style = style;
                    continue;
                }
                if *last_style == style {
                    continue;
                }
            }
            merged.push((start, style));
        }

        if merged.is_empty() {
            merged.push((0, self.te_primary_resolved_style(bus, te_handle)));
        }
        if merged[0].0 != 0 {
            merged.insert(0, (0, Self::te_style_at_offset(&existing_runs, 0)));
        }

        let run_count = merged.len().min(u16::MAX as usize);
        let style_handle = Self::te_style_handle(bus, te_handle);
        if style_handle == 0 {
            return false;
        }
        let style_ptr = Self::ensure_handle_capacity(
            bus,
            style_handle,
            Self::TE_STYLE_RUNS_OFFSET + ((run_count as u32 + 1) * 4),
        );
        if style_ptr == 0 {
            return false;
        }

        let mut style_table_handle = bus.read_long(style_ptr + Self::TE_STYLE_STYLE_TABLE_OFFSET);
        if style_table_handle == 0 {
            style_table_handle = Self::allocate_handle_with_data(bus, 0);
            bus.write_long(
                style_ptr + Self::TE_STYLE_STYLE_TABLE_OFFSET,
                style_table_handle,
            );
        }
        let style_table_ptr = Self::ensure_handle_capacity(
            bus,
            style_table_handle,
            (run_count as u32) * Self::ST_ELEMENT_SIZE,
        );
        if style_table_ptr == 0 {
            return false;
        }

        bus.write_word(style_ptr + Self::TE_STYLE_N_RUNS_OFFSET, run_count as u16);
        bus.write_word(style_ptr + Self::TE_STYLE_N_STYLES_OFFSET, run_count as u16);
        for (index, (start, style)) in merged.iter().take(run_count).enumerate() {
            let style_element_ptr = style_table_ptr + (index as u32 * Self::ST_ELEMENT_SIZE);
            Self::te_write_style_table_element(bus, style_element_ptr, *style);
            let run_ptr = style_ptr + Self::TE_STYLE_RUNS_OFFSET + (index as u32 * 4);
            bus.write_word(run_ptr, (*start).min(u16::MAX as usize) as u16);
            bus.write_word(run_ptr + 2, index.min(u16::MAX as usize) as u16);
        }
        Self::te_update_styled_run_sentinel(bus, te_handle, text_len);
        true
    }

    #[allow(dead_code)]
    fn te_line_metrics(&self, bus: &MacMemoryBus, te_handle: u32) -> (i16, i16) {
        let te_ptr = Self::te_record_ptr(bus, te_handle);
        if te_ptr == 0 {
            let metrics = get_font_metrics(self.tx_font, Self::font_lookup_size(self.tx_size));
            return (
                metrics.ascent + metrics.descent + metrics.leading,
                metrics.ascent,
            );
        }

        let line_height = bus.read_word(te_ptr + Self::TE_LINE_HEIGHT_OFFSET) as i16;
        let font_ascent = bus.read_word(te_ptr + Self::TE_FONT_ASCENT_OFFSET) as i16;
        if line_height > 0 && font_ascent >= 0 {
            return (line_height, font_ascent);
        }

        let (_, _, _, _, resolved_line_height, resolved_ascent) =
            self.te_primary_style(bus, te_handle);
        (resolved_line_height, resolved_ascent)
    }

    fn initialize_te_record(
        &mut self,
        bus: &mut MacMemoryBus,
        te_handle: u32,
        dest_rect: (i16, i16, i16, i16),
        view_rect: (i16, i16, i16, i16),
    ) {
        let te_ptr = Self::te_record_ptr(bus, te_handle);
        if te_ptr == 0 {
            return;
        }

        let metrics = get_font_metrics(self.tx_font, Self::font_lookup_size(self.tx_size));
        let line_height = metrics.ascent + metrics.descent + metrics.leading;
        let h_text = bus.alloc(4);
        if h_text != 0 {
            bus.write_long(h_text, 0);
        }

        Self::te_write_rect_words(bus, te_ptr + Self::TE_DEST_RECT_OFFSET, dest_rect);
        Self::te_write_rect_words(bus, te_ptr + Self::TE_VIEW_RECT_OFFSET, view_rect);
        Self::te_write_rect_words(bus, te_ptr + Self::TE_SEL_RECT_OFFSET, dest_rect);
        bus.write_word(te_ptr + Self::TE_LINE_HEIGHT_OFFSET, line_height as u16);
        bus.write_word(te_ptr + Self::TE_FONT_ASCENT_OFFSET, metrics.ascent as u16);
        bus.write_word(te_ptr + Self::TE_SEL_POINT_OFFSET, dest_rect.0 as u16);
        bus.write_word(te_ptr + Self::TE_SEL_POINT_OFFSET + 2, dest_rect.1 as u16);
        bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, 0);
        bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, 0);
        bus.write_long(te_ptr + Self::TE_CARET_TIME_OFFSET, self.tick_count);
        bus.write_word(te_ptr + Self::TE_CARET_STATE_OFFSET, 0);
        bus.write_word(te_ptr + Self::TE_LENGTH_OFFSET, 0);
        bus.write_long(te_ptr + Self::TE_HTEXT_OFFSET, h_text);
        bus.write_word(te_ptr + Self::TE_TX_FONT_OFFSET, self.tx_font as u16);
        bus.write_word(te_ptr + Self::TE_TX_FACE_OFFSET, self.tx_face as u16);
        bus.write_word(te_ptr + Self::TE_TX_MODE_OFFSET, self.tx_mode as u16);
        bus.write_word(te_ptr + Self::TE_TX_SIZE_OFFSET, self.tx_size as u16);
        bus.write_long(te_ptr + Self::TE_IN_PORT_OFFSET, self.current_port);
        self.textedit_states.remove(&te_handle);
    }

    fn initialize_styled_te_record(
        &mut self,
        bus: &mut MacMemoryBus,
        te_handle: u32,
        dest_rect: (i16, i16, i16, i16),
        view_rect: (i16, i16, i16, i16),
    ) {
        let te_ptr = Self::te_record_ptr(bus, te_handle);
        if te_ptr == 0 {
            return;
        }

        let style_size = Self::font_lookup_size(self.tx_size);
        let metrics = get_font_metrics(self.tx_font, style_size);
        let line_height = metrics.ascent + metrics.descent + metrics.leading;

        let h_text = Self::allocate_handle_with_data(bus, 0);
        let style_table = Self::allocate_handle_with_data(bus, Self::ST_ELEMENT_SIZE);
        let lh_table = Self::allocate_handle_with_data(bus, Self::LH_ELEMENT_SIZE);
        let null_scrap = Self::allocate_handle_with_data(bus, Self::STYLE_SCRAP_REC_SIZE);
        let null_style = Self::allocate_handle_with_data(bus, Self::NULL_STYLE_REC_SIZE);
        let style_handle = Self::allocate_handle_with_data(bus, 0x1C);

        let style_table_ptr = if style_table != 0 {
            bus.read_long(style_table)
        } else {
            0
        };
        if style_table_ptr != 0 {
            bus.write_word(style_table_ptr + Self::ST_ELEMENT_REFCOUNT_OFFSET, 1);
            bus.write_word(
                style_table_ptr + Self::ST_ELEMENT_HEIGHT_OFFSET,
                line_height as u16,
            );
            bus.write_word(
                style_table_ptr + Self::ST_ELEMENT_ASCENT_OFFSET,
                metrics.ascent as u16,
            );
            bus.write_word(
                style_table_ptr + Self::ST_ELEMENT_FONT_OFFSET,
                self.tx_font as u16,
            );
            bus.write_word(
                style_table_ptr + Self::ST_ELEMENT_FACE_OFFSET,
                self.tx_face as u16,
            );
            bus.write_word(
                style_table_ptr + Self::ST_ELEMENT_SIZE_OFFSET,
                style_size as u16,
            );
            bus.write_word(
                style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET,
                self.fg_color.0,
            );
            bus.write_word(
                style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 2,
                self.fg_color.1,
            );
            bus.write_word(
                style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 4,
                self.fg_color.2,
            );
        }

        let lh_table_ptr = if lh_table != 0 {
            bus.read_long(lh_table)
        } else {
            0
        };
        if lh_table_ptr != 0 {
            bus.write_word(
                lh_table_ptr + Self::LH_ELEMENT_HEIGHT_OFFSET,
                line_height as u16,
            );
            bus.write_word(
                lh_table_ptr + Self::LH_ELEMENT_ASCENT_OFFSET,
                metrics.ascent as u16,
            );
        }

        let null_scrap_ptr = if null_scrap != 0 {
            bus.read_long(null_scrap)
        } else {
            0
        };
        if null_scrap_ptr != 0 {
            bus.write_word(null_scrap_ptr + Self::SCRAP_N_STYLES_OFFSET, 0);
            bus.write_long(
                null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_START_CHAR_OFFSET,
                0,
            );
            bus.write_word(
                null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_HEIGHT_OFFSET,
                line_height as u16,
            );
            bus.write_word(
                null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_ASCENT_OFFSET,
                metrics.ascent as u16,
            );
            bus.write_word(
                null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_FONT_OFFSET,
                self.tx_font as u16,
            );
            bus.write_word(
                null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_FACE_OFFSET,
                self.tx_face as u16,
            );
            bus.write_word(
                null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_SIZE_OFFSET,
                style_size as u16,
            );
            bus.write_word(
                null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_COLOR_OFFSET,
                self.fg_color.0,
            );
            bus.write_word(
                null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_COLOR_OFFSET + 2,
                self.fg_color.1,
            );
            bus.write_word(
                null_scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET + Self::SCRAP_STYLE_COLOR_OFFSET + 4,
                self.fg_color.2,
            );
        }

        let null_style_ptr = if null_style != 0 {
            bus.read_long(null_style)
        } else {
            0
        };
        if null_style_ptr != 0 {
            bus.write_long(null_style_ptr + Self::NULL_STYLE_SCRAP_OFFSET, null_scrap);
        }

        let style_ptr = if style_handle != 0 {
            bus.read_long(style_handle)
        } else {
            0
        };
        if style_ptr != 0 {
            bus.write_word(style_ptr + Self::TE_STYLE_N_RUNS_OFFSET, 1);
            bus.write_word(style_ptr + Self::TE_STYLE_N_STYLES_OFFSET, 1);
            bus.write_long(style_ptr + Self::TE_STYLE_STYLE_TABLE_OFFSET, style_table);
            bus.write_long(style_ptr + Self::TE_STYLE_LH_TABLE_OFFSET, lh_table);
            bus.write_long(style_ptr + Self::TE_STYLE_NULL_STYLE_OFFSET, null_style);
            bus.write_word(style_ptr + Self::TE_STYLE_RUNS_OFFSET, 0);
            bus.write_word(style_ptr + Self::TE_STYLE_RUNS_OFFSET + 2, 0);
            bus.write_word(style_ptr + Self::TE_STYLE_RUNS_OFFSET + 4, 1);
            bus.write_word(style_ptr + Self::TE_STYLE_RUNS_OFFSET + 6, 0xFFFF);
        }

        Self::te_write_rect_words(bus, te_ptr + Self::TE_DEST_RECT_OFFSET, dest_rect);
        Self::te_write_rect_words(bus, te_ptr + Self::TE_VIEW_RECT_OFFSET, view_rect);
        Self::te_write_rect_words(bus, te_ptr + Self::TE_SEL_RECT_OFFSET, dest_rect);
        bus.write_word(te_ptr + Self::TE_LINE_HEIGHT_OFFSET, 0xFFFF);
        bus.write_word(te_ptr + Self::TE_FONT_ASCENT_OFFSET, 0xFFFF);
        bus.write_word(te_ptr + Self::TE_SEL_POINT_OFFSET, dest_rect.0 as u16);
        bus.write_word(te_ptr + Self::TE_SEL_POINT_OFFSET + 2, dest_rect.1 as u16);
        bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, 0);
        bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, 0);
        bus.write_long(te_ptr + Self::TE_CARET_TIME_OFFSET, self.tick_count);
        bus.write_word(te_ptr + Self::TE_CARET_STATE_OFFSET, 0);
        bus.write_word(te_ptr + Self::TE_JUST_OFFSET, 0);
        bus.write_word(te_ptr + Self::TE_LENGTH_OFFSET, 0);
        bus.write_long(te_ptr + Self::TE_HTEXT_OFFSET, h_text);
        bus.write_long(te_ptr + Self::TE_TX_FONT_OFFSET, style_handle);
        bus.write_word(te_ptr + Self::TE_TX_MODE_OFFSET, self.tx_mode as u16);
        bus.write_word(te_ptr + Self::TE_TX_SIZE_OFFSET, 0xFFFF);
        bus.write_long(te_ptr + Self::TE_IN_PORT_OFFSET, self.current_port);
        bus.write_word(te_ptr + Self::TE_N_LINES_OFFSET, 0);
        bus.write_word(te_ptr + Self::TE_LINE_STARTS_OFFSET, 0);
        self.textedit_states.remove(&te_handle);
    }

    fn te_text_length(bus: &MacMemoryBus, te_handle: u32) -> usize {
        let te_ptr = Self::te_record_ptr(bus, te_handle);
        if te_ptr == 0 {
            return 0;
        }

        let stored = bus.read_word(te_ptr + Self::TE_LENGTH_OFFSET) as usize;
        if stored != 0 {
            return stored;
        }

        let h_text = bus.read_long(te_ptr + Self::TE_HTEXT_OFFSET);
        if h_text == 0 {
            return 0;
        }
        let text_ptr = bus.read_long(h_text);
        if text_ptr == 0 {
            0
        } else {
            bus.get_alloc_size(text_ptr).unwrap_or(0) as usize
        }
    }

    fn te_text_bytes(bus: &MacMemoryBus, te_handle: u32) -> Vec<u8> {
        let len = Self::te_text_length(bus, te_handle);
        if len == 0 {
            return Vec::new();
        }

        let h_text = Self::te_text_handle(bus, te_handle);
        if h_text == 0 {
            return Vec::new();
        }
        let text_ptr = bus.read_long(h_text);
        if text_ptr == 0 {
            return Vec::new();
        }
        bus.read_bytes(text_ptr, len)
    }

    pub(crate) fn te_find_word_bounds(
        &self,
        bus: &MacMemoryBus,
        te_handle: u32,
        current_pos: usize,
    ) -> (u16, u16) {
        let text_bytes = Self::te_text_bytes(bus, te_handle);
        let len = text_bytes.len();
        if len == 0 {
            return (0, 0);
        }

        let pos = current_pos.min(len);
        if pos < len && Self::te_word_break_byte(text_bytes[pos]) {
            return (pos as u16, pos as u16);
        }

        let mut start = pos;
        while start > 0 && !Self::te_word_break_byte(text_bytes[start - 1]) {
            start -= 1;
        }

        let mut end = pos;
        while end < len && !Self::te_word_break_byte(text_bytes[end]) {
            end += 1;
        }

        (start as u16, end as u16)
    }

    #[allow(dead_code)]
    fn te_reset_styled_metadata(
        &mut self,
        bus: &mut MacMemoryBus,
        te_handle: u32,
        text_len: usize,
    ) {
        let style_handle = Self::te_style_handle(bus, te_handle);
        if style_handle == 0 {
            return;
        }

        let (font, face, size, color, line_height, ascent) = self.te_primary_style(bus, te_handle);
        let style_ptr = bus.read_long(style_handle);
        if style_ptr == 0 {
            return;
        }

        let style_table_handle = bus.read_long(style_ptr + Self::TE_STYLE_STYLE_TABLE_OFFSET);
        let style_table_ptr = if style_table_handle != 0 {
            bus.read_long(style_table_handle)
        } else {
            0
        };
        if style_table_ptr != 0 {
            bus.write_word(style_table_ptr + Self::ST_ELEMENT_REFCOUNT_OFFSET, 1);
            bus.write_word(
                style_table_ptr + Self::ST_ELEMENT_HEIGHT_OFFSET,
                line_height as u16,
            );
            bus.write_word(
                style_table_ptr + Self::ST_ELEMENT_ASCENT_OFFSET,
                ascent as u16,
            );
            bus.write_word(style_table_ptr + Self::ST_ELEMENT_FONT_OFFSET, font as u16);
            bus.write_word(style_table_ptr + Self::ST_ELEMENT_FACE_OFFSET, face as u16);
            bus.write_word(style_table_ptr + Self::ST_ELEMENT_SIZE_OFFSET, size as u16);
            bus.write_word(style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET, color.0);
            bus.write_word(style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 2, color.1);
            bus.write_word(style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 4, color.2);
        }

        let lh_handle = bus.read_long(style_ptr + Self::TE_STYLE_LH_TABLE_OFFSET);
        let lh_ptr = if lh_handle != 0 {
            bus.read_long(lh_handle)
        } else {
            0
        };
        if lh_ptr != 0 {
            bus.write_word(lh_ptr + Self::LH_ELEMENT_HEIGHT_OFFSET, line_height as u16);
            bus.write_word(lh_ptr + Self::LH_ELEMENT_ASCENT_OFFSET, ascent as u16);
        }

        bus.write_word(style_ptr + Self::TE_STYLE_N_RUNS_OFFSET, 1);
        bus.write_word(style_ptr + Self::TE_STYLE_N_STYLES_OFFSET, 1);
        bus.write_word(style_ptr + Self::TE_STYLE_RUNS_OFFSET, 0);
        bus.write_word(style_ptr + Self::TE_STYLE_RUNS_OFFSET + 2, 0);
        bus.write_word(
            style_ptr + Self::TE_STYLE_RUNS_OFFSET + 4,
            text_len.saturating_add(1).min(u16::MAX as usize) as u16,
        );
        bus.write_word(style_ptr + Self::TE_STYLE_RUNS_OFFSET + 6, 0xFFFF);
    }

    fn te_set_text_contents(&mut self, bus: &mut MacMemoryBus, te_handle: u32, text: &[u8]) {
        let te_ptr = Self::te_record_ptr(bus, te_handle);
        if te_ptr == 0 {
            return;
        }

        let mut h_text = bus.read_long(te_ptr + Self::TE_HTEXT_OFFSET);
        if h_text == 0 {
            h_text = Self::allocate_handle_with_data(bus, 0);
            bus.write_long(te_ptr + Self::TE_HTEXT_OFFSET, h_text);
        }

        let text_ptr = Self::ensure_text_handle_size(bus, h_text, text.len());
        if !text.is_empty() && text_ptr != 0 {
            bus.write_bytes(text_ptr, text);
        }

        let clamped_len = text.len().min(u16::MAX as usize) as u16;
        bus.write_word(te_ptr + Self::TE_LENGTH_OFFSET, clamped_len);
        bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, clamped_len);
        bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, clamped_len);
        self.te_recalculate_layout(bus, te_handle);
    }

    fn textedit_idle(&mut self, cpu: &mut impl CpuOps, bus: &mut MacMemoryBus, te_handle: u32) {
        if trace_textedit_enabled() {
            eprintln!("[TE] TEIdle hTE=${te_handle:08X}");
        }
        let te_ptr = Self::te_record_ptr(bus, te_handle);
        if te_ptr == 0 {
            return;
        }
        if bus.read_word(te_ptr + Self::TE_ACTIVE_OFFSET) == 0 {
            return;
        }

        let sel_start = bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET);
        let sel_end = bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET);
        if sel_start != sel_end {
            return;
        }

        let last_toggle = bus.read_long(te_ptr + Self::TE_CARET_TIME_OFFSET);
        if self.tick_count.wrapping_sub(last_toggle) < Self::TE_CARET_BLINK_TICKS {
            return;
        }

        let caret_state = bus.read_word(te_ptr + Self::TE_CARET_STATE_OFFSET);
        bus.write_word(
            te_ptr + Self::TE_CARET_STATE_OFFSET,
            if caret_state == 0 { 1 } else { 0 },
        );
        bus.write_long(te_ptr + Self::TE_CARET_TIME_OFFSET, self.tick_count);
        self.draw_te_contents(cpu, bus, te_handle);
    }

    fn te_insert_text(&mut self, bus: &mut MacMemoryBus, te_handle: u32, text: &[u8]) {
        let te_ptr = Self::te_record_ptr(bus, te_handle);
        if te_ptr == 0 {
            return;
        }

        let existing = Self::te_text_bytes(bus, te_handle);
        let mut sel_start = bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize;
        let mut sel_end = bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize;
        let text_len = existing.len();
        sel_start = sel_start.min(text_len);
        sel_end = sel_end.min(text_len);
        if sel_end < sel_start {
            std::mem::swap(&mut sel_start, &mut sel_end);
        }

        let mut merged =
            Vec::with_capacity(sel_start + text.len() + text_len.saturating_sub(sel_end));
        merged.extend_from_slice(&existing[..sel_start]);
        merged.extend_from_slice(text);
        merged.extend_from_slice(&existing[sel_end..]);
        self.te_set_text_contents(bus, te_handle, &merged);

        let insertion_end = (sel_start + text.len()).min(u16::MAX as usize) as u16;
        bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, insertion_end);
        bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, insertion_end);
    }

    fn te_char_width(&self, font: i16, size: i16, byte: u8) -> i16 {
        let size = Self::font_lookup_size(size);
        let (_face, scale) = get_font_face_scaled(font, size);
        let ch = byte as char;
        if let Some((glyph, _)) = crate::quickdraw::text::get_glyph(font, size, ch) {
            self.glyph_advance(glyph) * scale
        } else {
            self.missing_glyph_advance() * scale
        }
    }

    fn te_measure_text_width(
        &self,
        font: i16,
        size: i16,
        text_bytes: &[u8],
        start: usize,
        end: usize,
    ) -> i16 {
        let mut width = 0i16;
        for &b in &text_bytes[start..end] {
            width += self.te_char_width(font, size, b);
        }
        width
    }

    fn te_word_break_byte(byte: u8) -> bool {
        byte <= 0x20
    }

    fn te_next_break(
        &self,
        font: i16,
        size: i16,
        text_bytes: &[u8],
        offset: usize,
        max_width: i16,
    ) -> usize {
        let len = text_bytes.len();
        if offset >= len {
            return len;
        }

        let mut width = 0i16;
        let mut index = offset;
        while width <= max_width && index < len && !matches!(text_bytes[index], b'\r' | b'\n') {
            width = width.saturating_add(self.te_char_width(font, size, text_bytes[index]));
            index += 1;
        }

        let mut next = if width > max_width {
            let max_index = index.saturating_sub(1);
            if text_bytes[max_index] == b' ' {
                let mut scan = index;
                while scan < len && text_bytes[scan] == b' ' {
                    scan += 1;
                }
                scan
            } else {
                let mut scan = max_index;
                while scan > offset && !Self::te_word_break_byte(text_bytes[scan - 1]) {
                    scan -= 1;
                }
                if scan == offset {
                    max_index
                } else {
                    scan
                }
            }
        } else if index == len {
            len
        } else {
            index + 1
        };

        if next == offset && offset < len {
            next += 1;
        }
        next
    }

    fn te_wrap_lines(
        &self,
        font: i16,
        size: i16,
        text_bytes: &[u8],
        box_width: i16,
    ) -> Vec<(usize, usize)> {
        let mut lines: Vec<(usize, usize)> = Vec::new();
        let mut line_start = 0usize;
        while line_start < text_bytes.len() {
            let next = self.te_next_break(font, size, text_bytes, line_start, box_width);
            lines.push((line_start, next.min(text_bytes.len())));
            line_start = next;
        }

        lines
    }

    fn te_recalculate_layout(&mut self, bus: &mut MacMemoryBus, te_handle: u32) {
        let te_ptr = Self::te_record_ptr(bus, te_handle);
        if te_ptr == 0 {
            return;
        }

        let text_bytes = Self::te_text_bytes(bus, te_handle);
        let text_len = text_bytes.len();
        let dest_rect = Self::te_read_rect(bus, te_ptr + Self::TE_DEST_RECT_OFFSET);
        let (font, _, size, _, line_height, font_ascent) = self.te_primary_style(bus, te_handle);
        let styled = Self::te_is_styled_record(bus, te_ptr);
        let style_runs = if styled {
            self.te_style_runs(bus, te_handle, text_len)
        } else {
            Vec::new()
        };
        let uses_styled_runs = styled && !style_runs.is_empty();
        let lines = if text_bytes.is_empty() {
            Vec::new()
        } else if uses_styled_runs {
            self.te_wrap_lines_styled(&style_runs, &text_bytes, (dest_rect.3 - dest_rect.1).max(0))
        } else {
            self.te_wrap_lines(font, size, &text_bytes, (dest_rect.3 - dest_rect.1).max(0))
        };
        let line_count = lines.len();
        let te_ptr = Self::ensure_te_record_line_capacity(bus, te_handle, line_count);
        if te_ptr == 0 {
            return;
        }

        bus.write_word(
            te_ptr + Self::TE_N_LINES_OFFSET,
            line_count.min(u16::MAX as usize) as u16,
        );
        if styled {
            bus.write_word(te_ptr + Self::TE_LINE_HEIGHT_OFFSET, 0xFFFF);
            bus.write_word(te_ptr + Self::TE_FONT_ASCENT_OFFSET, 0xFFFF);
        } else {
            bus.write_word(te_ptr + Self::TE_LINE_HEIGHT_OFFSET, line_height as u16);
            bus.write_word(te_ptr + Self::TE_FONT_ASCENT_OFFSET, font_ascent as u16);
        }

        for index in 0..=line_count.saturating_add(1) {
            bus.write_word(te_ptr + Self::TE_LINE_STARTS_OFFSET + (index as u32 * 2), 0);
        }
        for (index, (start, _)) in lines.iter().enumerate() {
            bus.write_word(
                te_ptr + Self::TE_LINE_STARTS_OFFSET + (index as u32 * 2),
                (*start).min(u16::MAX as usize) as u16,
            );
        }
        bus.write_word(
            te_ptr + Self::TE_LINE_STARTS_OFFSET + (line_count as u32 * 2),
            text_len.min(u16::MAX as usize) as u16,
        );
        bus.write_word(
            te_ptr + Self::TE_LINE_STARTS_OFFSET + ((line_count as u32 + 1) * 2),
            0,
        );

        if styled {
            let style_handle = Self::te_style_handle(bus, te_handle);
            if style_handle != 0 {
                let style_ptr = bus.read_long(style_handle);
                if style_ptr != 0 {
                    let lh_handle = bus.read_long(style_ptr + Self::TE_STYLE_LH_TABLE_OFFSET);
                    if lh_handle != 0 {
                        let lh_ptr = Self::ensure_handle_capacity(
                            bus,
                            lh_handle,
                            ((line_count + 1) as u32) * Self::LH_ELEMENT_SIZE,
                        );
                        if lh_ptr != 0 {
                            for index in 0..=line_count {
                                let (line_height, font_ascent) = if uses_styled_runs
                                    && index < lines.len()
                                {
                                    let (start, end) = lines[index];
                                    Self::te_line_metrics_for_range(&style_runs, start, end)
                                } else if uses_styled_runs {
                                    Self::te_line_metrics_for_range(&style_runs, text_len, text_len)
                                } else {
                                    (line_height, font_ascent)
                                };
                                let element = lh_ptr + (index as u32 * Self::LH_ELEMENT_SIZE);
                                bus.write_word(
                                    element + Self::LH_ELEMENT_HEIGHT_OFFSET,
                                    line_height as u16,
                                );
                                bus.write_word(
                                    element + Self::LH_ELEMENT_ASCENT_OFFSET,
                                    font_ascent as u16,
                                );
                            }
                        }
                    }
                }
            }
        }

        if styled {
            Self::te_update_styled_run_sentinel(bus, te_handle, text_len);
        }
    }

    fn te_line_origin_x(just: i16, box_left: i16, box_right: i16, line_width: i16) -> i16 {
        // Per Inside Macintosh: Text 1993, p. 7320-7323 (and MPW
        // Universal Headers `TextEdit.h`):
        //   teJustLeft   =  0  (flush left — system default for LTR)
        //   teJustCenter =  1  (centered)
        //   teJustRight  = -1  (flush right)
        //   teForceLeft  = -2  (force flush left, overrides localised right-to-left)
        match just {
            1 => box_left + ((box_right - box_left) - line_width) / 2,
            -1 => box_right - line_width,
            _ => box_left.saturating_add(Self::TE_LINE_LEFT_INSET), // 0 / -2 / any other → flush left inside destRect
        }
    }

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

        let port_version = bus.read_word(port + 6);
        let is_color = (port_version & 0xC000) != 0;
        let (base, row_bytes, top, left, bottom, right) = if is_color {
            let pix_map_handle = bus.read_long(port + 2);
            if pix_map_handle == 0 {
                return false;
            }
            let pix_map_ptr = bus.read_long(pix_map_handle);
            if pix_map_ptr == 0 {
                return false;
            }
            (
                bus.read_long(pix_map_ptr),
                bus.read_word(pix_map_ptr + 4) & 0x3FFF,
                bus.read_word(pix_map_ptr + 6) as i16,
                bus.read_word(pix_map_ptr + 8) as i16,
                bus.read_word(pix_map_ptr + 10) as i16,
                bus.read_word(pix_map_ptr + 12) as i16,
            )
        } else {
            (
                bus.read_long(port + 2),
                bus.read_word(port + 6) & 0x3FFF,
                bus.read_word(port + 8) as i16,
                bus.read_word(port + 10) as i16,
                bus.read_word(port + 12) as i16,
                bus.read_word(port + 14) as i16,
            )
        };

        base != 0 && row_bytes != 0 && top < bottom && left < right
    }

    fn draw_te_contents(&mut self, cpu: &mut impl CpuOps, bus: &mut MacMemoryBus, te_handle: u32) {
        let te_ptr = Self::te_record_ptr(bus, te_handle);
        if te_ptr == 0 {
            return;
        }

        let text_bytes = Self::te_text_bytes(bus, te_handle);
        let dest_rect = Self::te_read_rect(bus, te_ptr + Self::TE_DEST_RECT_OFFSET);
        let view_rect = Self::te_read_rect(bus, te_ptr + Self::TE_VIEW_RECT_OFFSET);
        let te_port = bus.read_long(te_ptr + Self::TE_IN_PORT_OFFSET);
        if !Self::port_has_drawable_bitmap(bus, te_port) {
            return;
        }
        let just = bus.read_word(te_ptr + Self::TE_JUST_OFFSET) as i16;
        let (font, face, size, color, _, _) = self.te_primary_style(bus, te_handle);
        let styled_runs = if Self::te_is_styled_record(bus, te_ptr) {
            self.te_style_runs(bus, te_handle, text_bytes.len())
        } else {
            Vec::new()
        };
        let uses_styled_runs = !styled_runs.is_empty() && Self::te_is_styled_record(bus, te_ptr);
        let old_font = self.tx_font;
        let old_face = self.tx_face;
        let old_size = self.tx_size;
        let old_fg = self.fg_color;
        let old_loc = self.pn_loc;
        let previous_port = self.current_port;
        let previous_gdevice = self.current_gdevice;
        let switched_port = te_port != 0 && te_port != previous_port;

        // Always sync A5 globals to te_port before drawing, even if port didn't change.
        // draw_generic_shape reads from A5 globals, and untrack_window can set self.current_port
        // without updating A5 globals, causing a divergence that sends drawing to the wrong port.
        if te_port != 0 {
            self.set_current_port_state(bus, cpu, te_port, None);
        }
        if trace_textedit_enabled() {
            let (vis_top, vis_left, vis_bottom, vis_right) = {
                let vis_handle = bus.read_long(self.current_port + 24);
                let vis_ptr = if vis_handle != 0 {
                    bus.read_long(vis_handle)
                } else {
                    0
                };
                if vis_ptr != 0 {
                    (
                        bus.read_word(vis_ptr + 2) as i16,
                        bus.read_word(vis_ptr + 4) as i16,
                        bus.read_word(vis_ptr + 6) as i16,
                        bus.read_word(vis_ptr + 8) as i16,
                    )
                } else {
                    (0, 0, 0, 0)
                }
            };
            let (clip_top, clip_left, clip_bottom, clip_right) = {
                let clip_handle = bus.read_long(self.current_port + 28);
                let clip_ptr = if clip_handle != 0 {
                    bus.read_long(clip_handle)
                } else {
                    0
                };
                if clip_ptr != 0 {
                    (
                        bus.read_word(clip_ptr + 2) as i16,
                        bus.read_word(clip_ptr + 4) as i16,
                        bus.read_word(clip_ptr + 6) as i16,
                        bus.read_word(clip_ptr + 8) as i16,
                    )
                } else {
                    (0, 0, 0, 0)
                }
            };
            eprintln!(
                "[TE] draw_te_contents hTE=${:08X} te_port=${:08X} prev_port=${:08X} current_port=${:08X} switched={} gworld={} dest=({},{},{},{}) view=({},{},{},{}) vis=({},{},{},{}) clip=({},{},{},{})",
                te_handle,
                te_port,
                previous_port,
                self.current_port,
                switched_port,
                self.gworld_devices.contains_key(&te_port),
                dest_rect.0,
                dest_rect.1,
                dest_rect.2,
                dest_rect.3,
                view_rect.0,
                view_rect.1,
                view_rect.2,
                view_rect.3,
                vis_top,
                vis_left,
                vis_bottom,
                vis_right,
                clip_top,
                clip_left,
                clip_bottom,
                clip_right,
            );
        }

        let checksum_view_rect =
            |bus: &MacMemoryBus, port: u32, rect: (i16, i16, i16, i16)| -> u64 {
                if port == 0 {
                    return 0;
                }
                let port_version = bus.read_word(port + 6);
                let is_color = (port_version & 0xC000) != 0;
                let (pix_base, row_bytes, bounds_top, bounds_left, bounds_bottom, bounds_right) =
                    if is_color {
                        let pix_map_handle = bus.read_long(port + 2);
                        if pix_map_handle == 0 {
                            return 0;
                        }
                        let pix_map_ptr = bus.read_long(pix_map_handle);
                        if pix_map_ptr == 0 {
                            return 0;
                        }
                        (
                            bus.read_long(pix_map_ptr),
                            (bus.read_word(pix_map_ptr + 4) & 0x3FFF) as u32,
                            bus.read_word(pix_map_ptr + 6) as i16,
                            bus.read_word(pix_map_ptr + 8) as i16,
                            bus.read_word(pix_map_ptr + 10) as i16,
                            bus.read_word(pix_map_ptr + 12) as i16,
                        )
                    } else {
                        (
                            bus.read_long(port + 2),
                            (bus.read_word(port + 6) & 0x3FFF) as u32,
                            bus.read_word(port + 8) as i16,
                            bus.read_word(port + 10) as i16,
                            bus.read_word(port + 12) as i16,
                            bus.read_word(port + 14) as i16,
                        )
                    };
                let top = rect.0.max(bounds_top);
                let left = rect.1.max(bounds_left);
                let bottom = rect.2.min(bounds_bottom);
                let right = rect.3.min(bounds_right);
                if top >= bottom || left >= right || pix_base == 0 {
                    return 0;
                }
                let mut sum = 0u64;
                for y in top..bottom {
                    let dy = (y - bounds_top) as u32;
                    for x in left..right {
                        let dx = (x - bounds_left) as u32;
                        sum =
                            sum.wrapping_add(bus.read_byte(pix_base + dy * row_bytes + dx) as u64);
                    }
                }
                sum
            };
        let checksum_before = if trace_textedit_enabled() {
            checksum_view_rect(bus, self.current_port, view_rect)
        } else {
            0
        };

        self.tx_font = font;
        self.tx_face = face;
        self.tx_size = size;
        self.fg_color = color;

        let clip_top = view_rect.0;
        let clip_left = view_rect.1;
        let clip_bottom = view_rect.2;
        let clip_right = view_rect.3;
        let box_width = (dest_rect.3 - dest_rect.1).max(0);

        self.draw_rect(
            cpu,
            bus,
            &Rect {
                top: clip_top,
                left: clip_left,
                bottom: clip_bottom,
                right: clip_right,
            },
            ShapeOp::Erase,
        );
        let lines = if text_bytes.is_empty() {
            vec![(0usize, 0usize)]
        } else if uses_styled_runs {
            self.te_wrap_lines_styled(&styled_runs, &text_bytes, box_width)
        } else {
            self.te_wrap_lines(font, size, &text_bytes, box_width)
        };

        let mut top = dest_rect.0;
        let mut visible_lines = Vec::new();
        let mut visual_lines = Vec::new();
        for (index, (start, end)) in lines.iter().enumerate() {
            let line_height = Self::te_height_for_line(bus, te_handle, index);
            let line_ascent = Self::te_ascent_for_line(bus, te_handle, index);
            let line_bottom = top.saturating_add(line_height);
            if line_bottom <= clip_top {
                top = line_bottom;
                continue;
            }
            if top >= clip_bottom {
                break;
            }
            visible_lines.push(index);
            let mut trimmed_end = *end;
            while trimmed_end > *start
                && matches!(text_bytes[trimmed_end - 1], b' ' | b'\r' | b'\n')
            {
                trimmed_end -= 1;
            }
            let line_width = if uses_styled_runs {
                self.te_measure_text_width_styled(&styled_runs, &text_bytes, *start, trimmed_end)
            } else {
                self.te_measure_text_width(font, size, &text_bytes, *start, trimmed_end)
            };
            let x = Self::te_line_origin_x(just, dest_rect.1, dest_rect.3, line_width);
            visual_lines.push((*start, *end, top, line_bottom, x));
            self.pn_loc = (top.saturating_add(line_ascent), x);
            for (offset, &byte) in text_bytes[*start..trimmed_end].iter().enumerate() {
                if uses_styled_runs {
                    let style = Self::te_style_at_offset(&styled_runs, *start + offset);
                    self.tx_font = style.font;
                    self.tx_face = style.face;
                    self.tx_size = style.size;
                    self.fg_color = style.color;
                }
                self.draw_char(cpu, bus, byte as char);
            }
            top = line_bottom;
        }

        let te_active = bus.read_word(te_ptr + Self::TE_ACTIVE_OFFSET) != 0;
        let caret_visible = bus.read_word(te_ptr + Self::TE_CARET_STATE_OFFSET) == 0;
        let outline_hilite = self.te_feature_bit(te_handle, Self::TE_FEATURE_OUTLINE_HILITE);
        let sel_start = bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize;
        let sel_end = bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize;
        let (selection_start, selection_end) = if sel_start <= sel_end {
            (sel_start, sel_end)
        } else {
            (sel_end, sel_start)
        };
        if selection_start == selection_end {
            if te_active && caret_visible {
                if let Some(&(line_start, line_end, line_top, line_bottom, line_x)) = visual_lines
                    .iter()
                    .find(|&&(start, end, _, _, _)| {
                        selection_start >= start && selection_start <= end
                    })
                    .or_else(|| visual_lines.last())
                {
                    let caret_offset = selection_start.min(line_end).max(line_start);
                    let measured_width = if uses_styled_runs {
                        self.te_measure_text_width_styled(
                            &styled_runs,
                            &text_bytes,
                            line_start,
                            caret_offset,
                        )
                    } else {
                        self.te_measure_text_width(
                            font,
                            size,
                            &text_bytes,
                            line_start,
                            caret_offset,
                        )
                    };
                    let mut caret_x = line_x + measured_width;
                    if caret_offset > line_start {
                        caret_x = caret_x.saturating_sub(1);
                    }
                    let caret_width = self.ui_theme().text_theme().caret_width.max(1);
                    if !self.draw_theme_caret(
                        bus,
                        line_top,
                        caret_x,
                        line_bottom,
                        caret_x.saturating_add(caret_width),
                    ) {
                        self.draw_rect(
                            cpu,
                            bus,
                            &Rect {
                                top: line_top,
                                left: caret_x,
                                bottom: line_bottom,
                                right: caret_x.saturating_add(1),
                            },
                            ShapeOp::Paint,
                        );
                    }
                }
            }
        } else if te_active || outline_hilite {
            // IM:I I-375/I-385 and Text 1993 p. 2-80: an active edit
            // record displays its selection range by highlighting the
            // selected characters. Classic black-and-white TextEdit uses
            // inverse video; non-classic themes own their selection chrome.
            for &(line_start, line_end, line_top, line_bottom, line_x) in &visual_lines {
                let start = selection_start.max(line_start);
                let end = selection_end.min(line_end);
                if start >= end {
                    continue;
                }
                let mut selection_left = line_x
                    + if uses_styled_runs {
                        self.te_measure_text_width_styled(
                            &styled_runs,
                            &text_bytes,
                            line_start,
                            start,
                        )
                    } else {
                        self.te_measure_text_width(font, size, &text_bytes, line_start, start)
                    };
                let selection_right = line_x
                    + if uses_styled_runs {
                        self.te_measure_text_width_styled(
                            &styled_runs,
                            &text_bytes,
                            line_start,
                            end,
                        )
                    } else {
                        self.te_measure_text_width(font, size, &text_bytes, line_start, end)
                    };
                if start == line_start && matches!(just, 0 | -2) {
                    // BasiliskII/System 7.5.3 `a9d3_teupdate_selection_visual`
                    // shows a selection beginning at a left-justified visual
                    // line includes TextEdit's one-pixel left inset; glyphs
                    // still draw at destRect.left + TE_LINE_LEFT_INSET.
                    selection_left = selection_left.saturating_sub(Self::TE_LINE_LEFT_INSET);
                }
                if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
                    if te_active {
                        self.draw_rect(
                            cpu,
                            bus,
                            &Rect {
                                top: line_top,
                                left: selection_left,
                                bottom: line_bottom,
                                right: selection_right,
                            },
                            ShapeOp::Invert,
                        );
                    }
                } else {
                    // Text 1993, "Outline Highlighting" and TEFeatureFlag
                    // teFOutlineHilite: inactive selection ranges are framed,
                    // while active ranges keep normal highlighted selection chrome.
                    self.draw_theme_text_selection(
                        bus,
                        line_top,
                        selection_left,
                        line_bottom,
                        selection_right,
                        te_active,
                    );
                }
            }
        }

        self.tx_font = old_font;
        self.tx_face = old_face;
        self.tx_size = old_size;
        self.fg_color = old_fg;
        self.pn_loc = old_loc;
        if trace_textedit_enabled() {
            eprintln!(
                "[TE] draw_te_contents visible_lines hTE=${:08X} {:?}",
                te_handle, visible_lines
            );
            eprintln!(
                "[TE] draw_te_contents checksum hTE=${:08X} before={} after={}",
                te_handle,
                checksum_before,
                checksum_view_rect(bus, self.current_port, view_rect)
            );
        }

        if switched_port {
            self.set_current_port_state(bus, cpu, previous_port, Some(previous_gdevice));
        }
    }

    fn te_feature_bit(&self, te_handle: u32, feature: u16) -> bool {
        let mask = 1u16.checked_shl(feature as u32).unwrap_or(0);
        self.textedit_states
            .get(&te_handle)
            .map(|state| (state.feature_bits & mask) != 0)
            .unwrap_or(false)
    }

    /// Whether auto-scroll is enabled on the TextEdit record at
    /// `te_handle` (set by `TEAutoView` per IM:V V-173). Public
    /// projection over `textedit_states[te].feature_bits` so
    /// integration tests can observe TEAutoView's effect without
    /// access to the private feature-bit constants.
    pub fn te_auto_scroll_enabled(&self, te_handle: u32) -> bool {
        self.te_feature_bit(te_handle, Self::TE_FEATURE_AUTO_SCROLL)
    }

    fn set_te_feature_bit(&mut self, te_handle: u32, feature: u16, enabled: bool) {
        let mask = 1u16.checked_shl(feature as u32).unwrap_or(0);
        let state = self.textedit_states.entry(te_handle).or_default();
        if enabled {
            state.feature_bits |= mask;
        } else {
            state.feature_bits &= !mask;
        }
    }

    fn initialize_dialog_item_handles(
        &mut self,
        bus: &mut MacMemoryBus,
        dialog_ptr: u32,
        items: &[DialogItem],
    ) {
        self.initialize_dialog_item_handles_from(bus, dialog_ptr, items, 0);
    }

    fn initialize_dialog_item_handles_from(
        &mut self,
        bus: &mut MacMemoryBus,
        dialog_ptr: u32,
        items: &[DialogItem],
        start_index: usize,
    ) {
        // Item lists in memory hold live item handles or userItem ProcPtrs.
        // GetNewDialog reaches here with a copy of the DITL resource; NewDialog
        // initializes the caller-supplied item-list handle.
        // Inside Macintosh Volume I, I-405, I-412; MTE 1992 p. 6-114.
        for (index, item) in items.iter().enumerate().skip(start_index) {
            let item_no = index as i16 + 1;
            let Some(item_handle_addr) = Self::dialog_item_handle_addr(bus, dialog_ptr, item_no)
            else {
                continue;
            };

            let base_type = item.item_type & 0x7F;
            let item_handle = match base_type {
                8 | 16 => {
                    let text_bytes = item.text.as_bytes();
                    let handle = bus.alloc(4);
                    let text_ptr = if text_bytes.is_empty() {
                        0
                    } else {
                        let ptr = bus.alloc(text_bytes.len() as u32);
                        bus.write_bytes(ptr, text_bytes);
                        ptr
                    };
                    bus.write_long(handle, text_ptr);
                    self.dialog_item_handles.insert(handle, (dialog_ptr, index));
                    handle
                }
                32 => self
                    .find_resource_any(*b"ICON", item.resource_id)
                    .map(|(_, ptr)| {
                        self.get_or_create_resource_handle(bus, *b"ICON", item.resource_id, ptr)
                    })
                    .unwrap_or(0),
                4..=6 => {
                    let existing = bus.read_long(item_handle_addr);
                    if existing != 0 {
                        existing
                    } else {
                        self.create_standard_dialog_control_handle(bus, dialog_ptr, item_no, item)
                    }
                }
                7 => self
                    .find_resource_any(*b"CNTL", item.resource_id)
                    .map(|(_, cntl_ptr)| {
                        let value = bus.read_word(cntl_ptr + 8) as i16;
                        let vis_word = bus.read_word(cntl_ptr + 10);
                        let max = bus.read_word(cntl_ptr + 12) as i16;
                        let min = bus.read_word(cntl_ptr + 14) as i16;
                        let proc_id = bus.read_word(cntl_ptr + 16) as i16;
                        let ref_con = bus.read_long(cntl_ptr + 18);
                        let title_len = bus.read_byte(cntl_ptr + 22) as usize;
                        let title = bus.read_bytes(cntl_ptr + 23, title_len.min(255));
                        let ctrl_ptr = bus.alloc(296);
                        let handle = bus.alloc(4);
                        bus.write_long(handle, ctrl_ptr);
                        self.initialize_control_record(
                            bus,
                            ctrl_ptr,
                            dialog_ptr,
                            item.rect,
                            &title,
                            vis_word != 0,
                            value,
                            min,
                            max,
                            proc_id,
                            ref_con,
                        );
                        self.ensure_control_aux_record(bus, handle);
                        let old_head = bus.read_long(dialog_ptr + 140);
                        bus.write_long(ctrl_ptr, old_head);
                        bus.write_long(dialog_ptr + 140, handle);
                        self.dialog_control_handles
                            .insert(handle, (dialog_ptr, item_no));
                        self.dialog_control_values
                            .insert((dialog_ptr, item_no), value);
                        handle
                    })
                    .unwrap_or(0),
                64 => self
                    .find_resource_any(*b"PICT", item.resource_id)
                    .map(|(_, ptr)| {
                        self.get_or_create_resource_handle(bus, *b"PICT", item.resource_id, ptr)
                    })
                    .unwrap_or(0),
                0 => item.proc_ptr,
                _ => 0,
            };

            bus.write_long(item_handle_addr, item_handle);
        }
    }

    fn create_standard_dialog_control_handle(
        &mut self,
        bus: &mut MacMemoryBus,
        dialog_ptr: u32,
        item_no: i16,
        item: &DialogItem,
    ) -> u32 {
        let proc_id = match item.item_type & 0x7F {
            4 => 0, // btnCtrl -> pushButProc
            5 => 1, // chkCtrl -> checkBoxProc
            6 => 2, // radCtrl -> radioButProc
            _ => return 0,
        };
        let value = self
            .dialog_control_values
            .get(&(dialog_ptr, item_no))
            .copied()
            .unwrap_or(0);
        let ctrl_ptr = bus.alloc(296);
        let handle = bus.alloc(4);
        if ctrl_ptr == 0 || handle == 0 {
            return 0;
        }

        bus.write_long(handle, ctrl_ptr);
        self.initialize_control_record(
            bus,
            ctrl_ptr,
            dialog_ptr,
            item.rect,
            item.text.as_bytes(),
            true,
            value,
            0,
            1,
            proc_id,
            0,
        );
        self.ensure_control_aux_record(bus, handle);

        let old_head = bus.read_long(dialog_ptr + 140);
        bus.write_long(ctrl_ptr, old_head);
        bus.write_long(dialog_ptr + 140, handle);
        self.dialog_control_handles
            .insert(handle, (dialog_ptr, item_no));
        self.dialog_control_values
            .insert((dialog_ptr, item_no), value);
        handle
    }

    fn ditl_item_record_len_at(
        bus: &MacMemoryBus,
        ptr: u32,
        data_len: u32,
        record_offset: u32,
    ) -> Option<u32> {
        if record_offset + 14 > data_len {
            return None;
        }

        let item_type = bus.read_byte(ptr + record_offset + 12);
        let data_len_byte = bus.read_byte(ptr + record_offset + 13);
        let base_type = item_type & 0x7F;
        let payload_offset = record_offset + 14;
        let remaining = data_len.saturating_sub(payload_offset);
        let payload_len = Self::ditl_item_payload_len(base_type, data_len_byte, remaining)?;
        let padded = (payload_len + 1) & !1;
        if padded > remaining {
            return None;
        }

        Some(14 + padded)
    }

    fn ditl_used_len(bus: &MacMemoryBus, ptr: u32, data_len: u32) -> u32 {
        if ptr == 0 || data_len < 2 {
            return data_len;
        }

        let max_index = bus.read_word(ptr) as i16;
        let count = if max_index < 0 {
            0
        } else {
            max_index as usize + 1
        };
        let mut offset = 2u32;
        for _ in 0..count {
            let Some(record_len) = Self::ditl_item_record_len_at(bus, ptr, data_len, offset) else {
                break;
            };
            offset = offset.saturating_add(record_len);
        }

        offset.min(data_len)
    }

    fn dialog_local_size(bus: &MacMemoryBus, dialog_ptr: u32) -> (i16, i16) {
        let height = (bus.read_word(dialog_ptr + 20) as i16)
            .saturating_sub(bus.read_word(dialog_ptr + 16) as i16);
        let width = (bus.read_word(dialog_ptr + 22) as i16)
            .saturating_sub(bus.read_word(dialog_ptr + 18) as i16);
        (height, width)
    }

    fn append_ditl_offset(&self, bus: &MacMemoryBus, dialog_ptr: u32, method: i16) -> (i16, i16) {
        match method {
            // overlayDITL: appended rectangles are already dialog-local.
            // Macintosh Toolbox Essentials 1992, pp. 6-108, 6-153.
            0 => (0, 0),
            // appendDITLRight / appendDITLBottom position relative to the
            // dialog's upper-right or lower-left coordinate. The HLE updates
            // item rectangles here; full window resizing is handled by callers
            // that subsequently show/redraw the dialog.
            1 => {
                let (_, width) = Self::dialog_local_size(bus, dialog_ptr);
                (0, width)
            }
            2 => {
                let (height, _) = Self::dialog_local_size(bus, dialog_ptr);
                (height, 0)
            }
            item_method if item_method < 0 => {
                let item_no = item_method.checked_neg().unwrap_or(i16::MAX) as usize;
                self.dialog_items
                    .get(&dialog_ptr)
                    .and_then(|items| item_no.checked_sub(1).and_then(|index| items.get(index)))
                    .map(|item| (item.rect.0, item.rect.1))
                    .unwrap_or((0, 0))
            }
            _ => (0, 0),
        }
    }

    fn offset_dialog_item_rect(item: &mut DialogItem, v_delta: i16, h_delta: i16) {
        item.rect = (
            item.rect.0.saturating_add(v_delta),
            item.rect.1.saturating_add(h_delta),
            item.rect.2.saturating_add(v_delta),
            item.rect.3.saturating_add(h_delta),
        );
    }

    fn erase_retained_dialog_items_after_ditl_shorten(
        &mut self,
        bus: &mut MacMemoryBus,
        dialog_ptr: u32,
        removed_items: &[DialogItem],
    ) {
        if removed_items.is_empty() || dialog_ptr == 0 {
            return;
        }
        let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
        for item in removed_items {
            let (top, left, bottom, right) = Self::dialog_item_screen_rect(bounds, item.rect);
            self.fill_rect_clipped_to_dialog(
                bus,
                bounds,
                (top - 2, left - 2, bottom + 2, right + 2),
                false,
            );
        }
        if self.dialog_visible_snapshots.contains_key(&dialog_ptr) {
            let pixels = self.save_dialog_pixels(bus, bounds);
            self.dialog_visible_snapshots
                .insert(dialog_ptr, PersistentDialogSnapshot { bounds, pixels });
        }
        let tracking_bounds = self
            .dialog_tracking
            .as_ref()
            .filter(|tracking| tracking.dialog_ptr == dialog_ptr)
            .map(|tracking| tracking.bounds);
        if let Some(bounds) = tracking_bounds {
            let rendered = self.save_dialog_pixels(bus, bounds);
            if let Some(tracking) = self.dialog_tracking.as_mut() {
                if tracking.dialog_ptr == dialog_ptr {
                    tracking.rendered_pixels = rendered;
                    tracking.rendered_pixels_final = true;
                }
            }
        }
    }

    pub(crate) fn append_ditl_to_dialog(
        &mut self,
        bus: &mut MacMemoryBus,
        dialog_ptr: u32,
        ditl_handle: u32,
        method: i16,
    ) -> u16 {
        if dialog_ptr == 0 || ditl_handle == 0 {
            return self
                .dialog_items
                .get(&dialog_ptr)
                .map(|items| items.len() as u16)
                .unwrap_or(0);
        }

        let source_ptr = bus.read_long(ditl_handle);
        let Some(source_alloc_len) = bus.get_alloc_size(source_ptr) else {
            return self
                .dialog_items
                .get(&dialog_ptr)
                .map(|items| items.len() as u16)
                .unwrap_or(0);
        };
        let source_len = Self::ditl_used_len(bus, source_ptr, source_alloc_len);
        if source_len <= 2 {
            return self
                .dialog_items
                .get(&dialog_ptr)
                .map(|items| items.len() as u16)
                .unwrap_or(0);
        }

        let mut appended_items = Self::parse_ditl(bus, source_ptr, source_len);
        if appended_items.is_empty() {
            return self
                .dialog_items
                .get(&dialog_ptr)
                .map(|items| items.len() as u16)
                .unwrap_or(0);
        }

        let mut items_handle = bus.read_long(dialog_ptr + 156);
        if items_handle == 0 {
            items_handle = bus.alloc(4);
            let empty_ditl_ptr = bus.alloc(2);
            bus.write_word(empty_ditl_ptr, 0xFFFF);
            bus.write_long(items_handle, empty_ditl_ptr);
            bus.write_long(dialog_ptr + 156, items_handle);
        }

        let old_ptr = bus.read_long(items_handle);
        let old_alloc_len = bus.get_alloc_size(old_ptr).unwrap_or(0);
        let old_len = if old_ptr != 0 && old_alloc_len >= 2 {
            Self::ditl_used_len(bus, old_ptr, old_alloc_len).max(2)
        } else {
            2
        };
        let raw_existing_items = if old_ptr != 0 && old_alloc_len >= 2 {
            Self::parse_ditl(bus, old_ptr, old_len)
        } else {
            Vec::new()
        };
        let mut all_items = self
            .dialog_items
            .get(&dialog_ptr)
            .cloned()
            .unwrap_or_else(|| raw_existing_items.clone());
        if all_items.len() != raw_existing_items.len() && !raw_existing_items.is_empty() {
            all_items = raw_existing_items;
        }

        let start_index = all_items.len();
        let source_body_len = source_len.saturating_sub(2);
        let new_len = old_len.saturating_add(source_body_len);
        let new_ptr = bus.alloc(new_len);
        if new_ptr == 0 {
            return all_items.len() as u16;
        }

        if old_ptr != 0 && old_alloc_len >= 2 {
            let old_bytes = bus.read_bytes(old_ptr, old_len as usize);
            bus.write_bytes(new_ptr, &old_bytes);
        } else {
            bus.write_word(new_ptr, 0xFFFF);
        }
        let source_body = bus.read_bytes(source_ptr + 2, source_body_len as usize);
        bus.write_bytes(new_ptr + old_len, &source_body);

        let (v_delta, h_delta) = self.append_ditl_offset(bus, dialog_ptr, method);
        let mut source_offset = 2u32;
        let mut dest_offset = old_len;
        for item in &mut appended_items {
            if v_delta != 0 || h_delta != 0 {
                Self::offset_dialog_item_rect(item, v_delta, h_delta);
                bus.write_word(new_ptr + dest_offset + 4, item.rect.0 as u16);
                bus.write_word(new_ptr + dest_offset + 6, item.rect.1 as u16);
                bus.write_word(new_ptr + dest_offset + 8, item.rect.2 as u16);
                bus.write_word(new_ptr + dest_offset + 10, item.rect.3 as u16);
            }

            let Some(record_len) =
                Self::ditl_item_record_len_at(bus, source_ptr, source_len, source_offset)
            else {
                break;
            };
            source_offset = source_offset.saturating_add(record_len);
            dest_offset = dest_offset.saturating_add(record_len);
        }

        all_items.extend(appended_items);
        let new_count = all_items.len();
        let max_index = if new_count == 0 {
            0xFFFF
        } else {
            (new_count as u16).saturating_sub(1)
        };
        bus.write_word(new_ptr, max_index);
        bus.write_long(items_handle, new_ptr);

        self.dialog_items.insert(dialog_ptr, all_items.clone());
        self.initialize_dialog_item_handles_from(bus, dialog_ptr, &all_items, start_index);

        new_count as u16
    }

    pub(crate) fn shorten_ditl_in_dialog(
        &mut self,
        bus: &mut MacMemoryBus,
        dialog_ptr: u32,
        number_items: u16,
    ) -> u16 {
        if dialog_ptr == 0 {
            return 0;
        }

        let items_handle = bus.read_long(dialog_ptr + 156);
        let ditl_ptr = if items_handle != 0 {
            bus.read_long(items_handle)
        } else {
            0
        };
        let ditl_len = bus.get_alloc_size(ditl_ptr).unwrap_or(0);
        let raw_items = if ditl_ptr != 0 && ditl_len >= 2 {
            Self::parse_ditl(bus, ditl_ptr, Self::ditl_used_len(bus, ditl_ptr, ditl_len))
        } else {
            Vec::new()
        };
        let mut items = self
            .dialog_items
            .get(&dialog_ptr)
            .cloned()
            .unwrap_or(raw_items);
        let remove_count = (number_items as usize).min(items.len());
        let keep_count = items.len().saturating_sub(remove_count);

        for item_no in (keep_count + 1)..=items.len() {
            let item_no = item_no as i16;
            let handle = Self::dialog_item_handle(bus, dialog_ptr, item_no);
            self.dialog_control_values.remove(&(dialog_ptr, item_no));
            self.dialog_control_handles.remove(&handle);
            self.dialog_item_handles.remove(&handle);
        }

        let removed_items = items[keep_count..].to_vec();
        items.truncate(keep_count);
        if ditl_ptr != 0 && ditl_len >= 2 {
            let max_index = if keep_count == 0 {
                0xFFFF
            } else {
                (keep_count as u16).saturating_sub(1)
            };
            bus.write_word(ditl_ptr, max_index);
        }
        self.dialog_items.insert(dialog_ptr, items);
        self.erase_retained_dialog_items_after_ditl_shorten(bus, dialog_ptr, &removed_items);

        keep_count as u16
    }

    // ========== DLOG / DITL Resource Parsing ==========

    /// Parse a DLOG resource from guest memory.
    /// Returns (bounds, procID, visible, itemsID, title, position).
    /// Inside Macintosh Volume I, I-437
    /// Macintosh Toolbox Essentials 1992, p. 6-148
    fn parse_dlog(
        bus: &MacMemoryBus,
        ptr: u32,
        data_len: u32,
    ) -> ((i16, i16, i16, i16), i16, bool, i16, String, u16) {
        let top = bus.read_word(ptr) as i16;
        let left = bus.read_word(ptr + 2) as i16;
        let bottom = bus.read_word(ptr + 4) as i16;
        let right = bus.read_word(ptr + 6) as i16;
        let proc_id = bus.read_word(ptr + 8) as i16;
        let visible = bus.read_byte(ptr + 10) != 0;
        // +11: filler
        // +12: goAwayFlag (1 byte)
        // +13: filler
        let _ref_con = bus.read_long(ptr + 14);
        let items_id = bus.read_word(ptr + 18) as i16;
        // +20: title as Pascal string
        let title_len = bus.read_byte(ptr + 20) as usize;
        let mut title_bytes = vec![0u8; title_len];
        for (i, byte) in title_bytes.iter_mut().enumerate() {
            *byte = bus.read_byte(ptr + 21 + i as u32);
        }
        let title = decode_mac_roman_for_render(&title_bytes);

        // Read positioning constant after the title Pascal string.
        // Macintosh Toolbox Essentials 1992, pp. 4-125 to 4-126
        // The position word follows the title, padded to an even boundary.
        let title_end = 21 + title_len as u32;
        let padded_end = (title_end + 1) & !1;
        let position = if padded_end + 2 <= data_len {
            bus.read_word(ptr + padded_end)
        } else {
            0
        };

        (
            (top, left, bottom, right),
            proc_id,
            visible,
            items_id,
            title,
            position,
        )
    }

    /// Parse an ALRT resource from guest memory.
    /// Returns (bounds, itemsID, stages, position).
    /// Inside Macintosh Volume I, I-425 to I-426
    /// Macintosh Toolbox Essentials 1992, p. 6-150
    fn parse_alrt(
        bus: &MacMemoryBus,
        ptr: u32,
        data_len: u32,
    ) -> ((i16, i16, i16, i16), i16, u16, u16) {
        let bounds = if data_len >= 8 {
            (
                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,
            )
        } else {
            (0, 0, 0, 0)
        };
        let items_id = if data_len >= 10 {
            bus.read_word(ptr + 8) as i16
        } else {
            0
        };
        let stages = if data_len >= 12 {
            bus.read_word(ptr + 10)
        } else {
            0
        };
        // System 7 compiled ALRT resources append the same positioning
        // constants as DLOG resources after the 12-byte classic template.
        // Macintosh Toolbox Essentials 1992, p. 6-150
        let position = if data_len >= 14 {
            bus.read_word(ptr + 12)
        } else {
            0
        };
        (bounds, items_id, stages, position)
    }

    fn positioned_dialog_bounds(
        &self,
        mut bounds: (i16, i16, i16, i16),
        position: u16,
    ) -> (i16, i16, i16, i16) {
        match position {
            // alertPositionMainScreen / ParentWindow / ParentWindowScreen:
            // MTE 1992 p. 4-126 defines alert position as about one-fifth
            // of the unused screen/window space above the new window.
            0x300A | 0x700A | 0xB00A => {
                let (_, _, screen_w, screen_h, _) = self.get_screen_params();
                let dialog_w = bounds.3 - bounds.1;
                let dialog_h = bounds.2 - bounds.0;
                let new_left = (screen_w - dialog_w) / 2;
                let new_top = (screen_h - dialog_h) / 5;
                bounds = (new_top, new_left, new_top + dialog_h, new_left + dialog_w);
            }
            // centerMainScreen / centerParentWindow: true vertical center
            // Macintosh Toolbox Essentials 1992, p. 4-126
            0x280A | 0x680A | 0xA80A | 0x380A => {
                let (_, _, screen_w, screen_h, _) = self.get_screen_params();
                let dialog_w = bounds.3 - bounds.1;
                let dialog_h = bounds.2 - bounds.0;
                let new_left = (screen_w - dialog_w) / 2;
                let new_top = (screen_h - dialog_h) / 2;
                bounds = (new_top, new_left, new_top + dialog_h, new_left + dialog_w);
            }
            _ => {} // noAutoCenter (0x0000) or unknown: use raw bounds
        }
        bounds
    }

    fn alert_stage_default_item(stages: u16, stage_word: u16) -> (u32, u32, Option<i16>) {
        let stage_idx = (stage_word as u32).min(3);
        // Each stage occupies 4 bits, stage 1 in the low nibble.
        let nibble = ((stages as u32) >> (stage_idx * 4)) & 0xF;
        let box_drawn = (nibble & 0x04) != 0;
        let default_item = if (nibble & 0x08) == 0 { 1 } else { 2 };
        (stage_idx, nibble, box_drawn.then_some(default_item))
    }

    fn enabled_button_count(items: &[DialogItem]) -> usize {
        items
            .iter()
            .filter(|item| (item.item_type & 0x7F) == 4 && (item.item_type & 0x80) == 0)
            .count()
    }

    fn begin_interactive_alert<C: CpuOps>(
        &mut self,
        cpu: &mut C,
        bus: &mut MacMemoryBus,
        sp: u32,
        alert_id: i16,
        _filter_proc: u32,
        bounds: (i16, i16, i16, i16),
        items_id: i16,
        position: u16,
        default_item: i16,
    ) -> bool {
        let Some((_, ditl_data)) = self.find_resource_any(*b"DITL", items_id) else {
            return false;
        };
        let ditl_len = bus.get_alloc_size(ditl_data).unwrap_or(0);
        let items = Self::parse_ditl(bus, ditl_data, ditl_len);
        if Self::enabled_button_count(&items) <= 1 {
            return false;
        }

        let items_handle = {
            let handle = bus.alloc(4);
            bus.write_long(handle, ditl_data);
            Self::duplicate_handle_data(bus, handle)
        };
        let bounds = self.positioned_dialog_bounds(bounds, position);
        let dialog_ptr = self.finish_dialog_creation(
            bus,
            cpu,
            0,
            bounds,
            "",
            true,
            1,
            false,
            alert_id as u32,
            items_handle,
            items.clone(),
        );
        if dialog_ptr == 0 {
            return false;
        }
        bus.write_word(dialog_ptr + 168, default_item as u16);

        let saved_pixels = self
            .dialog_saved_pixels
            .get(&dialog_ptr)
            .cloned()
            .unwrap_or_else(|| self.save_dialog_pixels(bus, bounds));
        let rendered_pixels = self.save_dialog_pixels(bus, bounds);
        bus.write_word(sp + 6, 0);
        self.dialog_modal_entered.insert(dialog_ptr);
        self.dialog_tracking = Some(DialogTrackingState {
            dialog_ptr,
            bounds,
            title: String::new(),
            proc_id: 1,
            items,
            default_item,
            cancel_item: 0,
            edit_text: String::new(),
            edit_item: 0,
            saved_pixels,
            // Alert is a Pascal FUNCTION with a 6-byte argument frame
            // (filterProc + alertID) and a 2-byte result slot at SP+6. The
            // shared dialog completion code writes A7 as stack_ptr + 8, so
            // store SP-2 for alerts.
            stack_ptr: sp.wrapping_sub(2),
            item_hit_ptr: sp + 6,
            rendered_pixels,
            flash_remaining: 0,
            flash_delay: 0,
            flash_item: 0,
            edit_text_modified: false,
            draw_proc_queue: VecDeque::new(),
            draw_procs_done: true,
            rendered_pixels_final: true,
            filter_proc: 0,
            game_managed: false,
            last_filter_event: None,
            popup_draws: Vec::new(),
            active_popup: None,
            active_button: None,
            active_user_item: None,
        });
        true
    }

    fn finish_interactive_alert<C: CpuOps>(
        &mut self,
        cpu: &mut C,
        bus: &mut MacMemoryBus,
        hit: i16,
    ) {
        let Some(saved) = self.dialog_tracking.take() else {
            return;
        };
        if saved.item_hit_ptr != 0 {
            bus.write_word(saved.item_hit_ptr, hit as u16);
        }
        let stack_after = saved.stack_ptr + 8;
        let dialog_ptr = saved.dialog_ptr;
        self.dialog_saved_pixels
            .insert(dialog_ptr, saved.saved_pixels.clone());
        self.close_dialog_window(bus, cpu, dialog_ptr, true);
        cpu.write_reg(Register::A7, stack_after);
    }

    fn handle_interactive_alert_refire<C: CpuOps>(&mut self, cpu: &mut C, bus: &mut MacMemoryBus) {
        if self.dialog_tracking.is_none() {
            return;
        }

        if self
            .dialog_tracking
            .as_ref()
            .is_some_and(|tracking| tracking.active_button.is_some())
        {
            self.handle_dialog_button_tracking(bus);
            return;
        }

        if self
            .dialog_tracking
            .as_ref()
            .is_some_and(|tracking| tracking.flash_remaining > 0)
        {
            let (remaining, button_draw, finished_hit) = {
                let t = self.dialog_tracking.as_mut().unwrap();
                if t.flash_delay > 0 {
                    t.flash_delay -= 1;
                    return;
                }
                t.flash_remaining -= 1;
                t.flash_delay = 3;
                let remaining = t.flash_remaining;
                let flash_item = t.flash_item;
                let button_draw =
                    if remaining > 0 && flash_item > 0 && (flash_item as usize) <= t.items.len() {
                        let item = &t.items[(flash_item - 1) as usize];
                        Some((
                            Self::dialog_item_screen_rect(t.bounds, item.rect),
                            item.text.clone(),
                            flash_item == t.default_item,
                            remaining % 2 == 0,
                        ))
                    } else {
                        None
                    };
                (remaining, button_draw, flash_item)
            };

            if let Some((screen_rect, title, is_default, highlighted)) = button_draw {
                self.draw_dialog_button_highlight_state(
                    bus,
                    screen_rect,
                    &title,
                    is_default,
                    highlighted,
                );
            }
            if remaining == 0 {
                self.finish_interactive_alert(cpu, bus, finished_hit);
            }
            return;
        }

        let event = if !self.event_queue.is_empty() {
            let mut event = None;
            while let Some(e) = self.event_queue.pop_front() {
                match e.what {
                    1 | 2 | 3 | 6 => {
                        event = Some(e);
                        break;
                    }
                    _ => {}
                }
            }
            event
        } else {
            None
        };

        let Some(event) = event else {
            return;
        };

        match event.what {
            1 => {
                let (dialog_ptr, bounds, items, default_item) = {
                    let tracking = self.dialog_tracking.as_ref().unwrap();
                    (
                        tracking.dialog_ptr,
                        tracking.bounds,
                        tracking.items.clone(),
                        tracking.default_item,
                    )
                };
                let mut hit = self.dialog_item_hit_test(
                    bus,
                    &items,
                    bounds,
                    event.where_v,
                    event.where_h,
                    &self.dialog_popup_original_rects,
                    dialog_ptr,
                );
                if hit <= 0 {
                    hit =
                        Self::dialog_button_hit_test(&items, bounds, event.where_v, event.where_h);
                }
                if hit <= 0 {
                    return;
                }
                let item = &items[(hit - 1) as usize];
                let base_type = item.item_type & 0x7F;
                let is_disabled = (item.item_type & 0x80) != 0;
                if base_type != 4 || is_disabled {
                    return;
                }
                let rect = Self::dialog_item_screen_rect(bounds, item.rect);
                let is_default = hit == default_item;
                self.draw_dialog_button_highlight_state(bus, rect, &item.text, is_default, true);
                if self.mouse_button {
                    let tracking = self.dialog_tracking.as_mut().unwrap();
                    tracking.active_button = Some(super::dispatch::DialogButtonTrackingState {
                        item_no: hit,
                        rect: item.rect,
                        title: item.text.clone(),
                        is_default,
                        highlighted: true,
                    });
                } else {
                    self.start_dialog_button_flash(
                        bus, bounds, hit, item.rect, &item.text, is_default, true,
                    );
                }
            }
            3 => {
                let (default_item, cancel_item) = self
                    .dialog_tracking
                    .as_ref()
                    .map(|tracking| (tracking.default_item, tracking.cancel_item))
                    .unwrap_or((0, 0));
                let key_code = (event.message >> 8) as u16;
                let char_code = (event.message & 0xFF) as u8;
                let hit = if char_code == b'\r' || char_code == 0x03 {
                    default_item
                } else if char_code == 0x1B || key_code == 0x35 {
                    cancel_item
                } else {
                    0
                };
                if hit > 0 {
                    self.finish_interactive_alert(cpu, bus, hit);
                }
            }
            6 => {
                let Some(tracking) = self.dialog_tracking.as_ref() else {
                    return;
                };
                let dialog_ptr = tracking.dialog_ptr;
                let bounds = tracking.bounds;
                let items = tracking.items.clone();
                let default_item = tracking.default_item;
                self.begin_update_window(bus, dialog_ptr);
                self.set_current_port_state(bus, cpu, dialog_ptr, None);
                self.redraw_standard_dialog_items(
                    bus,
                    bounds,
                    &items,
                    default_item,
                    "",
                    0,
                    dialog_ptr,
                );
                let rendered = self.save_dialog_pixels(bus, bounds);
                if let Some(tracking) = self.dialog_tracking.as_mut() {
                    tracking.rendered_pixels = rendered;
                    tracking.rendered_pixels_final = true;
                }
                self.end_update_window(bus, dialog_ptr);
            }
            _ => {}
        }
    }

    /// Parse a DITL resource from guest memory into a list of DialogItems.
    /// Inside Macintosh Volume I, I-439
    fn parse_ditl(bus: &MacMemoryBus, ptr: u32, data_len: u32) -> Vec<DialogItem> {
        if data_len < 2 {
            return Vec::new();
        }
        let max_index = bus.read_word(ptr) as i16; // number of items minus 1
        let count = if max_index < 0 {
            0
        } else {
            max_index as usize + 1
        };
        let mut items = Vec::with_capacity(count);
        let mut offset = 2u32; // skip dlgMaxIndex

        for _ in 0..count {
            if offset + 14 > data_len {
                break;
            }
            // Read 4-byte handle/procPtr field.
            // For userItem types, the game may write a procedure pointer here
            // via SetDItem or direct memory manipulation.
            // Inside Macintosh Volume I, I-427
            let item_handle = bus.read_long(ptr + offset);
            offset += 4;
            // Read display rectangle
            let top = bus.read_word(ptr + offset) as i16;
            let left = bus.read_word(ptr + offset + 2) as i16;
            let bottom = bus.read_word(ptr + offset + 4) as i16;
            let right = bus.read_word(ptr + offset + 6) as i16;
            offset += 8;
            // Read type byte and data length
            let item_type = bus.read_byte(ptr + offset);
            let data_len_byte = bus.read_byte(ptr + offset + 1);
            offset += 2;

            let base_type = item_type & 0x7F; // strip itemDisable bit

            let mut text = String::new();
            let mut resource_id: i16 = 0;
            let remaining = data_len - offset;
            let Some(payload_len) =
                Self::ditl_item_payload_len(base_type, data_len_byte, remaining)
            else {
                break;
            };
            if matches!(base_type, 7 | 32 | 64) {
                resource_id = bus.read_word(ptr + offset) as i16;
            }

            let padded = (payload_len + 1) & !1;
            if padded > remaining {
                break;
            }

            if payload_len > 0 {
                match base_type {
                    // button (4), checkbox (5), radio (6), statText (8),
                    // editText (16): title/text data. IM:I I-427.
                    4 | 5 | 6 | 8 | 16 => {
                        let bytes = bus.read_bytes(ptr + offset, payload_len as usize);
                        text = decode_mac_roman_for_render(&bytes);
                    }
                    _ => {}
                }
            }

            // Advance past data, padded to even boundary
            offset += padded;

            // For userItems, itmhand is a ProcPtr, not a relocatable handle.
            // Dialog Manager preserves it in the duplicated DITL and calls it
            // during update/redraw. Some apps replace it later via SetDItem or
            // by writing the duplicated DITL directly.
            // Inside Macintosh Volume I, I-405, I-426 to I-427.
            let proc_ptr = if base_type == 0 { item_handle } else { 0 };

            items.push(DialogItem {
                item_type,
                rect: (top, left, bottom, right),
                text,
                resource_id,
                proc_ptr,
                sel_start: 0,
                sel_end: 0,
            });
        }
        items
    }

    fn ditl_item_payload_len(base_type: u8, data_len_byte: u8, remaining: u32) -> Option<u32> {
        let payload_len = match base_type {
            // userItem has only the byte after itmtype (reserved/empty),
            // not a following payload.
            0 => 0,
            // Help items use the byte after itmtype as a sized payload.
            // Macintosh Toolbox Essentials 1992, p. 6-154
            1 => u32::from(data_len_byte),
            // icon, picture, resCtrl: 2-byte resource ID. IM:I I-427
            // describes the byte after itmtype as length=2; MTE 1992
            // p. 6-153 documents the same compiled records as a
            // reserved byte plus the two-byte resource ID. Accept both.
            7 | 32 | 64 => {
                if remaining < 2 {
                    return None;
                }
                if data_len_byte >= 2 {
                    u32::from(data_len_byte)
                } else {
                    2
                }
            }
            _ => u32::from(data_len_byte),
        };
        Some(payload_len)
    }

    /// Re-read userItem proc pointers from the DITL data in guest memory.
    /// The game may write proc pointers directly to the DITL handle data
    /// after GetNewDialog returns, bypassing SetDItem.
    /// Inside Macintosh Volume I, I-427
    fn refresh_ditl_proc_ptrs(bus: &MacMemoryBus, dialog_ptr: u32, items: &mut [DialogItem]) {
        // Read the items handle from the DialogRecord (offset 156)
        let items_handle = bus.read_long(dialog_ptr + 156);
        if items_handle == 0 {
            return;
        }
        let ditl_ptr = bus.read_long(items_handle);
        if ditl_ptr == 0 {
            return;
        }

        // Walk the DITL data structure, reading the 4-byte itmhand field
        // for each item. Format: 2-byte count-1, then per item:
        //   4 bytes: itmhand (handle or proc pointer)
        //   8 bytes: itmr (Rect)
        //   1 byte: itmtype
        //   1 byte: itmlen
        //   itmlen bytes: data (padded to even)
        let mut offset = 2u32; // skip count word
        for item in items.iter_mut() {
            let handle = bus.read_long(ditl_ptr + offset);
            offset += 4; // itmhand
            offset += 8; // itmr (Rect)
            let _item_type = bus.read_byte(ditl_ptr + offset);
            let data_len = bus.read_byte(ditl_ptr + offset + 1) as u32;
            offset += 2; // itmtype + itmlen
            let padded = (data_len + 1) & !1;
            offset += padded;

            let base_type = item.item_type & 0x7F;
            if base_type == 0 {
                item.proc_ptr = handle;
            }
        }
    }

    fn duplicate_handle_data(bus: &mut MacMemoryBus, handle: u32) -> u32 {
        if handle == 0 {
            return 0;
        }

        let data_ptr = bus.read_long(handle);
        let new_handle = bus.alloc(4);
        if data_ptr == 0 {
            bus.write_long(new_handle, 0);
            return new_handle;
        }

        let size = bus.get_alloc_size(data_ptr).unwrap_or(0);
        let new_data_ptr = bus.alloc(size);
        for offset in 0..size {
            bus.write_byte(new_data_ptr + offset, bus.read_byte(data_ptr + offset));
        }
        bus.write_long(new_handle, new_data_ptr);
        new_handle
    }

    fn dispose_dialog_handle_storage(&mut self, bus: &mut MacMemoryBus, handle: u32) {
        if handle == 0 {
            return;
        }

        let resource_backing = self.loaded_handles.get(&handle).copied();
        self.detached_handles.remove(&handle);
        self.forget_resource_handle_index_for_handle(handle);
        self.loaded_handles.remove(&handle);
        self.resource_handle_files.remove(&handle);
        self.detached_handle_files.remove(&handle);
        self.handle_state_bits.remove(&handle);

        let data_ptr = bus.read_long(handle);
        if resource_backing.is_none() {
            bus.free(data_ptr);
        }
        bus.free(handle);
    }

    fn dispose_dialog_te_storage(&mut self, bus: &mut MacMemoryBus, te_handle: u32) {
        if te_handle == 0 {
            return;
        }

        let te_ptr = bus.read_long(te_handle);
        if te_ptr != 0 {
            let h_text = bus.read_long(te_ptr + Self::TE_HTEXT_OFFSET);
            self.dispose_dialog_handle_storage(bus, h_text);

            if Self::te_is_styled_record(bus, te_ptr) {
                let style_handle = bus.read_long(te_ptr + Self::TE_TX_FONT_OFFSET);
                self.dispose_dialog_handle_storage(bus, style_handle);
            }
            bus.free(te_ptr);
        }
        bus.free(te_handle);
        self.textedit_states.remove(&te_handle);
    }

    fn dispose_dialog_control_storage(&mut self, bus: &mut MacMemoryBus, ctrl_handle: u32) {
        if ctrl_handle == 0 {
            return;
        }

        let ctrl_ptr = bus.read_long(ctrl_handle);
        if ctrl_ptr != 0 {
            let owner = bus.read_long(ctrl_ptr + 4);
            if owner != 0 {
                let mut prev_handle = 0u32;
                let mut cur_handle = bus.read_long(owner + 140);
                while cur_handle != 0 {
                    let cur_ptr = bus.read_long(cur_handle);
                    if cur_ptr == 0 {
                        break;
                    }
                    if cur_handle == ctrl_handle {
                        let next = bus.read_long(cur_ptr);
                        if prev_handle == 0 {
                            bus.write_long(owner + 140, next);
                        } else {
                            let prev_ptr = bus.read_long(prev_handle);
                            if prev_ptr != 0 {
                                bus.write_long(prev_ptr, next);
                            }
                        }
                        break;
                    }
                    prev_handle = cur_handle;
                    cur_handle = bus.read_long(cur_ptr);
                }
            }
            self.release_control_aux_record(bus, ctrl_handle);
            self.control_proc_ids.remove(&ctrl_ptr);
            bus.free(ctrl_ptr);
        }
        bus.free(ctrl_handle);
    }

    fn dialog_item_storage_handles_from_ditl(
        bus: &MacMemoryBus,
        dialog_ptr: u32,
    ) -> Vec<(u8, u32)> {
        let items_handle = bus.read_long(dialog_ptr + 156);
        if items_handle == 0 {
            return Vec::new();
        }
        let ditl_ptr = bus.read_long(items_handle);
        if ditl_ptr == 0 {
            return Vec::new();
        }
        let data_len = bus.get_alloc_size(ditl_ptr).unwrap_or(0);
        if data_len < 2 {
            return Vec::new();
        }

        let max_index = bus.read_word(ditl_ptr) as i16;
        if max_index < 0 {
            return Vec::new();
        }

        let mut handles = Vec::new();
        let mut offset = 2u32;
        for _ in 0..=max_index {
            if offset + 14 > data_len {
                break;
            }

            let item_handle = bus.read_long(ditl_ptr + offset);
            offset += 12;
            let item_type = bus.read_byte(ditl_ptr + offset);
            let data_len_byte = bus.read_byte(ditl_ptr + offset + 1);
            offset += 2;

            let base_type = item_type & 0x7F;
            handles.push((base_type, item_handle));

            let payload_len = match base_type {
                0 => 0,
                7 | 32 | 64 if data_len_byte < 2 => 2,
                _ => u32::from(data_len_byte),
            };
            let padded = (payload_len + 1) & !1;
            if padded > data_len.saturating_sub(offset) {
                break;
            }
            offset += padded;
        }

        handles
    }

    fn clear_dialog_scoped_item_state(&mut self, dialog_ptr: u32) {
        self.dialog_items.remove(&dialog_ptr);
        self.dialog_item_handles
            .retain(|_, (dlg, _)| *dlg != dialog_ptr);
        self.dialog_control_handles
            .retain(|_, (dlg, _)| *dlg != dialog_ptr);
        self.dialog_control_values
            .retain(|(dlg, _), _| *dlg != dialog_ptr);
        self.dialog_edit_text_modified_items
            .retain(|(dlg, _)| *dlg != dialog_ptr);
        self.hidden_dialog_item_rects
            .retain(|(dlg, _), _| *dlg != dialog_ptr);
        self.dialog_item_popup_menus
            .retain(|(dlg, _), _| *dlg != dialog_ptr);
        self.dialog_popup_original_rects
            .retain(|(dlg, _), _| *dlg != dialog_ptr);
        self.dialog_popup_candidate_items
            .retain(|(dlg, _)| *dlg != dialog_ptr);
        if self
            .pending_dialog_popup_menu
            .is_some_and(|pending| pending.dialog_ptr == dialog_ptr)
        {
            self.pending_dialog_popup_menu = None;
        }
        self.dialog_cancel_items.remove(&dialog_ptr);
    }

    fn dispose_dialog_owned_items(&mut self, bus: &mut MacMemoryBus, dialog_ptr: u32) {
        let can_read_dialog_record =
            self.dialog_items.contains_key(&dialog_ptr) || self.window_list.contains(&dialog_ptr);
        let mut text_handles = Vec::new();
        let mut control_handles = Vec::new();

        if can_read_dialog_record {
            for (base_type, handle) in Self::dialog_item_storage_handles_from_ditl(bus, dialog_ptr)
            {
                match base_type {
                    8 | 16 => text_handles.push(handle),
                    4..=7 => control_handles.push(handle),
                    _ => {}
                }
            }
        }

        text_handles.extend(
            self.dialog_item_handles
                .iter()
                .filter_map(|(&handle, &(dlg, _))| {
                    if dlg == dialog_ptr {
                        Some(handle)
                    } else {
                        None
                    }
                }),
        );
        control_handles.extend(self.dialog_control_handles.iter().filter_map(
            |(&handle, &(dlg, _))| {
                if dlg == dialog_ptr {
                    Some(handle)
                } else {
                    None
                }
            },
        ));

        text_handles.sort_unstable();
        text_handles.dedup();
        control_handles.sort_unstable();
        control_handles.dedup();

        for handle in text_handles {
            self.dispose_dialog_handle_storage(bus, handle);
        }
        for handle in control_handles {
            self.dispose_dialog_control_storage(bus, handle);
        }

        if can_read_dialog_record {
            let text_h = bus.read_long(dialog_ptr + 160);
            self.dispose_dialog_te_storage(bus, text_h);
            bus.write_long(dialog_ptr + 160, 0);
        }

        self.clear_dialog_scoped_item_state(dialog_ptr);
    }

    fn dispose_dialog_record_and_item_list(&mut self, bus: &mut MacMemoryBus, dialog_ptr: u32) {
        if !self.dialog_items.contains_key(&dialog_ptr) && !self.window_list.contains(&dialog_ptr) {
            return;
        }

        let items_handle = bus.read_long(dialog_ptr + 156);
        self.dispose_dialog_handle_storage(bus, items_handle);
        bus.free(dialog_ptr);
    }

    pub(crate) fn dialog_screen_bounds(
        bus: &MacMemoryBus,
        dialog_ptr: u32,
    ) -> (i16, i16, i16, i16) {
        let port_height =
            bus.read_word(dialog_ptr + 20) as i16 - bus.read_word(dialog_ptr + 16) as i16;
        let port_width =
            bus.read_word(dialog_ptr + 22) as i16 - bus.read_word(dialog_ptr + 18) as i16;

        let port_version = bus.read_word(dialog_ptr + 6);
        let is_cgraf = (port_version & 0xC000) == 0xC000;
        let (top, left) = if is_cgraf {
            let pixmap_handle = bus.read_long(dialog_ptr + 2);
            let pixmap_ptr = if pixmap_handle != 0 {
                bus.read_long(pixmap_handle)
            } else {
                0
            };
            if pixmap_ptr != 0 {
                (
                    -(bus.read_word(pixmap_ptr + 6) as i16),
                    -(bus.read_word(pixmap_ptr + 8) as i16),
                )
            } else {
                (0, 0)
            }
        } else {
            (
                -(bus.read_word(dialog_ptr + 8) as i16),
                -(bus.read_word(dialog_ptr + 10) as i16),
            )
        };

        (top, left, top + port_height, left + port_width)
    }

    fn dialog_edit_state(
        bus: &MacMemoryBus,
        dialog_ptr: u32,
        items: &[DialogItem],
    ) -> (String, i16, i16) {
        let default_item = match bus.read_word(dialog_ptr + 168) as i16 {
            value if value > 0 => value,
            _ => 1,
        };

        let edit_field = bus.read_word(dialog_ptr + 164) as i16;
        let stored_edit_item = if edit_field >= 0 { edit_field + 1 } else { 0 };
        let stored_is_valid = stored_edit_item > 0
            && items
                .get((stored_edit_item - 1) as usize)
                .is_some_and(|item| (item.item_type & 0x7F) == 16 && (item.item_type & 0x80) == 0);
        let edit_item = if stored_is_valid {
            stored_edit_item
        } else {
            items
                .iter()
                .position(|item| (item.item_type & 0x7F) == 16 && (item.item_type & 0x80) == 0)
                .map(|idx| (idx + 1) as i16)
                .unwrap_or(0)
        };
        if edit_item <= 0 {
            return (String::new(), 0, default_item);
        }

        let edit_text = Self::text_item_string_from_handle_if_present(
            bus,
            Self::dialog_item_handle(bus, dialog_ptr, edit_item),
        );
        if let Some(edit_text) = edit_text {
            return (edit_text, edit_item, default_item);
        }

        let fallback = items
            .get((edit_item - 1) as usize)
            .map(|item| item.text.clone())
            .unwrap_or_default();
        (fallback, edit_item, default_item)
    }

    fn sync_tracking_active_edit_item(tracking: &mut DialogTrackingState) {
        let edit_item = tracking.edit_item;
        if edit_item <= 0 {
            return;
        }
        if let Some(item) = tracking.items.get_mut((edit_item - 1) as usize) {
            if (item.item_type & 0x7F) == 16 {
                item.text = tracking.edit_text.clone();
            }
        }
    }

    fn set_tracking_active_edit_selection(
        tracking: &mut DialogTrackingState,
        sel_start: usize,
        sel_end: usize,
    ) {
        let edit_item = tracking.edit_item;
        if edit_item <= 0 {
            return;
        }
        if let Some(item) = tracking.items.get_mut((edit_item - 1) as usize) {
            if (item.item_type & 0x7F) == 16 {
                let text_len = tracking.edit_text.len();
                item.sel_start = sel_start.min(text_len).min(i16::MAX as usize) as i16;
                item.sel_end = sel_end.min(text_len).min(i16::MAX as usize) as i16;
            }
        }
    }

    fn textedit_key_result(
        existing: &[u8],
        sel_start: usize,
        sel_end: usize,
        key: u8,
    ) -> (Vec<u8>, usize) {
        let text_len = existing.len();
        let s = sel_start.min(text_len);
        let e = sel_end.min(text_len);
        let (s, e) = if s > e { (e, s) } else { (s, e) };

        if key == 0x08 {
            if s != e {
                let mut merged = Vec::with_capacity(text_len - (e - s));
                merged.extend_from_slice(&existing[..s]);
                merged.extend_from_slice(&existing[e..]);
                return (merged, s);
            }
            if s > 0 {
                let mut merged = Vec::with_capacity(text_len - 1);
                merged.extend_from_slice(&existing[..s - 1]);
                merged.extend_from_slice(&existing[s..]);
                return (merged, s - 1);
            }
            return (existing.to_vec(), s);
        }

        let mut merged = Vec::with_capacity(s + 1 + text_len.saturating_sub(e));
        merged.extend_from_slice(&existing[..s]);
        merged.push(key);
        merged.extend_from_slice(&existing[e..]);
        (merged, s + 1)
    }

    fn apply_dialog_select_key_to_edit_item(
        &mut self,
        bus: &mut MacMemoryBus,
        dialog_ptr: u32,
        items: &mut [DialogItem],
        edit_item: i16,
        key: u8,
    ) -> bool {
        if edit_item <= 0 {
            return false;
        }
        let Some(item) = items.get_mut((edit_item - 1) as usize) else {
            return false;
        };
        let base_type = item.item_type & 0x7F;
        let is_disabled = (item.item_type & 0x80) != 0;
        if base_type != 16 || is_disabled {
            return false;
        }

        let item_handle = Self::dialog_item_handle(bus, dialog_ptr, edit_item);
        let existing = Self::text_item_bytes_from_handle_if_present(bus, item_handle)
            .unwrap_or_else(|| item.text.as_bytes().to_vec());
        let text_handle = bus.read_long(dialog_ptr + 160);
        let te_ptr = Self::te_record_ptr(bus, text_handle);
        let (sel_start, sel_end) = if te_ptr != 0 {
            (
                bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize,
                bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize,
            )
        } else {
            (item.sel_start.max(0) as usize, item.sel_end.max(0) as usize)
        };

        let (updated, insertion_point) =
            Self::textedit_key_result(&existing, sel_start, sel_end, key);
        let clamped_insertion = insertion_point.min(u16::MAX as usize) as u16;
        item.text = decode_mac_roman_for_render(&updated);
        item.sel_start = clamped_insertion as i16;
        item.sel_end = clamped_insertion as i16;

        if item_handle != 0 {
            let len = updated.len().min(255);
            let data_ptr = Self::ensure_text_handle_size(bus, item_handle, len);
            if data_ptr != 0 && len > 0 {
                bus.write_bytes(data_ptr, &updated[..len]);
            }
        }

        if te_ptr != 0 {
            self.te_set_text_contents(bus, text_handle, &updated);
            bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, clamped_insertion);
            bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, clamped_insertion);
        }

        true
    }

    fn activate_dialog_edit_item<C: CpuOps>(
        &mut self,
        bus: &mut MacMemoryBus,
        cpu: &mut C,
        dialog_ptr: u32,
        items: &[DialogItem],
        edit_item: i16,
    ) -> bool {
        if edit_item <= 0 {
            return false;
        }
        let Some(item) = items.get((edit_item - 1) as usize) else {
            return false;
        };
        if (item.item_type & 0x7F) != 16 || (item.item_type & 0x80) != 0 {
            return false;
        }

        bus.write_word(dialog_ptr + 164, (edit_item - 1) as u16);

        let text_handle = bus.read_long(dialog_ptr + 160);
        let te_ptr = Self::te_record_ptr(bus, text_handle);
        if te_ptr == 0 {
            return true;
        }

        let item_handle = Self::dialog_item_handle(bus, dialog_ptr, edit_item);
        let text = Self::text_item_bytes_from_handle_if_present(bus, item_handle)
            .unwrap_or_else(|| item.text.as_bytes().to_vec());
        self.te_set_text_contents(bus, text_handle, &text);

        let text_len = text.len().min(u16::MAX as usize);
        let sel_start = item.sel_start.max(0) as usize;
        let sel_end = item.sel_end.max(0) as usize;
        bus.write_word(
            te_ptr + Self::TE_SEL_START_OFFSET,
            sel_start.min(text_len) as u16,
        );
        bus.write_word(
            te_ptr + Self::TE_SEL_END_OFFSET,
            sel_end.min(text_len) as u16,
        );
        // MTE 1992 p. 6-139 and IM:I I-417: DialogSelect mouse-down in an
        // enabled editText item displays the insertion point or selection.
        // The Dialog Manager shares one TERecord across editText items, so
        // activating an item must make that record active before later null
        // events call TEIdle for caret blinking.
        bus.write_word(te_ptr + Self::TE_ACTIVE_OFFSET, 1);
        bus.write_long(te_ptr + Self::TE_CARET_TIME_OFFSET, self.tick_count);
        bus.write_word(te_ptr + Self::TE_CARET_STATE_OFFSET, 0);
        self.draw_te_contents(cpu, bus, text_handle);
        true
    }

    fn dialog_item_selection_range(item: &DialogItem, text_len: usize) -> Option<(usize, usize)> {
        let start = (item.sel_start.max(0) as usize).min(text_len);
        let end = (item.sel_end.max(0) as usize).min(text_len);
        let (start, end) = if start <= end {
            (start, end)
        } else {
            (end, start)
        };
        (start < end).then_some((start, end))
    }

    fn redraw_dialog_text_item(&mut self, bus: &mut MacMemoryBus, dialog_ptr: u32, item_no: i16) {
        if !self.window_visible(bus, dialog_ptr) {
            return;
        }
        let Some(items) = self.dialog_items.get(&dialog_ptr).cloned() else {
            return;
        };
        let Some(item) = items.get((item_no - 1) as usize) else {
            return;
        };
        let base_type = item.item_type & 0x7F;
        if base_type != 8 && base_type != 16 {
            return;
        }

        let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
        let (top, left, bottom, right) = bounds;
        let (it, il, ib, ir) = item.rect;
        let abs_top = top + it;
        let abs_left = left + il;
        let abs_bottom = top + ib;
        let abs_right = left + ir;
        if abs_top >= bottom || abs_bottom <= top || abs_left >= right || abs_right <= left {
            return;
        }

        let (edit_text, edit_item, default_item) = Self::dialog_edit_state(bus, dialog_ptr, &items);
        self.redraw_standard_dialog_items(
            bus,
            bounds,
            &items,
            default_item,
            &edit_text,
            edit_item,
            dialog_ptr,
        );

        if self.dialog_visible_snapshots.contains_key(&dialog_ptr) {
            let pixels = self.save_dialog_pixels(bus, bounds);
            self.dialog_visible_snapshots
                .insert(dialog_ptr, PersistentDialogSnapshot { bounds, pixels });
        }
        self.capture_gui_frame(
            bus,
            &format!("set_dialog_item_text_{:08X}_{}", dialog_ptr, item_no),
        );
    }

    fn flush_dialog_edit_item_texts(
        &mut self,
        bus: &mut MacMemoryBus,
        dialog_ptr: u32,
        items: &[DialogItem],
        active_edit_item: i16,
        active_edit_text: &str,
    ) {
        for (idx, item) in items.iter().enumerate() {
            if (item.item_type & 0x7F) != 16 {
                continue;
            }
            let item_no = (idx + 1) as i16;
            let text = if item_no == active_edit_item {
                active_edit_text
            } else {
                &item.text
            };
            let bytes = text.as_bytes();
            let len = bytes.len().min(255);
            let item_handle = Self::dialog_item_handle(bus, dialog_ptr, item_no);
            if item_handle != 0 {
                let data_ptr = Self::ensure_text_handle_size(bus, item_handle, len);
                if data_ptr != 0 && len > 0 {
                    bus.write_bytes(data_ptr, &bytes[..len]);
                }
            }
            if let Some(cached_items) = self.dialog_items.get_mut(&dialog_ptr) {
                if idx < cached_items.len() {
                    cached_items[idx].text = text.to_string();
                    cached_items[idx].sel_start = item.sel_start;
                    cached_items[idx].sel_end = item.sel_end;
                }
            }
        }
    }

    fn finish_dialog_creation<C: CpuOps>(
        &mut self,
        bus: &mut MacMemoryBus,
        cpu: &mut C,
        storage_ptr: u32,
        bounds: (i16, i16, i16, i16),
        title: &str,
        visible: bool,
        proc_id: i16,
        go_away_flag: bool,
        ref_con: u32,
        items_handle: u32,
        items: Vec<DialogItem>,
    ) -> u32 {
        let dlg_ptr = if storage_ptr != 0 {
            storage_ptr
        } else {
            // GetNewDialog/NewDialog allocate storage when dStorage is NIL
            // (IM:I I-424, I-412). System 7's Memory Manager zone layout
            // gives manager-owned records stable low-byte placement that
            // some 68K apps accidentally depend on; keep that shape local
            // to Dialog Manager records instead of changing all heap blocks.
            bus.alloc_aligned(170, MANAGER_DIALOG_RECORD_ALIGNMENT)
        };

        self.window_stack.push((
            self.front_window,
            self.window_bounds,
            self.window_proc_id,
            self.window_title.clone(),
        ));

        let screen_base: u32 = bus.read_long(0x0824);
        self.init_cgraf_window(
            bus,
            cpu,
            dlg_ptr,
            screen_base,
            bounds.0,
            bounds.1,
            bounds.2,
            bounds.3,
            title,
            proc_id,
            visible,
            false,
            go_away_flag,
            ref_con,
        );
        self.set_current_port_state(bus, cpu, dlg_ptr, None);

        // DialogRecord starts with a WindowRecord; +108 is windowKind,
        // not the WDEF procID. Dialog boxes and alerts must use
        // dialogKind=2. The WDEF procID is retained in window_proc_ids.
        // Inside Macintosh Volume I, I-407..I-408, I-273; Volume VI, 11-42.
        bus.write_word(dlg_ptr + 108, 2);
        bus.write_long(dlg_ptr + 156, items_handle);

        // SetDAFont / SetDialogFont affect subsequently created dialog and
        // alert grafPorts; assembly callers may set DlgFont directly.
        // Inside Macintosh Volume I, I-412; MTE 1992 p. 6-104.
        let dialog_font = bus.read_word(crate::memory::globals::addr::DLG_FONT) as i16;
        bus.write_word(dlg_ptr + 68, dialog_font as u16);
        self.tx_font = dialog_font;
        if let Some(state) = self.port_draw_states.get_mut(&dlg_ptr) {
            state.tx_font = dialog_font;
        }

        let text_h = Self::allocate_te_handle(bus);
        bus.write_long(dlg_ptr + 160, text_h);
        bus.write_word(dlg_ptr + 164, 0xFFFF); // editField = -1
        bus.write_word(dlg_ptr + 166, 0); // editOpen
        bus.write_word(dlg_ptr + 168, 1); // aDefItem

        self.initialize_dialog_item_handles(bus, dlg_ptr, &items);
        self.dialog_items.insert(dlg_ptr, items.clone());

        if visible {
            // NewDialog/GetNewDialog create and display visible dialogs
            // immediately. userItem contents remain app-owned and are skipped
            // until DrawDialog/ModalDialog/update callbacks can run, but the
            // standard shell, controls, text, and resource items must still be
            // visible. Inside Macintosh Volume I, I-405, I-412, I-421.
            let (edit_text, edit_item, default_item) =
                Self::dialog_edit_state(bus, dlg_ptr, &items);
            self.draw_dialog(
                bus,
                bounds,
                proc_id,
                title,
                &items,
                default_item,
                &edit_text,
                edit_item,
                false,
                dlg_ptr,
            );
            if !Self::dialog_is_game_managed(bounds, &items) {
                let pixels = self.save_dialog_pixels(bus, bounds);
                self.dialog_visible_snapshots
                    .insert(dlg_ptr, PersistentDialogSnapshot { bounds, pixels });
            }
            for item in &items {
                if Self::dialog_item_intersects_bounds(bounds, item) {
                    self.validate_window_rect(bus, dlg_ptr, item.rect);
                }
            }
            self.queue_window_update_event(dlg_ptr);
            self.capture_gui_frame(bus, &format!("get_new_dialog_initial_{:08X}", dlg_ptr));
        } else {
            self.dialog_initial_draw_deferred.remove(&dlg_ptr);
        }

        dlg_ptr
    }

    // ========== Dialog Drawing Helpers ==========

    /// Save framebuffer pixels under a dialog region (including shadow).
    /// Supports both 1bpp and 8bpp modes.
    pub(crate) fn save_dialog_pixels(
        &self,
        bus: &MacMemoryBus,
        rect: (i16, i16, i16, i16),
    ) -> Vec<u8> {
        let (screen_base, row_bytes, _, screen_h, pixel_size) = self.get_screen_params();
        let (top, left, bottom, right) = rect;
        let margin = Self::DBOX_FRAME_MARGIN;
        let save_top = top - margin; // dBoxProc structure region
        let save_left = left - margin;
        let save_bottom = bottom + margin;
        let save_right = right + margin;
        // Guard against negative y (top < 5) and y >= screen_h. `y as u32`
        // sign-extends a negative i16 to a huge value that overflows when
        // multiplied by row_bytes. Off-screen rows contribute zeros.
        let row_width = (save_right - save_left) as usize;
        let row_count = (save_bottom - save_top) as usize;
        let mut saved = Vec::new();
        let screen_h_i16 = screen_h;
        for y in save_top..save_bottom {
            let y_on_screen = y >= 0 && y < screen_h_i16;
            if pixel_size == 8 {
                let on_screen_row =
                    y_on_screen && save_left >= 0 && (save_right as u32) <= row_bytes;
                if on_screen_row {
                    // Pre-allocate capacity on first iteration.
                    if saved.capacity() == 0 {
                        saved.reserve(row_count * row_width);
                    }
                    let start = saved.len();
                    saved.resize(start + row_width, 0);
                    let row_addr = screen_base + (y as u32) * row_bytes + (save_left as u32);
                    bus.read_bytes_into(row_addr, &mut saved[start..start + row_width]);
                } else if y_on_screen {
                    for x in save_left..save_right {
                        if x < 0 || (x as u32) >= row_bytes {
                            saved.push(0);
                        } else {
                            let addr = screen_base + (y as u32) * row_bytes + (x as u32);
                            saved.push(bus.read_byte(addr));
                        }
                    }
                } else {
                    // Entire row off-screen — pad with zeros.
                    saved.resize(saved.len() + row_width, 0);
                }
            } else if y_on_screen {
                let byte_left = (save_left.max(0) as u32) / 8;
                let byte_right = (save_right as u32).div_ceil(8);
                let bx_end = byte_right.min(row_bytes);
                if byte_left < bx_end {
                    let len = (bx_end - byte_left) as usize;
                    let start = saved.len();
                    saved.resize(start + len, 0);
                    let row_addr = screen_base + (y as u32) * row_bytes + byte_left;
                    bus.read_bytes_into(row_addr, &mut saved[start..start + len]);
                }
            }
            // 1bpp off-screen row: silently produces nothing — the
            // byte_left < bx_end check + row_count-based saved.len tracking
            // tolerates short rows.
        }
        saved
    }

    pub(crate) fn ensure_dialog_background_saved(
        &mut self,
        bus: &MacMemoryBus,
        dialog_ptr: u32,
        bounds: (i16, i16, i16, i16),
    ) {
        if dialog_ptr == 0
            || !self.dialog_items.contains_key(&dialog_ptr)
            || self.dialog_saved_pixels.contains_key(&dialog_ptr)
        {
            return;
        }
        let background = self.save_dialog_pixels(bus, bounds);
        self.dialog_saved_pixels.insert(dialog_ptr, background);
    }

    pub(crate) fn ensure_dialog_background_saved_for_screen_port(
        &mut self,
        bus: &MacMemoryBus,
        port: u32,
    ) {
        if port == 0 || !self.dialog_items.contains_key(&port) {
            return;
        }
        let bounds = Self::dialog_screen_bounds(bus, port);
        self.ensure_dialog_background_saved(bus, port, bounds);
    }

    /// Restore previously saved framebuffer pixels under a dialog.
    pub(crate) fn restore_dialog_pixels(
        &self,
        bus: &mut MacMemoryBus,
        rect: (i16, i16, i16, i16),
        saved: &[u8],
    ) {
        let (screen_base, row_bytes, _, screen_h, pixel_size) = self.get_screen_params();
        let (top, left, bottom, right) = rect;
        let margin = Self::DBOX_FRAME_MARGIN;
        let save_top = top - margin;
        let save_left = left - margin;
        let save_bottom = bottom + margin;
        let save_right = right + margin;
        // Mirror save_dialog_pixels y-bounds guard — saved bytes for off-screen
        // rows are skipped so write position stays aligned with the packed input buffer.
        let row_width = (save_right - save_left) as usize;
        let mut idx = 0;
        let screen_h_i16 = screen_h;
        for y in save_top..save_bottom {
            let y_on_screen = y >= 0 && y < screen_h_i16;
            if pixel_size == 8 {
                let on_screen_row =
                    y_on_screen && save_left >= 0 && (save_right as u32) <= row_bytes;
                if on_screen_row && idx + row_width <= saved.len() {
                    let row_addr = screen_base + (y as u32) * row_bytes + (save_left as u32);
                    bus.write_bytes(row_addr, &saved[idx..idx + row_width]);
                    idx += row_width;
                } else if y_on_screen {
                    for x in save_left..save_right {
                        if idx < saved.len() {
                            if x >= 0 && (x as u32) < row_bytes {
                                let addr = screen_base + (y as u32) * row_bytes + (x as u32);
                                bus.write_byte(addr, saved[idx]);
                            }
                            idx += 1;
                        }
                    }
                } else {
                    // Off-screen row: skip the packed bytes that
                    // save_dialog_pixels padded in.
                    idx += row_width.min(saved.len() - idx);
                }
            } else if y_on_screen {
                let byte_left = (save_left.max(0) as u32) / 8;
                let byte_right = (save_right as u32).div_ceil(8);
                let bx_end = byte_right.min(row_bytes);
                if byte_left < bx_end {
                    let len = (bx_end - byte_left) as usize;
                    if idx + len <= saved.len() {
                        let row_addr = screen_base + (y as u32) * row_bytes + byte_left;
                        bus.write_bytes(row_addr, &saved[idx..idx + len]);
                        idx += len;
                    } else {
                        for bx in byte_left..bx_end {
                            if idx < saved.len() {
                                bus.write_byte(
                                    screen_base + (y as u32) * row_bytes + bx,
                                    saved[idx],
                                );
                                idx += 1;
                            }
                        }
                    }
                }
            }
            // 1bpp off-screen: save produced no bytes for this row,
            // so there's nothing to advance idx over here.
        }
    }

    fn restore_dialog_pixels_outside_rect(
        &self,
        bus: &mut MacMemoryBus,
        rect: (i16, i16, i16, i16),
        saved: &[u8],
        keep_rect: (i16, i16, i16, i16),
    ) {
        let (screen_base, row_bytes, _, screen_h, pixel_size) = self.get_screen_params();
        let (top, left, bottom, right) = rect;
        let margin = Self::DBOX_FRAME_MARGIN;
        let save_top = top - margin;
        let save_left = left - margin;
        let save_bottom = bottom + margin;
        let save_right = right + margin;
        let row_width = (save_right - save_left) as usize;
        let mut idx = 0;
        let screen_h_i16 = screen_h;
        for y in save_top..save_bottom {
            let y_on_screen = y >= 0 && y < screen_h_i16;
            if pixel_size == 8 {
                if y_on_screen {
                    for x in save_left..save_right {
                        if idx < saved.len() {
                            if (y < keep_rect.0
                                || y >= keep_rect.2
                                || x < keep_rect.1
                                || x >= keep_rect.3)
                                && x >= 0
                                && (x as u32) < row_bytes
                            {
                                let addr = screen_base + (y as u32) * row_bytes + (x as u32);
                                bus.write_byte(addr, saved[idx]);
                            }
                            idx += 1;
                        }
                    }
                } else {
                    idx += row_width.min(saved.len().saturating_sub(idx));
                }
            } else if y_on_screen {
                let byte_left = (save_left.max(0) as u32) / 8;
                let byte_right = (save_right as u32).div_ceil(8);
                let bx_end = byte_right.min(row_bytes);
                if byte_left < bx_end {
                    for bx in byte_left..bx_end {
                        if idx >= saved.len() {
                            break;
                        }
                        let mut restore_mask = 0u8;
                        for bit in 0..8u32 {
                            let x = (bx * 8 + bit) as i16;
                            if x >= save_left
                                && x < save_right
                                && (y < keep_rect.0
                                    || y >= keep_rect.2
                                    || x < keep_rect.1
                                    || x >= keep_rect.3)
                            {
                                restore_mask |= 0x80 >> bit;
                            }
                        }
                        if restore_mask != 0 {
                            let addr = screen_base + (y as u32) * row_bytes + bx;
                            let current = bus.read_byte(addr);
                            bus.write_byte(
                                addr,
                                (current & !restore_mask) | (saved[idx] & restore_mask),
                            );
                        }
                        idx += 1;
                    }
                }
            }
        }
    }

    /// Save framebuffer pixels for an exact rectangle (no margin).
    /// Guards off-screen y (y < 0 or y >= screen_h) from sign-extend overflow.
    /// Off-screen rows pad 8bpp output with zeros; 1bpp output is short by that row.
    fn save_rect_pixels(&self, bus: &MacMemoryBus, rect: (i16, i16, i16, i16)) -> Vec<u8> {
        let (screen_base, row_bytes, _, screen_h, pixel_size) = self.get_screen_params();
        let (top, left, bottom, right) = rect;
        let row_width = (right - left).max(0) as usize;
        let mut saved = Vec::with_capacity((bottom - top).max(0) as usize * row_width);
        let screen_h_i16 = screen_h;
        for y in top..bottom {
            let y_on_screen = y >= 0 && y < screen_h_i16;
            if pixel_size == 8 {
                let on_screen_row = y_on_screen && left >= 0 && (right as u32) <= row_bytes;
                if on_screen_row {
                    let row_addr = screen_base + (y as u32) * row_bytes + (left as u32);
                    saved.extend_from_slice(&bus.read_bytes(row_addr, row_width));
                } else if y_on_screen {
                    for x in left..right {
                        if x < 0 || (x as u32) >= row_bytes {
                            saved.push(0);
                        } else {
                            saved.push(
                                bus.read_byte(screen_base + (y as u32) * row_bytes + (x as u32)),
                            );
                        }
                    }
                } else {
                    saved.resize(saved.len() + row_width, 0);
                }
            } else if y_on_screen {
                let byte_left = (left.max(0) as u32) / 8;
                let byte_right = (right.max(0) as u32).div_ceil(8);
                let bx_end = byte_right.min(row_bytes);
                if byte_left < bx_end {
                    let len = (bx_end - byte_left) as usize;
                    let row_addr = screen_base + (y as u32) * row_bytes + byte_left;
                    saved.extend_from_slice(&bus.read_bytes(row_addr, len));
                }
            }
        }
        saved
    }

    fn saved_rect_has_non_background_content(&self, saved: &[u8]) -> bool {
        saved.iter().any(|&byte| byte != 0)
    }

    fn user_item_preserve_rects(
        &self,
        dialog_ptr: u32,
        bounds: (i16, i16, i16, i16),
        items: &[DialogItem],
        skip_popup_user_items: bool,
        skip_disabled_placeholders: bool,
    ) -> Vec<((i16, i16, i16, i16), bool)> {
        items
            .iter()
            .enumerate()
            .filter(|(i, it)| {
                (it.item_type & 0x7F) == 0
                    && (!skip_disabled_placeholders
                        || (it.item_type & 0x80) == 0
                        || it.proc_ptr != 0)
                    && (!skip_popup_user_items
                        || !self
                            .dialog_item_popup_menus
                            .contains_key(&(dialog_ptr, (i + 1) as i16)))
            })
            .map(|(_, it)| {
                let (it_t, it_l, it_b, it_r) = it.rect;
                let rect = (
                    bounds.0 + it_t,
                    bounds.1 + it_l,
                    bounds.0 + it_b,
                    bounds.1 + it_r,
                );
                (rect, it.item_type == 0)
            })
            .collect()
    }

    fn restore_user_item_preserved_pixels(
        &self,
        bus: &mut MacMemoryBus,
        rects: &[((i16, i16, i16, i16), bool)],
        backups: &[Vec<u8>],
    ) {
        for ((rect, always_restore), pixels) in rects.iter().zip(backups.iter()) {
            if *always_restore || self.saved_rect_has_non_background_content(pixels) {
                self.restore_rect_pixels(bus, *rect, pixels);
            }
        }
    }

    fn draw_dialog_preserving_user_items(
        &mut self,
        bus: &mut MacMemoryBus,
        bounds: (i16, i16, i16, i16),
        proc_id: i16,
        title: &str,
        items: &[DialogItem],
        default_item: i16,
        edit_text: &str,
        edit_item: i16,
        skip_pictures: bool,
        dialog_ptr: u32,
        preserve_user_items: bool,
        skip_popup_user_items: bool,
        skip_disabled_placeholders: bool,
    ) {
        let user_item_rects = if preserve_user_items {
            self.user_item_preserve_rects(
                dialog_ptr,
                bounds,
                items,
                skip_popup_user_items,
                skip_disabled_placeholders,
            )
        } else {
            Vec::new()
        };
        let user_item_backups: Vec<Vec<u8>> = user_item_rects
            .iter()
            .map(|&(r, _)| self.save_rect_pixels(bus, r))
            .collect();

        self.draw_dialog(
            bus,
            bounds,
            proc_id,
            title,
            items,
            default_item,
            edit_text,
            edit_item,
            skip_pictures,
            dialog_ptr,
        );

        self.restore_user_item_preserved_pixels(bus, &user_item_rects, &user_item_backups);
        self.capture_gui_frame(bus, &format!("draw_dialog_preserved_{:08X}", dialog_ptr));
    }

    fn fill_rect_clipped_to_dialog(
        &self,
        bus: &mut MacMemoryBus,
        bounds: (i16, i16, i16, i16),
        rect: (i16, i16, i16, i16),
        value: bool,
    ) {
        let top = rect.0.max(bounds.0);
        let left = rect.1.max(bounds.1);
        let bottom = rect.2.min(bounds.2);
        let right = rect.3.min(bounds.3);
        if top >= bottom || left >= right {
            return;
        }

        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
            self.get_screen_params();
        Self::fb_fill_rect(
            bus,
            screen_base,
            row_bytes,
            pixel_size,
            screen_width,
            screen_height,
            top,
            left,
            bottom,
            right,
            value,
        );
    }

    fn dialog_item_enclosing_local_rect(
        item_type: u8,
        rect: (i16, i16, i16, i16),
    ) -> (i16, i16, i16, i16) {
        match item_type & 0x7F {
            // IM:IV IV-59 notes that Dialog Manager drawing can extend
            // outside an editText item's display rectangle by 3 pixels.
            16 => (rect.0 - 3, rect.1 - 3, rect.2 + 3, rect.3 + 3),
            _ => rect,
        }
    }

    fn erase_dialog_item_enclosing_rect(
        &self,
        bus: &mut MacMemoryBus,
        dialog_ptr: u32,
        local_rect: (i16, i16, i16, i16),
    ) {
        let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
        let screen_rect = (
            bounds.0 + local_rect.0,
            bounds.1 + local_rect.1,
            bounds.0 + local_rect.2,
            bounds.1 + local_rect.3,
        );
        self.fill_rect_clipped_to_dialog(bus, bounds, screen_rect, false);
    }

    /// Restore framebuffer pixels to an exact rectangle (no margin).
    ///
    /// Mirrors save_rect_pixels off-screen guards so the idx position stays
    /// aligned with the packed input buffer.
    fn restore_rect_pixels(
        &self,
        bus: &mut MacMemoryBus,
        rect: (i16, i16, i16, i16),
        saved: &[u8],
    ) {
        let (screen_base, row_bytes, _, screen_h, pixel_size) = self.get_screen_params();
        let (top, left, bottom, right) = rect;
        let row_width = (right - left).max(0) as usize;
        let mut idx = 0;
        let screen_h_i16 = screen_h;
        for y in top..bottom {
            let y_on_screen = y >= 0 && y < screen_h_i16;
            if pixel_size == 8 {
                let on_screen_row = y_on_screen && left >= 0 && (right as u32) <= row_bytes;
                if on_screen_row && idx + row_width <= saved.len() {
                    let row_addr = screen_base + (y as u32) * row_bytes + (left as u32);
                    bus.write_bytes(row_addr, &saved[idx..idx + row_width]);
                    idx += row_width;
                } else if y_on_screen {
                    for x in left..right {
                        if idx < saved.len() {
                            if x >= 0 && (x as u32) < row_bytes {
                                bus.write_byte(
                                    screen_base + (y as u32) * row_bytes + (x as u32),
                                    saved[idx],
                                );
                            }
                            idx += 1;
                        }
                    }
                } else {
                    idx = (idx + row_width).min(saved.len());
                }
            } else if y_on_screen {
                let byte_left = (left.max(0) as u32) / 8;
                let byte_right = (right.max(0) as u32).div_ceil(8);
                let bx_end = byte_right.min(row_bytes);
                if byte_left < bx_end {
                    let len = (bx_end - byte_left) as usize;
                    if idx + len <= saved.len() {
                        let row_addr = screen_base + (y as u32) * row_bytes + byte_left;
                        bus.write_bytes(row_addr, &saved[idx..idx + len]);
                        idx += len;
                    } else {
                        for bx in byte_left..bx_end {
                            if idx < saved.len() {
                                bus.write_byte(
                                    screen_base + (y as u32) * row_bytes + bx,
                                    saved[idx],
                                );
                                idx += 1;
                            }
                        }
                    }
                }
            }
        }
    }

    /// Draw a complete dialog box with frame and all items.
    /// When `skip_pictures` is true, icon and picture items are skipped
    /// (used during redraw_chrome to avoid re-parsing PICTs every frame).
    pub(crate) fn draw_dialog(
        &mut self,
        bus: &mut MacMemoryBus,
        bounds: (i16, i16, i16, i16),
        proc_id: i16,
        title: &str,
        items: &[DialogItem],
        default_item: i16,
        edit_text: &str,
        edit_item: i16,
        skip_pictures: bool,
        dialog_ptr: u32,
    ) {
        self.ensure_dialog_background_saved(bus, dialog_ptr, bounds);
        self.dialog_initial_draw_deferred.remove(&dialog_ptr);

        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
            self.get_screen_params();
        let (top, left, bottom, right) = bounds;

        // Fill dialog background with white — but skip for game-managed
        // dialogs whose in-bounds items are userItems, since the game draws
        // its own background and the white fill would overwrite that content.
        let game_managed = Self::dialog_is_game_managed(bounds, items);
        if game_managed {
            eprintln!(
                "[DIALOG] Skipping white fill for game-managed dialog at ({},{},{},{}), {} items",
                top,
                left,
                bottom,
                right,
                items.len()
            );
        }
        let themed_frame = match proc_id {
            1 => (
                top - Self::DBOX_FRAME_MARGIN,
                left - Self::DBOX_FRAME_MARGIN,
                bottom + Self::DBOX_FRAME_MARGIN,
                right + Self::DBOX_FRAME_MARGIN,
            ),
            2 => (top - 1, left - 1, bottom + 1, right + 1),
            3 => (top - 1, left - 1, bottom + 3, right + 3),
            _ => (top, left, bottom + 2, right + 2),
        };
        if !self.draw_theme_dialog_frame(
            bus,
            (top, left, bottom, right),
            themed_frame,
            proc_id,
            true,
            !game_managed,
        ) {
            if !game_managed {
                Self::fb_fill_rect(
                    bus,
                    screen_base,
                    row_bytes,
                    pixel_size,
                    screen_width,
                    screen_height,
                    top,
                    left,
                    bottom,
                    right,
                    false,
                );
            }

            // Draw border
            // Inside Macintosh Volume I, I-299:
            // procID 0 = documentProc (title bar + border)
            // procID 1 = dBoxProc (double border, no title)
            // procID 2 = plainDBox (single border, no title)
            // procID 3 = altDBoxProc (shadow border, no title)
            // procID 4 = noGrowDocProc (title bar + border, no grow)
            match proc_id {
                1 => {
                    self.draw_classic_dbox_frame(bus, top, left, bottom, right);
                }
                2 => {
                    // plainDBox: single-pixel WDEF border outside the content
                    // bounds, matching the Window Manager procID 2 path.
                    // Inside Macintosh Volume I, I-275.
                    self.draw_rect_border(bus, top - 1, left - 1, bottom + 1, right + 1);
                }
                3 => {
                    // altDBoxProc: single border + shadow
                    self.draw_rect_border(bus, top - 1, left - 1, bottom + 1, right + 1);
                    self.draw_shadow(bus, top - 1, left - 1, bottom + 1, right + 1);
                }
                0 | 4 => {
                    // documentProc/noGrowDocProc use the Window Manager's
                    // document WDEF title-bar chrome. DrawDialog is allowed
                    // on modeless dialogs too (IM:I I-417; MTE 1992 p. 6-142),
                    // so route through the same WDEF frame path used by
                    // visible window creation instead of the modal-box fallback.
                    let saved_bounds = self.window_bounds;
                    let saved_proc_id = self.window_proc_id;
                    let saved_title = self.window_title.clone();
                    let saved_go_away = self.go_away_flag;
                    self.window_bounds = bounds;
                    self.window_proc_id = proc_id;
                    self.window_title = if title.is_empty() {
                        Self::dialog_window_title(bus, dialog_ptr)
                    } else {
                        title.to_string()
                    };
                    self.go_away_flag = dialog_ptr != 0 && bus.read_byte(dialog_ptr + 112) != 0;
                    self.draw_window_frame(bus);
                    self.window_bounds = saved_bounds;
                    self.window_proc_id = saved_proc_id;
                    self.window_title = saved_title;
                    self.go_away_flag = saved_go_away;
                }
                _ => {
                    // Default: single border + shadow
                    self.draw_rect_border(bus, top, left, bottom, right);
                    self.draw_shadow(bus, top, left, bottom, right);
                }
            }
        }
        // Optional per-iteration trace: gate SYSTEMLESS_TRACE_DIALOG_ITEMS=1
        // logs each item's type + relative rect + computed absolute rect.
        // Use to localize artifact-producing items in modal dialog rendering.
        let trace_items = trace_dialog_items_enabled();
        if trace_items {
            eprintln!(
                "[DLG] draw_dialog bounds=({},{},{},{}) proc_id={} edit_item={} default_item={} items={}",
                top,
                left,
                bottom,
                right,
                proc_id,
                edit_item,
                default_item,
                items.len()
            );
        }

        // IM:I I-407 and MTE 1992 p. 6-56: aDefItem/first-item default
        // controls Return/Enter semantics for modal dialogs, but ordinary
        // dialogs draw any bold default-button outline with an application
        // userItem. MTE 1992 glossary confirms Dialog Manager auto-draws
        // the bold outline for alerts, while applications draw it for
        // dialogs. DrawDialog therefore draws standard buttons without
        // synthesizing default-button chrome.
        let auto_default_outline = false;

        // Draw each item
        for (i, item) in items.iter().enumerate() {
            let item_num = (i + 1) as i16; // 1-based
            let (it, il, ib, ir) = item.rect;
            // Offset by dialog origin
            let abs_top = top + it;
            let abs_left = left + il;
            let abs_bottom = top + ib;
            let abs_right = left + ir;

            let base_type = item.item_type & 0x7F;
            if trace_items {
                eprintln!(
                    "[DLG]   item #{} type=0x{:02X} (base={}) rel=({},{},{},{}) abs=({},{},{},{}) rsrc={} text={:?}",
                    item_num,
                    item.item_type,
                    base_type,
                    it,
                    il,
                    ib,
                    ir,
                    abs_top,
                    abs_left,
                    abs_bottom,
                    abs_right,
                    item.resource_id,
                    item.text,
                );
            }

            // Generic "fully outside dialog" clip. Inside Macintosh
            // Volume I, I-309: dialog items are drawn through the dialog
            // window's port whose visRgn/clipRgn restrict drawing to the
            // dialog bounds. Real Mac OS QuickDraw clips item draws
            // automatically; Systemless's per-type handlers write to the
            // framebuffer directly with no clip. Items whose rect partially
            // extends beyond bounds are NOT clipped here — the per-type
            // handlers can refine if needed.
            if abs_top >= bottom || abs_bottom <= top || abs_left >= right || abs_right <= left {
                if trace_items {
                    eprintln!("[DLG]     -> skipped (fully outside dialog rect)");
                }
                continue;
            }

            // Buttons, checkboxes, radios, and resource-backed controls are
            // Control Manager controls. Draw them in a second pass below so
            // their z-order matches DrawControls: reverse creation order,
            // with the earliest-created control frontmost (IM:I I-322; MTE
            // 1992 pp. 5-87..5-88).
            if matches!(base_type, 4..=7) {
                continue;
            }

            let enabled = (item.item_type & 0x80) == 0;
            match base_type {
                // Button (ctrlItem + btnCtrl = 4)
                4 => {
                    // IM:I I-405: itemDisable stops Dialog Manager event
                    // reporting, while the standard System 7 control
                    // appearance is unchanged. Route the semantic state to
                    // non-classic theme chrome without changing metrics or
                    // existing hit handling.
                    self.draw_button_with_enabled(
                        bus,
                        abs_top,
                        abs_left,
                        abs_bottom,
                        abs_right,
                        &item.text,
                        auto_default_outline && item_num == default_item,
                        enabled,
                    );
                }
                // Checkbox (ctrlItem + chkCtrl = 5)
                5 => {
                    let checked = self
                        .dialog_control_values
                        .get(&(dialog_ptr, item_num))
                        .copied()
                        .unwrap_or(0)
                        != 0;
                    self.draw_checkbox_with_enabled(
                        bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, checked, enabled,
                    );
                }
                // Radio button (ctrlItem + radCtrl = 6)
                6 => {
                    let selected = self
                        .dialog_control_values
                        .get(&(dialog_ptr, item_num))
                        .copied()
                        .unwrap_or(0)
                        != 0;
                    self.draw_radio_with_enabled(
                        bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, selected,
                        enabled,
                    );
                }
                // Static text (8)
                8 => {
                    self.draw_static_text(
                        bus, abs_top, abs_left, abs_bottom, abs_right, &item.text,
                    );
                }
                // Edit text (16)
                16 => {
                    let display_text = if item_num == edit_item {
                        edit_text
                    } else {
                        &item.text
                    };
                    // MTE 1992 glossary "disabled item": disabled dialog
                    // items do not report user events. Route that semantic
                    // state to themed field chrome while leaving TextEdit
                    // metrics and existing hit handling unchanged.
                    let selection_range = if item_num == edit_item {
                        Self::dialog_item_selection_range(item, display_text.len())
                    } else {
                        None
                    };
                    self.draw_edit_text_with_cursor(
                        bus,
                        abs_top,
                        abs_left,
                        abs_bottom,
                        abs_right,
                        display_text,
                        selection_range,
                        false,
                        enabled,
                    );
                }
                // Icon (32)
                32 => {
                    if !skip_pictures && item.resource_id != 0 {
                        let drew_cicn = self
                            .find_resource_any(*b"cicn", item.resource_id)
                            .is_some_and(|(_, icon_ptr)| {
                                self.draw_cicn_icon(
                                    bus, abs_top, abs_left, abs_bottom, abs_right, icon_ptr,
                                )
                            });
                        if !drew_cicn {
                            if let Some((_, icon_ptr)) =
                                self.find_resource_any(*b"ICON", item.resource_id)
                            {
                                // ICON resource: 32x32 1-bit bitmap = 128 bytes
                                // Inside Macintosh Volume I, I-205
                                self.draw_icon(
                                    bus, abs_top, abs_left, abs_bottom, abs_right, icon_ptr,
                                );
                            }
                        }
                    }
                }
                // Picture (64)
                64 => {
                    // Items reaching here overlap the dialog rect (the
                    // fully-outside clip happens above the match).
                    if !skip_pictures && item.resource_id != 0 {
                        if let Some((_, pic_ptr)) =
                            self.find_resource_any(*b"PICT", item.resource_id)
                        {
                            // Draw PICT into the item's display rectangle
                            let device_ct_seed =
                                Self::ctab_seed(bus, self.current_gdevice_ctab_handle(bus))
                                    .unwrap_or(0);
                            super::pict::draw_picture(
                                bus,
                                pic_ptr,
                                abs_top,
                                abs_left,
                                abs_bottom,
                                abs_right,
                                self.screen_mode,
                                &self.device_clut,
                                device_ct_seed,
                                None,
                            );
                        }
                    }
                }
                // resCtrl — DITL item backed by a live CNTL resource.
                // Macintosh Toolbox Essentials 1992, 3-31.
                7 => {
                    if let Some(ctrl_handle) =
                        self.dialog_control_handle_for_item(dialog_ptr, item_num)
                    {
                        let ctrl_ptr = bus.read_long(ctrl_handle);
                        let proc_id_ctrl =
                            self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0);
                        let value = self
                            .dialog_control_values
                            .get(&(dialog_ptr, item_num))
                            .copied()
                            .unwrap_or_else(|| bus.read_word(ctrl_ptr + 18) as i16);
                        let min = bus.read_word(ctrl_ptr + 20) as i16;
                        let max = bus.read_word(ctrl_ptr + 22) as i16;
                        let hilite = bus.read_byte(ctrl_ptr + 17);
                        let title =
                            decode_mac_roman_for_render(&Self::control_title_bytes(bus, ctrl_ptr));

                        match proc_id_ctrl {
                            0 => self.draw_button_with_enabled(
                                bus,
                                abs_top,
                                abs_left,
                                abs_bottom,
                                abs_right,
                                &title,
                                auto_default_outline && item_num == default_item,
                                enabled,
                            ),
                            1 => self.draw_checkbox_with_enabled(
                                bus,
                                abs_top,
                                abs_left,
                                abs_bottom,
                                abs_right,
                                &title,
                                value != 0,
                                enabled,
                            ),
                            2 => self.draw_radio_with_enabled(
                                bus,
                                abs_top,
                                abs_left,
                                abs_bottom,
                                abs_right,
                                &title,
                                value != 0,
                                enabled,
                            ),
                            16 => self.draw_scroll_bar(
                                bus, abs_top, abs_left, abs_bottom, abs_right, value, min, max,
                                hilite,
                            ),
                            proc_id if Self::is_popup_menu_proc_id(proc_id) => {
                                let menu_id = self.popup_control_menu_id(bus, ctrl_ptr, min);
                                let selected = value.max(1) as usize;
                                let item_title = self.popup_menu_item_title(bus, menu_id, selected);
                                let (draw_top, draw_left, draw_bottom, draw_right) = self
                                    .popup_control_box_rect(
                                        bus, abs_top, abs_left, abs_bottom, abs_right, menu_id,
                                        max, proc_id,
                                    );
                                self.draw_popup_control_label(
                                    bus,
                                    abs_top,
                                    abs_left,
                                    abs_bottom,
                                    draw_left,
                                    &title,
                                    enabled && hilite != 255,
                                );
                                self.draw_popup_control_with_state(
                                    bus,
                                    draw_top,
                                    draw_left,
                                    draw_bottom,
                                    draw_right,
                                    &item_title.unwrap_or_default(),
                                    enabled && hilite != 255,
                                    hilite == 1,
                                );
                            }
                            _ => {
                                self.draw_button_with_enabled(
                                    bus,
                                    abs_top,
                                    abs_left,
                                    abs_bottom,
                                    abs_right,
                                    &title,
                                    auto_default_outline && item_num == default_item,
                                    enabled,
                                );
                            }
                        }
                    } else {
                        self.draw_button_with_enabled(
                            bus,
                            abs_top,
                            abs_left,
                            abs_bottom,
                            abs_right,
                            "",
                            auto_default_outline && item_num == default_item,
                            enabled,
                        );
                    }
                }
                // userItem (0) and unknown: skip
                _ => {}
            }
        }

        // Draw standard controls in reverse DITL/control creation order.
        // NewControl adds each control to the beginning of the window's
        // control list, and DrawControls draws that list in reverse so the
        // earliest-created controls appear frontmost on overlap (IM:I
        // I-319/I-322). Dialog Manager standard DITL controls use the same
        // Control Manager semantics.
        for (i, item) in items.iter().enumerate().rev() {
            let item_num = (i + 1) as i16;
            let (it, il, ib, ir) = item.rect;
            let abs_top = top + it;
            let abs_left = left + il;
            let abs_bottom = top + ib;
            let abs_right = left + ir;
            let base_type = item.item_type & 0x7F;
            if !matches!(base_type, 4..=7) {
                continue;
            }
            if abs_top >= bottom || abs_bottom <= top || abs_left >= right || abs_right <= left {
                continue;
            }

            let enabled = (item.item_type & 0x80) == 0;
            if trace_items {
                if let Some(ctrl_handle) = self.dialog_control_handle_for_item(dialog_ptr, item_num)
                {
                    let ctrl_ptr = bus.read_long(ctrl_handle);
                    let proc_id_ctrl = self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0);
                    eprintln!(
                        "[DLG]     item={} control handle=${:08X} ptr=${:08X} visible={} hilite={} value={} min={} max={} proc_id={}",
                        item_num,
                        ctrl_handle,
                        ctrl_ptr,
                        ctrl_ptr != 0 && bus.read_byte(ctrl_ptr + 16) != 0,
                        if ctrl_ptr == 0 {
                            -1
                        } else {
                            bus.read_byte(ctrl_ptr + 17) as i16
                        },
                        if ctrl_ptr == 0 {
                            0
                        } else {
                            bus.read_word(ctrl_ptr + 18) as i16
                        },
                        if ctrl_ptr == 0 {
                            0
                        } else {
                            bus.read_word(ctrl_ptr + 20) as i16
                        },
                        if ctrl_ptr == 0 {
                            0
                        } else {
                            bus.read_word(ctrl_ptr + 22) as i16
                        },
                        proc_id_ctrl,
                    );
                } else {
                    eprintln!("[DLG]     item={} control handle=<none>", item_num);
                }
            }
            match base_type {
                4 => {
                    self.draw_button_with_enabled(
                        bus,
                        abs_top,
                        abs_left,
                        abs_bottom,
                        abs_right,
                        &item.text,
                        auto_default_outline && item_num == default_item,
                        enabled,
                    );
                }
                5 => {
                    let inactive = self.dialog_control_inactive(bus, dialog_ptr, item_num);
                    let checked = self
                        .dialog_control_values
                        .get(&(dialog_ptr, item_num))
                        .copied()
                        .unwrap_or(0)
                        != 0;
                    self.draw_checkbox_with_enabled_and_inactive(
                        bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, checked,
                        enabled, inactive,
                    );
                }
                6 => {
                    let inactive = self.dialog_control_inactive(bus, dialog_ptr, item_num);
                    let selected = self
                        .dialog_control_values
                        .get(&(dialog_ptr, item_num))
                        .copied()
                        .unwrap_or(0)
                        != 0;
                    self.draw_radio_with_enabled_and_inactive(
                        bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, selected,
                        enabled, inactive,
                    );
                }
                7 => {
                    if let Some(ctrl_handle) =
                        self.dialog_control_handle_for_item(dialog_ptr, item_num)
                    {
                        let ctrl_ptr = bus.read_long(ctrl_handle);
                        let proc_id_ctrl =
                            self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0);
                        let value = self
                            .dialog_control_values
                            .get(&(dialog_ptr, item_num))
                            .copied()
                            .unwrap_or_else(|| bus.read_word(ctrl_ptr + 18) as i16);
                        let min = bus.read_word(ctrl_ptr + 20) as i16;
                        let max = bus.read_word(ctrl_ptr + 22) as i16;
                        let hilite = bus.read_byte(ctrl_ptr + 17);
                        let title =
                            decode_mac_roman_for_render(&Self::control_title_bytes(bus, ctrl_ptr));

                        match proc_id_ctrl {
                            0 => self.draw_button_with_enabled(
                                bus,
                                abs_top,
                                abs_left,
                                abs_bottom,
                                abs_right,
                                &title,
                                auto_default_outline && item_num == default_item,
                                enabled,
                            ),
                            1 => self.draw_checkbox_with_enabled_and_inactive(
                                bus,
                                abs_top,
                                abs_left,
                                abs_bottom,
                                abs_right,
                                &title,
                                value != 0,
                                enabled,
                                hilite == 255,
                            ),
                            2 => self.draw_radio_with_enabled_and_inactive(
                                bus,
                                abs_top,
                                abs_left,
                                abs_bottom,
                                abs_right,
                                &title,
                                value != 0,
                                enabled,
                                hilite == 255,
                            ),
                            16 => self.draw_scroll_bar(
                                bus, abs_top, abs_left, abs_bottom, abs_right, value, min, max,
                                hilite,
                            ),
                            proc_id if Self::is_popup_menu_proc_id(proc_id) => {
                                let menu_id = self.popup_control_menu_id(bus, ctrl_ptr, min);
                                let selected = value.max(1) as usize;
                                let item_title = self.popup_menu_item_title(bus, menu_id, selected);
                                let (draw_top, draw_left, draw_bottom, draw_right) = self
                                    .popup_control_box_rect(
                                        bus, abs_top, abs_left, abs_bottom, abs_right, menu_id,
                                        max, proc_id,
                                    );
                                self.draw_popup_control_label(
                                    bus,
                                    abs_top,
                                    abs_left,
                                    abs_bottom,
                                    draw_left,
                                    &title,
                                    enabled && hilite != 255,
                                );
                                self.draw_popup_control_with_state(
                                    bus,
                                    draw_top,
                                    draw_left,
                                    draw_bottom,
                                    draw_right,
                                    &item_title.unwrap_or_default(),
                                    enabled && hilite != 255,
                                    hilite == 1,
                                );
                            }
                            _ => {
                                self.draw_button_with_enabled(
                                    bus,
                                    abs_top,
                                    abs_left,
                                    abs_bottom,
                                    abs_right,
                                    &title,
                                    auto_default_outline && item_num == default_item,
                                    enabled,
                                );
                            }
                        }
                    } else {
                        self.draw_button_with_enabled(
                            bus,
                            abs_top,
                            abs_left,
                            abs_bottom,
                            abs_right,
                            "",
                            auto_default_outline && item_num == default_item,
                            enabled,
                        );
                    }
                }
                _ => {}
            }
        }
        self.capture_gui_frame(bus, &format!("draw_dialog_{:08X}", dialog_ptr));
    }

    fn redraw_standard_dialog_items(
        &self,
        bus: &mut MacMemoryBus,
        bounds: (i16, i16, i16, i16),
        items: &[DialogItem],
        default_item: i16,
        edit_text: &str,
        edit_item: i16,
        dialog_ptr: u32,
    ) {
        let (top, left, bottom, right) = bounds;
        // Ordinary dialog default outlines are application-owned userItem
        // drawing, not synthesized by DrawDialog. See the DrawDialog path
        // above for the IM:I/MTE references.
        let auto_default_outline = false;
        for (i, item) in items.iter().enumerate() {
            let item_num = (i + 1) as i16;
            let (it, il, ib, ir) = item.rect;
            let abs_top = top + it;
            let abs_left = left + il;
            let abs_bottom = top + ib;
            let abs_right = left + ir;
            if abs_top >= bottom || abs_bottom <= top || abs_left >= right || abs_right <= left {
                continue;
            }

            let enabled = (item.item_type & 0x80) == 0;
            match item.item_type & 0x7F {
                4 => self.draw_button_with_enabled(
                    bus,
                    abs_top,
                    abs_left,
                    abs_bottom,
                    abs_right,
                    &item.text,
                    auto_default_outline && item_num == default_item,
                    enabled,
                ),
                5 => {
                    let inactive = self.dialog_control_inactive(bus, dialog_ptr, item_num);
                    let checked = self
                        .dialog_control_values
                        .get(&(dialog_ptr, item_num))
                        .copied()
                        .unwrap_or(0)
                        != 0;
                    self.fill_rect_clipped_to_dialog(
                        bus,
                        bounds,
                        (abs_top, abs_left, abs_bottom, abs_right),
                        false,
                    );
                    self.draw_checkbox_with_enabled_and_inactive(
                        bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, checked,
                        enabled, inactive,
                    );
                }
                6 => {
                    let inactive = self.dialog_control_inactive(bus, dialog_ptr, item_num);
                    self.fill_rect_clipped_to_dialog(
                        bus,
                        bounds,
                        (abs_top, abs_left, abs_bottom, abs_right),
                        false,
                    );
                    self.draw_radio_with_enabled_and_inactive(
                        bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, false, enabled,
                        inactive,
                    );
                }
                7 => {
                    if let Some(ctrl_handle) =
                        self.dialog_control_handle_for_item(dialog_ptr, item_num)
                    {
                        let ctrl_ptr = bus.read_long(ctrl_handle);
                        let proc_id_ctrl =
                            self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0);
                        let value = self
                            .dialog_control_values
                            .get(&(dialog_ptr, item_num))
                            .copied()
                            .unwrap_or_else(|| bus.read_word(ctrl_ptr + 18) as i16);
                        let min = bus.read_word(ctrl_ptr + 20) as i16;
                        let max = bus.read_word(ctrl_ptr + 22) as i16;
                        let hilite = bus.read_byte(ctrl_ptr + 17);
                        let title =
                            decode_mac_roman_for_render(&Self::control_title_bytes(bus, ctrl_ptr));
                        match proc_id_ctrl {
                            0 => self.draw_button_with_enabled(
                                bus,
                                abs_top,
                                abs_left,
                                abs_bottom,
                                abs_right,
                                &title,
                                auto_default_outline && item_num == default_item,
                                enabled,
                            ),
                            1 => {
                                self.fill_rect_clipped_to_dialog(
                                    bus,
                                    bounds,
                                    (abs_top, abs_left, abs_bottom, abs_right),
                                    false,
                                );
                                self.draw_checkbox_with_enabled_and_inactive(
                                    bus,
                                    abs_top,
                                    abs_left,
                                    abs_bottom,
                                    abs_right,
                                    &title,
                                    value != 0,
                                    enabled,
                                    hilite == 255,
                                )
                            }
                            2 => {
                                self.fill_rect_clipped_to_dialog(
                                    bus,
                                    bounds,
                                    (abs_top, abs_left, abs_bottom, abs_right),
                                    false,
                                );
                                self.draw_radio_with_enabled_and_inactive(
                                    bus,
                                    abs_top,
                                    abs_left,
                                    abs_bottom,
                                    abs_right,
                                    &title,
                                    value != 0,
                                    enabled,
                                    hilite == 255,
                                )
                            }
                            16 => self.draw_scroll_bar(
                                bus, abs_top, abs_left, abs_bottom, abs_right, value, min, max,
                                hilite,
                            ),
                            proc_id if Self::is_popup_menu_proc_id(proc_id) => {
                                let menu_id = self.popup_control_menu_id(bus, ctrl_ptr, min);
                                let selected = value.max(1) as usize;
                                let item_title = self.popup_menu_item_title(bus, menu_id, selected);
                                let (draw_top, draw_left, draw_bottom, draw_right) = self
                                    .popup_control_box_rect(
                                        bus, abs_top, abs_left, abs_bottom, abs_right, menu_id,
                                        max, proc_id,
                                    );
                                self.draw_popup_control_label(
                                    bus,
                                    abs_top,
                                    abs_left,
                                    abs_bottom,
                                    draw_left,
                                    &title,
                                    enabled && hilite != 255,
                                );
                                self.draw_popup_control_with_state(
                                    bus,
                                    draw_top,
                                    draw_left,
                                    draw_bottom,
                                    draw_right,
                                    &item_title.unwrap_or_default(),
                                    enabled && hilite != 255,
                                    hilite == 1,
                                );
                            }
                            _ => self.draw_button_with_enabled(
                                bus,
                                abs_top,
                                abs_left,
                                abs_bottom,
                                abs_right,
                                &title,
                                auto_default_outline && item_num == default_item,
                                enabled,
                            ),
                        }
                    } else {
                        self.draw_button_with_enabled(
                            bus,
                            abs_top,
                            abs_left,
                            abs_bottom,
                            abs_right,
                            "",
                            auto_default_outline && item_num == default_item,
                            enabled,
                        );
                    }
                }
                8 => {
                    self.fill_rect_clipped_to_dialog(
                        bus,
                        bounds,
                        (abs_top, abs_left, abs_bottom, abs_right),
                        false,
                    );
                    self.draw_static_text(bus, abs_top, abs_left, abs_bottom, abs_right, &item.text)
                }
                16 => {
                    let display_text = if item_num == edit_item {
                        edit_text
                    } else {
                        &item.text
                    };
                    let selection_range = if item_num == edit_item {
                        Self::dialog_item_selection_range(item, display_text.len())
                    } else {
                        None
                    };
                    self.draw_edit_text_with_cursor(
                        bus,
                        abs_top,
                        abs_left,
                        abs_bottom,
                        abs_right,
                        display_text,
                        selection_range,
                        false,
                        enabled,
                    );
                }
                _ => {}
            }
        }
    }

    fn refresh_dialog_tracking_snapshot(&mut self, bus: &mut MacMemoryBus) {
        let Some(tracking) = self.dialog_tracking.as_ref() else {
            return;
        };
        if tracking.game_managed || !tracking.rendered_pixels_final {
            return;
        }
        let bounds = tracking.bounds;
        let items = tracking.items.clone();
        let default_item = tracking.default_item;
        let edit_text = tracking.edit_text.clone();
        let edit_item = tracking.edit_item;
        let dialog_ptr = tracking.dialog_ptr;
        let popup_draws = tracking.popup_draws.clone();
        let rendered_pixels = tracking.rendered_pixels.clone();
        if !rendered_pixels.is_empty() {
            self.restore_dialog_pixels(bus, bounds, &rendered_pixels);
        }
        self.redraw_standard_dialog_items(
            bus,
            bounds,
            &items,
            default_item,
            &edit_text,
            edit_item,
            dialog_ptr,
        );
        self.redraw_dialog_popup_controls(bus, &popup_draws);
        let rendered = self.save_dialog_pixels(bus, bounds);
        if let Some(tracking) = self.dialog_tracking.as_mut() {
            tracking.rendered_pixels = rendered;
        }
    }

    fn persist_visible_dialog_snapshot(
        &mut self,
        bus: &MacMemoryBus,
        tracking: &DialogTrackingState,
    ) {
        let pixels = if tracking.rendered_pixels.is_empty() {
            self.save_dialog_pixels(bus, tracking.bounds)
        } else {
            tracking.rendered_pixels.clone()
        };
        self.dialog_visible_snapshots.insert(
            tracking.dialog_ptr,
            PersistentDialogSnapshot {
                bounds: tracking.bounds,
                pixels,
            },
        );
    }

    pub(crate) fn refresh_visible_dialog_snapshot_for_port(
        &mut self,
        bus: &MacMemoryBus,
        port: u32,
    ) {
        if port == 0 {
            return;
        }
        let Some(bounds) = self
            .dialog_visible_snapshots
            .get(&port)
            .map(|snapshot| snapshot.bounds)
            .or_else(|| {
                if self.dialog_items.contains_key(&port) && self.window_visible(bus, port) {
                    Some(Self::dialog_screen_bounds(bus, port))
                } else {
                    None
                }
            })
        else {
            return;
        };
        let pixels = self.save_dialog_pixels(bus, bounds);
        self.dialog_visible_snapshots
            .insert(port, PersistentDialogSnapshot { bounds, pixels });
    }

    pub(crate) fn refresh_visible_dialog_snapshot_after_bulk_port_draw(
        &mut self,
        bus: &MacMemoryBus,
        port: u32,
    ) {
        if port == 0 {
            return;
        }
        let game_managed_visible = self
            .dialog_items
            .get(&port)
            .filter(|_| self.window_visible(bus, port))
            .is_some_and(|items| {
                let bounds = Self::dialog_screen_bounds(bus, port);
                Self::dialog_is_game_managed(bounds, items)
            });
        if game_managed_visible {
            self.refresh_visible_dialog_snapshot_for_port(bus, port);
            return;
        }
        if !self.dialog_visible_snapshots.contains_key(&port) {
            return;
        }
        let modal_tracking_matches = self
            .dialog_tracking
            .as_ref()
            .is_some_and(|tracking| tracking.dialog_ptr == port);
        let app_draw_proc_matches = self.active_modeless_dialog_draw_proc == Some(port);
        if self.dialog_modal_entered.contains(&port)
            || modal_tracking_matches
            || app_draw_proc_matches
        {
            self.refresh_visible_dialog_snapshot_for_port(bus, port);
        }
    }

    pub(crate) fn redraw_retained_modal_dialog_click(&self, bus: &mut MacMemoryBus) {
        let Some(click) = self.retained_modal_dialog_click.as_ref() else {
            return;
        };
        if !click.highlighted || click.item_no <= 0 || self.front_window != click.dialog_ptr {
            return;
        }
        let bounds = self
            .dialog_visible_snapshots
            .get(&click.dialog_ptr)
            .map(|snapshot| snapshot.bounds)
            .unwrap_or_else(|| Self::dialog_screen_bounds(bus, click.dialog_ptr));
        let rect = Self::dialog_item_screen_rect(bounds, click.rect);
        self.draw_dialog_button_highlight_state(bus, rect, &click.title, click.is_default, true);
    }

    pub(crate) fn finalize_dialog_draw_procs_if_idle(&mut self, bus: &mut MacMemoryBus) {
        let Some(tracking) = self.dialog_tracking.as_ref() else {
            if let Some(dialog_ptr) = self.active_modeless_dialog_draw_proc.take() {
                self.refresh_visible_dialog_snapshot_for_port(bus, dialog_ptr);
                self.capture_gui_frame(
                    bus,
                    &format!("modeless_dialog_draw_proc_{:08X}", dialog_ptr),
                );
            }
            return;
        };
        if tracking.draw_procs_done || !tracking.draw_proc_queue.is_empty() {
            return;
        }

        let bounds = tracking.bounds;
        let items = tracking.items.clone();
        let default_item = tracking.default_item;
        let edit_text = tracking.edit_text.clone();
        let edit_item = tracking.edit_item;
        let dialog_ptr = tracking.dialog_ptr;
        let popup_draws = tracking.popup_draws.clone();
        let game_managed = tracking.game_managed;

        if !game_managed {
            if self.front_window == dialog_ptr {
                self.blit_window_to_screen(bus);
            }
            self.redraw_standard_dialog_items(
                bus,
                bounds,
                &items,
                default_item,
                &edit_text,
                edit_item,
                dialog_ptr,
            );
        }
        self.redraw_dialog_popup_controls(bus, &popup_draws);
        let rendered = self.save_dialog_pixels(bus, bounds);

        if let Some(tracking) = self.dialog_tracking.as_mut() {
            tracking.rendered_pixels = rendered;
            tracking.rendered_pixels_final = true;
            tracking.draw_procs_done = true;
        }
    }

    /// Draw a 1-pixel black rectangle border.
    pub(crate) fn draw_rect_border(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
    ) {
        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
            self.get_screen_params();
        // Top edge
        Self::fb_hline(
            bus,
            screen_base,
            row_bytes,
            pixel_size,
            screen_width,
            screen_height,
            top,
            left,
            right - 1,
            true,
        );
        // Bottom edge
        Self::fb_hline(
            bus,
            screen_base,
            row_bytes,
            pixel_size,
            screen_width,
            screen_height,
            bottom - 1,
            left,
            right - 1,
            true,
        );
        // Left edge
        for y in top..bottom {
            Self::fb_set_pixel(
                bus,
                screen_base,
                row_bytes,
                pixel_size,
                screen_width,
                screen_height,
                left,
                y,
                true,
            );
        }
        // Right edge
        for y in top..bottom {
            Self::fb_set_pixel(
                bus,
                screen_base,
                row_bytes,
                pixel_size,
                screen_width,
                screen_height,
                right - 1,
                y,
                true,
            );
        }
    }

    fn fill_dialog_rect(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
        black: bool,
    ) {
        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
            self.get_screen_params();
        Self::fb_fill_rect(
            bus,
            screen_base,
            row_bytes,
            pixel_size,
            screen_width,
            screen_height,
            top,
            left,
            bottom,
            right,
            black,
        );
    }

    fn draw_classic_dbox_frame(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
    ) {
        let margin = Self::DBOX_FRAME_MARGIN;
        // dBoxProc is a Window Manager WDEF variant, not a content-edge
        // dialog item border. IM:I I-273 describes a modal dialog box as a
        // rectangular window with its border inside the structure edge; the
        // boundsRect remains the grafPort content region (IM:I I-282).
        self.fill_dialog_rect(
            bus,
            top - margin,
            left - margin,
            bottom + margin,
            right + margin,
            false,
        );

        // System 7.5.3's standard WDEF draws an asymmetric black structure
        // edge: one pixel on top/left and a two-pixel shadow edge on
        // bottom/right, with a two-pixel inner band inset from the outer edge.
        self.fill_dialog_rect(bus, top - 8, left - 8, top - 7, right + 8, true);
        self.fill_dialog_rect(bus, top - 8, left - 8, bottom + 8, left - 7, true);
        self.fill_dialog_rect(bus, top - 8, right + 6, bottom + 8, right + 8, true);
        self.fill_dialog_rect(bus, bottom + 6, left - 8, bottom + 8, right + 8, true);

        self.fill_dialog_rect(bus, top - 5, left - 5, top - 4, right + 5, true);
        self.fill_dialog_rect(bus, top - 4, left - 5, top - 3, right + 4, true);
        self.fill_dialog_rect(bus, top - 5, left - 5, bottom + 5, left - 4, true);
        self.fill_dialog_rect(bus, top - 5, left - 4, bottom + 4, left - 3, true);
        self.fill_dialog_rect(bus, top - 5, right + 3, bottom + 4, right + 4, true);
        self.fill_dialog_rect(bus, bottom + 3, left - 5, bottom + 4, right + 4, true);
    }

    /// Draw a 2-pixel shadow on bottom and right edges.
    pub(crate) fn draw_shadow(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
    ) {
        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
            self.get_screen_params();
        // Right shadow (2px wide)
        for dx in 0..2i16 {
            for y in (top + 2)..=(bottom + dx) {
                Self::fb_set_pixel(
                    bus,
                    screen_base,
                    row_bytes,
                    pixel_size,
                    screen_width,
                    screen_height,
                    right + dx,
                    y,
                    true,
                );
            }
        }
        // Bottom shadow (2px tall)
        for dy in 0..2i16 {
            Self::fb_hline(
                bus,
                screen_base,
                row_bytes,
                pixel_size,
                screen_width,
                screen_height,
                bottom + dy,
                left + 2,
                right + 2,
                true,
            );
        }
    }

    /// Draw a popup menu control button (procID 1008 / popupMenuProc).
    /// Shows a bordered rectangle with a 1-px drop shadow, a downward-pointing
    /// triangle on the right, and the currently-selected item title.
    /// Macintosh Toolbox Essentials 1992, 3-31
    pub(crate) fn draw_popup_control(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
        title: &str,
    ) {
        self.draw_popup_control_with_state(bus, top, left, bottom, right, title, true, false);
    }

    pub(crate) fn popup_control_box_rect(
        &self,
        bus: &MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
        menu_id: i16,
        title_width: i16,
        proc_id: i16,
    ) -> (i16, i16, i16, i16) {
        let (_, _, screen_width, _, _) = self.get_screen_params();
        let box_top = top + 1;
        let box_left = left + title_width.max(0);
        let box_bottom = bottom - 2;
        let fixed_width = ((proc_id - 1008) & 0x0001) != 0;
        let box_right = if fixed_width {
            right - 1
        } else {
            let text_width = self.popup_menu_max_item_width(bus, menu_id);
            let auto_width = (text_width + 40).max(80);
            (box_left + auto_width).min(right - 1)
        };
        (
            box_top,
            box_left.min(screen_width),
            box_bottom,
            box_right.min(screen_width),
        )
    }

    fn popup_menu_max_item_width(&self, bus: &MacMemoryBus, menu_id: i16) -> i16 {
        let font_id = 0i16;
        let font_size = 12i16;
        let mut max_width = 0;
        for item_no in 1..=255usize {
            let Some(title) = self.popup_menu_item_title(bus, menu_id, item_no) else {
                break;
            };
            max_width = max_width.max(Self::fb_measure_string(&title, font_id, font_size));
        }
        max_width
    }

    pub(crate) fn draw_popup_control_label(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        popup_left: i16,
        title: &str,
        enabled: bool,
    ) {
        if title.is_empty() || popup_left <= left + 4 {
            return;
        }

        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
            self.get_screen_params();
        let font_id = 0i16;
        let font_size = 12i16;
        let metrics = get_font_metrics(font_id, font_size);
        let text_width = Self::fb_measure_string(title, font_id, font_size);
        let text_right = popup_left - 6;
        let text_x = (text_right - text_width).max(left);
        let text_y = top + ((bottom - top) + metrics.ascent - metrics.descent) / 2;

        Self::fb_draw_string(
            bus,
            screen_base,
            row_bytes,
            pixel_size,
            screen_width,
            screen_height,
            text_x,
            text_y,
            title,
            font_id,
            font_size,
        );
        if !enabled {
            self.dim_rect(bus, top, left, bottom, popup_left);
        }
    }

    pub(crate) fn draw_popup_control_with_state(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
        title: &str,
        enabled: bool,
        pressed: bool,
    ) {
        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
            self.get_screen_params();

        let font_id = 0i16;
        let font_size = 12i16;

        if !self.draw_theme_control_chrome(
            bus,
            ControlKind::PopupButton,
            top,
            left,
            bottom,
            right,
            enabled,
            pressed,
            false,
            false,
        ) {
            // Standard popup menu button appearance.
            // Macintosh Toolbox Essentials 1992, 5-26 to 5-27.
            Self::fb_fill_rect(
                bus,
                screen_base,
                row_bytes,
                pixel_size,
                screen_width,
                screen_height,
                top,
                left,
                bottom,
                right + 1,
                false,
            );
            if enabled {
                Self::fb_hline(
                    bus,
                    screen_base,
                    row_bytes,
                    pixel_size,
                    screen_width,
                    screen_height,
                    top,
                    left,
                    right,
                    true,
                );
                Self::fb_hline(
                    bus,
                    screen_base,
                    row_bytes,
                    pixel_size,
                    screen_width,
                    screen_height,
                    bottom - 2,
                    left,
                    right,
                    true,
                );
                for y in top..(bottom - 1) {
                    Self::fb_set_pixel(
                        bus,
                        screen_base,
                        row_bytes,
                        pixel_size,
                        screen_width,
                        screen_height,
                        left,
                        y,
                        true,
                    );
                    Self::fb_set_pixel(
                        bus,
                        screen_base,
                        row_bytes,
                        pixel_size,
                        screen_width,
                        screen_height,
                        right - 1,
                        y,
                        true,
                    );
                }
                for y in (top + 3)..(bottom - 1) {
                    Self::fb_set_pixel(
                        bus,
                        screen_base,
                        row_bytes,
                        pixel_size,
                        screen_width,
                        screen_height,
                        right,
                        y,
                        true,
                    );
                }
                Self::fb_hline(
                    bus,
                    screen_base,
                    row_bytes,
                    pixel_size,
                    screen_width,
                    screen_height,
                    bottom - 1,
                    left + 3,
                    right + 1,
                    true,
                );
            } else {
                for y in (top + 3)..(bottom - 2) {
                    if (y - top) % 2 == 1 {
                        Self::fb_set_pixel(
                            bus,
                            screen_base,
                            row_bytes,
                            pixel_size,
                            screen_width,
                            screen_height,
                            right,
                            y,
                            true,
                        );
                    }
                }
                for x in (left + 1)..=right {
                    if (x - left) % 2 == 1 {
                        Self::fb_set_pixel(
                            bus,
                            screen_base,
                            row_bytes,
                            pixel_size,
                            screen_width,
                            screen_height,
                            x,
                            bottom - 2,
                            true,
                        );
                    }
                }
            }

            // Downward-pointing triangle on right side (popup indicator)
            // Macintosh Toolbox Essentials 1992, 5-26
            let tri_x = right - 12;
            if enabled {
                for row in 0..6i16 {
                    Self::fb_hline(
                        bus,
                        screen_base,
                        row_bytes,
                        pixel_size,
                        screen_width,
                        screen_height,
                        top + 6 + row,
                        tri_x - 5 + row,
                        tri_x + 6 - row,
                        true,
                    );
                }
            } else {
                for row in [0i16, 2, 4] {
                    let start = tri_x - 4 + row;
                    let end = tri_x + 5 - row;
                    for x in start..end {
                        if (x - start) % 2 == 0 {
                            Self::fb_set_pixel(
                                bus,
                                screen_base,
                                row_bytes,
                                pixel_size,
                                screen_width,
                                screen_height,
                                x,
                                top + 7 + row,
                                true,
                            );
                        }
                    }
                }
            }
        }

        // Selected item text inside the box
        // Macintosh Toolbox Essentials 1992, 5-26
        if enabled && !title.is_empty() {
            let metrics = get_font_metrics(font_id, font_size);
            let text_x = left + 15;
            let text_y =
                top + (bottom - top - (metrics.ascent + metrics.descent)) / 2 + metrics.ascent - 1;
            Self::fb_draw_string_styled(
                bus,
                screen_base,
                row_bytes,
                pixel_size,
                screen_width,
                screen_height,
                text_x,
                text_y,
                title,
                font_id,
                font_size,
                0,
            );
        }
    }

    fn redraw_dialog_popup_controls(
        &self,
        bus: &mut MacMemoryBus,
        popup_draws: &[DialogPopupDraw],
    ) {
        for draw in popup_draws {
            let (top, left, bottom, right) = draw.rect;
            if draw.enabled && !draw.pressed {
                self.draw_popup_control(bus, top, left, bottom, right, &draw.title);
                continue;
            }
            self.draw_popup_control_with_state(
                bus,
                top,
                left,
                bottom,
                right,
                &draw.title,
                draw.enabled,
                draw.pressed,
            );
        }
    }

    /// Draw a framed round-rect directly to the framebuffer.
    /// Used for dialog default button outlines where no CpuOps/port is available.
    /// Macintosh Toolbox Essentials 1992, Listing 6-17
    fn fb_frame_round_rect(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
        oval_width: i16,
        oval_height: i16,
        pen_size: i16,
    ) {
        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
            self.get_screen_params();
        let r = Rect {
            top,
            left,
            bottom,
            right,
        };
        let outer_spans = self.compute_rrect_spans(&r, oval_width, oval_height);

        let r_inset = Rect {
            top: top + pen_size,
            left: left + pen_size,
            bottom: bottom - pen_size,
            right: right - pen_size,
        };
        let inner_spans = self.compute_rrect_spans(
            &r_inset,
            (oval_width - 2 * pen_size).max(0),
            (oval_height - 2 * pen_size).max(0),
        );

        for y in top..bottom {
            let outer_idx = (y - top) as usize;
            if outer_idx >= outer_spans.len() {
                continue;
            }
            let (ol, or) = outer_spans[outer_idx];

            let inner_idx = (y - r_inset.top) as usize;
            let (il, ir) =
                if y >= r_inset.top && y < r_inset.bottom && inner_idx < inner_spans.len() {
                    inner_spans[inner_idx]
                } else {
                    (or, ol) // no inner = draw full outer span
                };

            // Left border segment
            for x in ol..il.min(or) {
                Self::fb_set_pixel(
                    bus,
                    screen_base,
                    row_bytes,
                    pixel_size,
                    screen_width,
                    screen_height,
                    x,
                    y,
                    true,
                );
            }
            // Right border segment
            for x in ir.max(ol)..or {
                Self::fb_set_pixel(
                    bus,
                    screen_base,
                    row_bytes,
                    pixel_size,
                    screen_width,
                    screen_height,
                    x,
                    y,
                    true,
                );
            }
        }
    }

    /// Draw a button with optional default (thick) border.
    /// Inside Macintosh Volume I, I-405
    pub(crate) fn draw_button(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
        title: &str,
        is_default: bool,
    ) {
        self.draw_button_state(bus, top, left, bottom, right, title, is_default, true);
    }

    fn draw_button_with_enabled(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
        title: &str,
        is_default: bool,
        enabled: bool,
    ) {
        if enabled {
            self.draw_button(bus, top, left, bottom, right, title, is_default);
            return;
        }
        self.draw_button_state(bus, top, left, bottom, right, title, is_default, enabled);
    }

    fn draw_button_state(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
        title: &str,
        is_default: bool,
        enabled: bool,
    ) {
        if !self.draw_theme_push_button_chrome(
            bus, top, left, bottom, right, enabled, false, is_default,
        ) {
            self.fill_classic_button_shape(bus, top, left, bottom, right);
            self.draw_classic_button_outline(bus, top, left, bottom, right);

            // Default button: rounded bold outline (3px thick)
            // Macintosh Toolbox Essentials 1992, Listing 6-17
            // references/executor/src/error/system_error.cpp
            if is_default {
                let hilite_top = top - 4;
                let hilite_left = left - 4;
                let hilite_bottom = bottom + 4;
                let hilite_right = right + 4;
                let hilite_height = hilite_bottom - hilite_top;
                let oval = (hilite_height / 2 - 4).max(4);
                self.fb_frame_round_rect(
                    bus,
                    hilite_top,
                    hilite_left,
                    hilite_bottom,
                    hilite_right,
                    oval,
                    oval,
                    3,
                );
            }
        }

        self.draw_button_label(bus, top, left, bottom, right, title);
    }

    fn fill_classic_button_shape(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
    ) {
        if bottom - top < 8 || right - left < 8 {
            let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
                self.get_screen_params();
            Self::fb_fill_rect(
                bus,
                screen_base,
                row_bytes,
                pixel_size,
                screen_width,
                screen_height,
                top,
                left,
                bottom,
                right,
                false,
            );
            return;
        }

        // Classic push buttons are rounded controls inside the DITL display
        // rectangle. Fill the same rounded shape the CDEF frames so pixels in
        // the rectangular corner cutouts remain whatever the dialog port had
        // underneath.
        if bottom - top >= 24 {
            self.fill_dialog_rect(bus, top, left + 4, top + 1, right - 4, false);
            self.fill_dialog_rect(bus, top + 1, left + 2, top + 2, right - 2, false);
            self.fill_dialog_rect(bus, top + 2, left + 1, top + 4, right - 1, false);
            self.fill_dialog_rect(bus, top + 4, left, bottom - 4, right, false);
            self.fill_dialog_rect(bus, bottom - 4, left + 1, bottom - 2, right - 1, false);
            self.fill_dialog_rect(bus, bottom - 2, left + 2, bottom - 1, right - 2, false);
            self.fill_dialog_rect(bus, bottom - 1, left + 4, bottom, right - 4, false);
            return;
        }

        self.fill_dialog_rect(bus, top, left + 3, top + 1, right - 3, false);
        self.fill_dialog_rect(bus, top + 1, left + 1, top + 3, right - 1, false);
        self.fill_dialog_rect(bus, top + 3, left, bottom - 3, right, false);
        self.fill_dialog_rect(bus, bottom - 3, left + 1, bottom - 1, right - 1, false);
        self.fill_dialog_rect(bus, bottom - 1, left + 3, bottom, right - 3, false);
    }

    fn draw_button_label(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
        title: &str,
    ) {
        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
            self.get_screen_params();
        let font_id = 0i16; // Chicago
        let font_size = 12i16;
        let metrics = get_font_metrics(font_id, font_size);
        let text_w = Self::fb_measure_string(title, font_id, font_size);
        let text_x = left + (right - left - text_w) / 2;
        let text_y = top + (bottom - top - (metrics.ascent + metrics.descent)) / 2 + metrics.ascent;
        Self::fb_draw_string(
            bus,
            screen_base,
            row_bytes,
            pixel_size,
            screen_width,
            screen_height,
            text_x,
            text_y,
            title,
            font_id,
            font_size,
        );
    }

    fn draw_classic_button_outline(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
    ) {
        if bottom - top < 8 || right - left < 8 {
            self.draw_rect_border(bus, top, left, bottom, right);
            return;
        }

        // Dialog Manager buttons are standard controls whose display
        // rectangle becomes the control's enclosing rectangle (IM:I I-405).
        // The classic System 7 control definition draws the push button as a
        // one-pixel rounded rectangle inside that enclosing rectangle.
        if bottom - top >= 24 {
            self.fill_dialog_rect(bus, top, left + 4, top + 1, right - 4, true);
            self.fill_dialog_rect(bus, top + 1, left + 2, top + 2, left + 4, true);
            self.fill_dialog_rect(bus, top + 1, right - 4, top + 2, right - 2, true);
            self.fill_dialog_rect(bus, top + 2, left + 1, top + 3, left + 2, true);
            self.fill_dialog_rect(bus, top + 2, right - 2, top + 3, right - 1, true);
            self.fill_dialog_rect(bus, top + 3, left + 1, top + 4, left + 2, true);
            self.fill_dialog_rect(bus, top + 3, right - 2, top + 4, right - 1, true);
            self.fill_dialog_rect(bus, top + 4, left, bottom - 4, left + 1, true);
            self.fill_dialog_rect(bus, top + 4, right - 1, bottom - 4, right, true);
            self.fill_dialog_rect(bus, bottom - 4, left + 1, bottom - 3, left + 2, true);
            self.fill_dialog_rect(bus, bottom - 4, right - 2, bottom - 3, right - 1, true);
            self.fill_dialog_rect(bus, bottom - 3, left + 1, bottom - 2, left + 2, true);
            self.fill_dialog_rect(bus, bottom - 3, right - 2, bottom - 2, right - 1, true);
            self.fill_dialog_rect(bus, bottom - 2, left + 2, bottom - 1, left + 4, true);
            self.fill_dialog_rect(bus, bottom - 2, right - 4, bottom - 1, right - 2, true);
            self.fill_dialog_rect(bus, bottom - 1, left + 4, bottom, right - 4, true);
            return;
        }

        self.fill_dialog_rect(bus, top, left + 3, top + 1, right - 3, true);
        self.fill_dialog_rect(bus, top + 1, left + 1, top + 2, left + 3, true);
        self.fill_dialog_rect(bus, top + 1, right - 3, top + 2, right - 1, true);
        self.fill_dialog_rect(bus, top + 2, left + 1, top + 3, left + 2, true);
        self.fill_dialog_rect(bus, top + 2, right - 2, top + 3, right - 1, true);
        self.fill_dialog_rect(bus, top + 3, left, bottom - 3, left + 1, true);
        self.fill_dialog_rect(bus, top + 3, right - 1, bottom - 3, right, true);
        self.fill_dialog_rect(bus, bottom - 3, left + 1, bottom - 2, left + 2, true);
        self.fill_dialog_rect(bus, bottom - 3, right - 2, bottom - 2, right - 1, true);
        self.fill_dialog_rect(bus, bottom - 2, left + 1, bottom - 1, left + 3, true);
        self.fill_dialog_rect(bus, bottom - 2, right - 3, bottom - 1, right - 1, true);
        self.fill_dialog_rect(bus, bottom - 1, left + 3, bottom, right - 3, true);
    }

    fn draw_dialog_button_highlight_state(
        &self,
        bus: &mut MacMemoryBus,
        rect: (i16, i16, i16, i16),
        title: &str,
        is_default: bool,
        highlighted: bool,
    ) {
        let (top, left, bottom, right) = rect;
        if self.draw_theme_push_button_chrome(
            bus,
            top,
            left,
            bottom,
            right,
            true,
            highlighted,
            is_default,
        ) {
            self.draw_button_label(bus, top, left, bottom, right, title);
            return;
        }
        self.invert_button_rect(bus, top, left, bottom, right);
    }

    /// Draw a checkbox item.
    pub(crate) fn draw_checkbox(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        _right: i16,
        title: &str,
        checked: bool,
    ) {
        self.draw_checkbox_state(bus, top, left, bottom, _right, title, checked, true, false);
    }

    fn draw_checkbox_with_enabled(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        _right: i16,
        title: &str,
        checked: bool,
        enabled: bool,
    ) {
        self.draw_checkbox_with_enabled_and_inactive(
            bus, top, left, bottom, _right, title, checked, enabled, false,
        );
    }

    fn draw_checkbox_with_enabled_and_inactive(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        _right: i16,
        title: &str,
        checked: bool,
        enabled: bool,
        inactive: bool,
    ) {
        if enabled && !inactive {
            self.draw_checkbox(bus, top, left, bottom, _right, title, checked);
            return;
        }
        self.draw_checkbox_state(
            bus, top, left, bottom, _right, title, checked, enabled, inactive,
        );
    }

    fn draw_checkbox_state(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        _right: i16,
        title: &str,
        checked: bool,
        enabled: bool,
        inactive: bool,
    ) {
        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
            self.get_screen_params();
        let box_size = Self::STANDARD_CONTROL_MARK_SIZE;
        // Standard dialog checkbox/radio controls are drawn by the System
        // CDEF as a 12-pixel mark plus title inside the item rectangle
        // (MTE 1992 pp. 5-4..5-5; HIG 1992 pp. 209..212).
        let height = bottom - top;
        let box_top = top + (height - box_size) / 2;
        let box_left = left + Self::STANDARD_CONTROL_MARK_LEFT_INSET;
        if !self.draw_theme_control_chrome(
            bus,
            ControlKind::Checkbox,
            box_top,
            box_left,
            box_top + box_size,
            box_left + box_size,
            enabled && !inactive,
            false,
            checked,
            false,
        ) {
            // Draw checkbox box
            Self::fb_fill_rect(
                bus,
                screen_base,
                row_bytes,
                pixel_size,
                screen_width,
                screen_height,
                box_top,
                box_left,
                box_top + box_size,
                box_left + box_size,
                false,
            );
            self.draw_rect_border(
                bus,
                box_top,
                box_left,
                box_top + box_size,
                box_left + box_size,
            );
            if checked {
                // Draw X inside
                for i in 1..box_size - 1 {
                    Self::fb_set_pixel(
                        bus,
                        screen_base,
                        row_bytes,
                        pixel_size,
                        screen_width,
                        screen_height,
                        box_left + i,
                        box_top + i,
                        true,
                    );
                    Self::fb_set_pixel(
                        bus,
                        screen_base,
                        row_bytes,
                        pixel_size,
                        screen_width,
                        screen_height,
                        box_left + box_size - 1 - i,
                        box_top + i,
                        true,
                    );
                }
            }
        }
        // Draw label text to the right of the box. Checkbox/radio items are
        // controls, and SetDialogFont/SetDAFont does not affect control
        // titles (IM:I I-412; MTE 1992 p. 6-105).
        let font_id = 0i16;
        let font_size = 12i16;
        let metrics = get_font_metrics(font_id, font_size);
        let text_x = box_left + box_size + Self::STANDARD_CONTROL_TITLE_GAP;
        let text_y = top + (height + metrics.ascent - metrics.descent) / 2;
        let label_left = box_left + box_size + 1;
        let dim_disabled_title = !enabled && self.ui_theme_id() != UiThemeId::ClassicSystem7;
        self.draw_standard_control_title_text(
            bus,
            top,
            label_left,
            bottom,
            _right,
            text_x,
            text_y,
            title,
            font_id,
            font_size,
            inactive,
            dim_disabled_title,
        );
    }

    /// Draw a radio button item.
    pub(crate) fn draw_radio(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        _right: i16,
        title: &str,
        selected: bool,
    ) {
        self.draw_radio_state(bus, top, left, bottom, _right, title, selected, true, false);
    }

    fn draw_radio_with_enabled(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        _right: i16,
        title: &str,
        selected: bool,
        enabled: bool,
    ) {
        self.draw_radio_with_enabled_and_inactive(
            bus, top, left, bottom, _right, title, selected, enabled, false,
        );
    }

    fn draw_radio_with_enabled_and_inactive(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        _right: i16,
        title: &str,
        selected: bool,
        enabled: bool,
        inactive: bool,
    ) {
        if enabled && !inactive {
            self.draw_radio(bus, top, left, bottom, _right, title, selected);
            return;
        }
        self.draw_radio_state(
            bus, top, left, bottom, _right, title, selected, enabled, inactive,
        );
    }

    fn draw_radio_state(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        _right: i16,
        title: &str,
        selected: bool,
        enabled: bool,
        inactive: bool,
    ) {
        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
            self.get_screen_params();
        let mark_size = Self::STANDARD_CONTROL_MARK_SIZE;
        let height = bottom - top;
        let mark_top = top + (height - mark_size) / 2;
        let mark_left = left + Self::STANDARD_CONTROL_MARK_LEFT_INSET;
        if !self.draw_theme_control_chrome(
            bus,
            ControlKind::RadioButton,
            mark_top,
            mark_left,
            mark_top + mark_size,
            mark_left + mark_size,
            enabled && !inactive,
            false,
            selected,
            false,
        ) {
            Self::fb_fill_rect(
                bus,
                screen_base,
                row_bytes,
                pixel_size,
                screen_width,
                screen_height,
                mark_top,
                mark_left,
                mark_top + mark_size,
                mark_left + mark_size,
                false,
            );
            // Radio buttons are small circles, with a dot when selected
            // (IM:I I-312; MTE 1992 pp. 5-5..5-6; HIG 1992 pp. 209..210).
            self.draw_control_mask_12(
                bus,
                screen_base,
                row_bytes,
                pixel_size,
                screen_width,
                screen_height,
                mark_left,
                mark_top,
                &Self::CLASSIC_RADIO_OUTLINE_12,
            );
            if selected {
                self.draw_control_mask_12(
                    bus,
                    screen_base,
                    row_bytes,
                    pixel_size,
                    screen_width,
                    screen_height,
                    mark_left,
                    mark_top,
                    &Self::CLASSIC_RADIO_DOT_12,
                );
            }
        }
        // Label
        // SetDialogFont/SetDAFont affects statText and editText items but
        // not control titles (IM:I I-412; MTE 1992 p. 6-105).
        let font_id = 0i16;
        let font_size = 12i16;
        let metrics = get_font_metrics(font_id, font_size);
        let text_x = mark_left + mark_size + Self::STANDARD_CONTROL_TITLE_GAP;
        let text_y = top + (height + metrics.ascent - metrics.descent) / 2;
        let label_left = mark_left + mark_size + 1;
        let dim_disabled_title = !enabled && self.ui_theme_id() != UiThemeId::ClassicSystem7;
        self.draw_standard_control_title_text(
            bus,
            top,
            label_left,
            bottom,
            _right,
            text_x,
            text_y,
            title,
            font_id,
            font_size,
            inactive,
            dim_disabled_title,
        );
    }

    fn draw_standard_control_title_text(
        &self,
        bus: &mut MacMemoryBus,
        label_top: i16,
        label_left: i16,
        label_bottom: i16,
        label_right: i16,
        text_x: i16,
        text_y: i16,
        title: &str,
        font_id: i16,
        font_size: i16,
        inactive: bool,
        dim_disabled_title: bool,
    ) {
        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
            self.get_screen_params();
        if inactive && pixel_size == 8 {
            // Standard DITL controls are backed by ControlRecords. When a
            // game calls HiliteControl(255), their titles follow inactive
            // Control Manager rendering; itemDisable alone stays visually
            // active in classic dialogs.
            let [r, g, b] = Self::INACTIVE_STANDARD_CONTROL_TITLE_RGB;
            let gray_index = super::pict::closest_clut_index(r, g, b, &self.device_clut);
            Self::fb_draw_string_styled_index(
                bus,
                screen_base,
                row_bytes,
                pixel_size,
                screen_width,
                screen_height,
                text_x,
                text_y,
                title,
                font_id,
                font_size,
                0,
                gray_index,
            );
            return;
        }

        Self::fb_draw_string(
            bus,
            screen_base,
            row_bytes,
            pixel_size,
            screen_width,
            screen_height,
            text_x,
            text_y,
            title,
            font_id,
            font_size,
        );
        if inactive || dim_disabled_title {
            self.dim_rect(bus, label_top, label_left, label_bottom, label_right);
        }
    }

    fn draw_control_mask_12(
        &self,
        bus: &mut MacMemoryBus,
        screen_base: u32,
        row_bytes: u32,
        pixel_size: u16,
        screen_width: i16,
        screen_height: i16,
        left: i16,
        top: i16,
        mask: &[&str; 12],
    ) {
        for (dy, row) in mask.iter().enumerate() {
            for (dx, pixel) in row.as_bytes().iter().enumerate() {
                if *pixel == b'#' {
                    Self::fb_set_pixel(
                        bus,
                        screen_base,
                        row_bytes,
                        pixel_size,
                        screen_width,
                        screen_height,
                        left + dx as i16,
                        top + dy as i16,
                        true,
                    );
                }
            }
        }
    }

    /// Replace `^0`..`^3` in dialog/alert text with the strings most
    /// recently passed to `ParamText`. Returns a borrowed reference to
    /// the original `text` when no placeholders are present (the common
    /// case — most DITL items don't use ParamText), avoiding an
    /// allocation per draw_static_text call.
    /// Inside Macintosh Volume I, I-422.
    pub(crate) fn apply_param_text<'a>(&self, text: &'a str) -> std::borrow::Cow<'a, str> {
        if !text.contains('^') {
            return std::borrow::Cow::Borrowed(text);
        }
        let mut out = String::with_capacity(text.len());
        let mut chars = text.chars().peekable();
        while let Some(ch) = chars.next() {
            if ch == '^' {
                if let Some(&next) = chars.peek() {
                    if let Some(idx) = next.to_digit(10) {
                        if (idx as usize) < self.param_text.len() {
                            chars.next();
                            out.push_str(&decode_mac_roman_for_render(
                                &self.param_text[idx as usize],
                            ));
                            continue;
                        }
                    }
                }
            }
            out.push(ch);
        }
        std::borrow::Cow::Owned(out)
    }

    fn draw_static_text(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
        text: &str,
    ) {
        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
            self.get_screen_params();
        // SetDialogFont/SetDAFont affects statText and editText items but
        // not control titles (IM:I I-412; MTE 1992 p. 6-105).
        let font_id = self.tx_font;
        let font_size = Self::font_lookup_size(self.tx_size);
        let metrics = get_font_metrics(font_id, font_size);
        let line_height = metrics.ascent + metrics.descent + metrics.leading;
        let max_width = (right - left).saturating_sub(Self::TE_LINE_LEFT_INSET);
        let mut y = top + metrics.ascent;
        if y >= bottom && bottom > top {
            y = bottom - 1;
        }

        let substituted = self.apply_param_text(text);
        let text_bytes = substituted.as_bytes();
        let lines = self.te_wrap_lines(font_id, self.tx_size, text_bytes, max_width);

        for (start, end) in lines {
            let mut trimmed_end = end;
            while trimmed_end > start && matches!(text_bytes[trimmed_end - 1], b' ' | b'\r' | b'\n')
            {
                trimmed_end -= 1;
            }
            if trimmed_end > start && y <= bottom {
                // IM:I I-405 to I-406: statText draws like editText text
                // inside the display rectangle, including wrap and clipping,
                // but without the editText frame.
                let line: String = text_bytes[start..trimmed_end]
                    .iter()
                    .map(|&byte| byte as char)
                    .collect();
                Self::fb_draw_string(
                    bus,
                    screen_base,
                    row_bytes,
                    pixel_size,
                    screen_width,
                    screen_height,
                    left + Self::TE_LINE_LEFT_INSET,
                    y,
                    &line,
                    font_id,
                    font_size,
                );
            }
            y += line_height;
            if y > bottom {
                break;
            }
        }
    }

    /// Draw an editable text field with border and text.
    pub(crate) fn draw_edit_text(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
        text: &str,
        selected: bool,
    ) {
        let selection_range = selected.then_some((0, text.len()));
        self.draw_edit_text_with_cursor(
            bus,
            top,
            left,
            bottom,
            right,
            text,
            selection_range,
            !selected,
            true,
        );
    }

    fn draw_edit_text_with_cursor(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
        text: &str,
        selection_range: Option<(usize, usize)>,
        show_cursor: bool,
        enabled: bool,
    ) {
        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
            self.get_screen_params();
        // IM:I I-414 and HIG 1992 p. 184: the active editText item is a
        // text-entry field with visible focus feedback. The theme provider owns
        // that field chrome only; TextEdit metrics, text drawing, selection,
        // and caret positioning remain classic guest-visible behavior.
        let frame_top = top - Self::EDIT_TEXT_FRAME_OUTSET;
        let frame_left = left - Self::EDIT_TEXT_FRAME_OUTSET;
        let frame_bottom = bottom + Self::EDIT_TEXT_FRAME_OUTSET;
        let frame_right = right + Self::EDIT_TEXT_FRAME_OUTSET;

        if !self.draw_theme_text_field(
            bus,
            frame_top,
            frame_left,
            frame_bottom,
            frame_right,
            enabled,
            selection_range.is_some() || show_cursor,
        ) {
            // White fill
            Self::fb_fill_rect(
                bus,
                screen_base,
                row_bytes,
                pixel_size,
                screen_width,
                screen_height,
                frame_top,
                frame_left,
                frame_bottom,
                frame_right,
                false,
            );
            // IM:I I-405: editText's display rectangle becomes the TextEdit
            // view/dest rectangle, and Dialog Manager frames it three pixels
            // outside that display rectangle.
            self.draw_rect_border(bus, frame_top, frame_left, frame_bottom, frame_right);
        }
        let font_id = self.tx_font;
        let font_size = Self::font_lookup_size(self.tx_size);
        let metrics = get_font_metrics(font_id, font_size);
        let text_y = top + metrics.ascent;
        Self::fb_draw_string(
            bus,
            screen_base,
            row_bytes,
            pixel_size,
            screen_width,
            screen_height,
            left + 1,
            text_y,
            text,
            font_id,
            font_size,
        );
        if let Some((selection_start, selection_end)) = selection_range {
            // IM:I I-422 and MTE 1992 p. 6-131: SelIText /
            // SelectDialogItemText displays the selected range by inverting
            // the selected characters. Match TextEdit's left-justified
            // selection rectangle: glyphs draw at destRect.left+1, but a
            // selection beginning at character 0 includes the one-pixel inset.
            if !text.is_empty() {
                let text_bytes = text.as_bytes();
                let start = selection_start.min(text_bytes.len());
                let end = selection_end.min(text_bytes.len());
                if start < end {
                    let line_height = metrics.ascent + metrics.descent + metrics.leading;
                    let selection_top = top;
                    let selection_bottom = top.saturating_add(line_height).min(bottom);
                    let selection_left = if start == 0 {
                        left
                    } else {
                        left + Self::TE_LINE_LEFT_INSET
                            + self.te_measure_text_width(
                                font_id,
                                self.tx_size,
                                text_bytes,
                                0,
                                start,
                            )
                    };
                    let selection_right = if end >= text_bytes.len() {
                        right
                    } else {
                        left + Self::TE_LINE_LEFT_INSET
                            + self.te_measure_text_width(font_id, self.tx_size, text_bytes, 0, end)
                    };
                    if selection_left < selection_right && selection_top < selection_bottom {
                        if self.ui_theme_id() == UiThemeId::ClassicSystem7 {
                            self.invert_rect_exact(
                                bus,
                                selection_top,
                                selection_left,
                                selection_bottom,
                                selection_right,
                            );
                        } else {
                            self.draw_theme_text_selection(
                                bus,
                                selection_top,
                                selection_left,
                                selection_bottom,
                                selection_right,
                                true,
                            );
                        }
                    }
                }
            }
        } else if show_cursor {
            // Draw cursor bar at end of text
            let text_width = Self::fb_measure_string(text, font_id, font_size);
            let cursor_x = left + 3 + text_width;
            if cursor_x < right - 1
                && !self.draw_theme_caret(bus, top + 2, cursor_x, bottom - 1, cursor_x + 1)
            {
                for y in (top + 2)..=(bottom - 2) {
                    Self::fb_set_pixel(
                        bus,
                        screen_base,
                        row_bytes,
                        pixel_size,
                        screen_width,
                        screen_height,
                        cursor_x,
                        y,
                        true,
                    );
                }
            }
        }
    }

    fn dialog_cicn_layout(bus: &MacMemoryBus, icon_ptr: u32) -> Option<DialogCIconLayout> {
        if bus.get_alloc_size(icon_ptr).is_some_and(|size| size < 82) {
            return None;
        }

        // Imaging With QuickDraw 1994 p. 4-106: a compiled 'cicn'
        // resource starts with a 50-byte PixMap, 14-byte mask BitMap,
        // 14-byte 1-bit fallback BitMap, 4-byte iconData handle, then
        // mask bits, fallback bitmap bits, ColorTable, and PixMap data.
        let pm_row_bytes = (bus.read_word(icon_ptr + 4) & 0x3FFF) as u32;
        let pm_top = bus.read_word(icon_ptr + 6) as i16;
        let pm_left = bus.read_word(icon_ptr + 8) as i16;
        let pm_bottom = bus.read_word(icon_ptr + 10) as i16;
        let pm_right = bus.read_word(icon_ptr + 12) as i16;
        let pixel_size = bus.read_word(icon_ptr + 32);
        let mask_row_bytes = (bus.read_word(icon_ptr + 54) & 0x3FFF) as u32;
        let bmap_row_bytes = (bus.read_word(icon_ptr + 68) & 0x3FFF) as u32;

        let width = pm_right - pm_left;
        let height = pm_bottom - pm_top;
        if width <= 0 || height <= 0 || pm_row_bytes == 0 || mask_row_bytes == 0 {
            return None;
        }

        let height_u32 = height as u32;
        let mask_data_size = mask_row_bytes.checked_mul(height_u32)?;
        let bmap_data_size = bmap_row_bytes.checked_mul(height_u32)?;
        let mask_data_ptr = icon_ptr + 82;
        let bmap_data_ptr = mask_data_ptr.checked_add(mask_data_size)?;
        let ctab_ptr = bmap_data_ptr.checked_add(bmap_data_size)?;

        if let Some(resource_size) = bus.get_alloc_size(icon_ptr) {
            let resource_end = icon_ptr.checked_add(resource_size)?;
            if ctab_ptr.checked_add(8)? > resource_end {
                return None;
            }
        }

        let ct_size = bus.read_word(ctab_ptr + 6) as u32;
        let ctab_total_bytes = 8u32.checked_add((ct_size + 1).checked_mul(8)?)?;
        let pixel_data_ptr = ctab_ptr.checked_add(ctab_total_bytes)?;

        if let Some(resource_size) = bus.get_alloc_size(icon_ptr) {
            let resource_end = icon_ptr.checked_add(resource_size)?;
            let pixel_data_size = pm_row_bytes.checked_mul(height_u32)?;
            if pixel_data_ptr.checked_add(pixel_data_size)? > resource_end {
                return None;
            }
        }

        Some(DialogCIconLayout {
            width,
            height,
            pm_row_bytes,
            mask_row_bytes,
            bmap_row_bytes,
            pixel_size,
            mask_data_ptr,
            bmap_data_ptr,
            pixel_data_ptr,
        })
    }

    fn dialog_cicn_pixel_index(
        bus: &MacMemoryBus,
        data_ptr: u32,
        row_bytes: u32,
        pixel_size: u16,
        x: u32,
        y: u32,
    ) -> Option<u8> {
        let row_ptr = data_ptr.checked_add(y.checked_mul(row_bytes)?)?;
        match pixel_size {
            1 => {
                let byte = bus.read_byte(row_ptr.checked_add(x / 8)?);
                Some(((byte >> (7 - (x % 8))) & 1) as u8)
            }
            2 => {
                let byte = bus.read_byte(row_ptr.checked_add(x / 4)?);
                Some((byte >> (6 - 2 * (x % 4))) & 0x03)
            }
            4 => {
                let byte = bus.read_byte(row_ptr.checked_add(x / 2)?);
                Some(if x % 2 == 0 {
                    (byte >> 4) & 0x0F
                } else {
                    byte & 0x0F
                })
            }
            8 => Some(bus.read_byte(row_ptr.checked_add(x)?)),
            size if size > 8 && size % 8 == 0 => {
                let bytes_per_pixel = u32::from(size / 8);
                Some(bus.read_byte(row_ptr.checked_add(x.checked_mul(bytes_per_pixel)?)?))
            }
            _ => None,
        }
    }

    fn draw_cicn_icon(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
        icon_ptr: u32,
    ) -> bool {
        let Some(layout) = Self::dialog_cicn_layout(bus, icon_ptr) else {
            return false;
        };
        let (screen_base, row_bytes, screen_width, screen_height, screen_pixel_size) =
            self.get_screen_params();
        let dst_w = right - left;
        let dst_h = bottom - top;
        if dst_w <= 0 || dst_h <= 0 {
            return false;
        }

        for dy in 0..dst_h {
            let sy = (dy as i32 * i32::from(layout.height) / i32::from(dst_h)) as u32;
            let dst_y = top + dy;
            for dx in 0..dst_w {
                let sx = (dx as i32 * i32::from(layout.width) / i32::from(dst_w)) as u32;
                let dst_x = left + dx;
                let mask_byte =
                    bus.read_byte(layout.mask_data_ptr + sy * layout.mask_row_bytes + sx / 8);
                if (mask_byte & (0x80 >> (sx % 8))) == 0 {
                    continue;
                }

                if screen_pixel_size == 8 && layout.pixel_size >= 2 {
                    let Some(pixel_index) = Self::dialog_cicn_pixel_index(
                        bus,
                        layout.pixel_data_ptr,
                        layout.pm_row_bytes,
                        layout.pixel_size,
                        sx,
                        sy,
                    ) else {
                        continue;
                    };
                    if dst_x >= 0 && dst_y >= 0 && dst_x < screen_width && dst_y < screen_height {
                        bus.write_byte(
                            screen_base + (dst_y as u32) * row_bytes + dst_x as u32,
                            pixel_index,
                        );
                    }
                    continue;
                }

                let source_data_ptr = if layout.bmap_row_bytes != 0 {
                    layout.bmap_data_ptr
                } else {
                    layout.pixel_data_ptr
                };
                let source_row_bytes = if layout.bmap_row_bytes != 0 {
                    layout.bmap_row_bytes
                } else {
                    layout.pm_row_bytes
                };
                let source_pixel_size = if layout.bmap_row_bytes != 0 {
                    1
                } else {
                    layout.pixel_size
                };
                let Some(pixel_index) = Self::dialog_cicn_pixel_index(
                    bus,
                    source_data_ptr,
                    source_row_bytes,
                    source_pixel_size,
                    sx,
                    sy,
                ) else {
                    continue;
                };
                Self::fb_set_pixel(
                    bus,
                    screen_base,
                    row_bytes,
                    screen_pixel_size,
                    screen_width,
                    screen_height,
                    dst_x,
                    dst_y,
                    pixel_index != 0,
                );
            }
        }
        true
    }

    /// Draw a 32x32 1-bit ICON resource, scaled to fit the display rect.
    /// Inside Macintosh Volume I, I-205
    fn draw_icon(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
        icon_ptr: u32,
    ) {
        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
            self.get_screen_params();
        let dst_w = right - left;
        let dst_h = bottom - top;
        // ICON is 32x32 pixels, 1 bit per pixel, 4 bytes per row = 128 bytes
        for row in 0..32i16 {
            let row_data = bus.read_long(icon_ptr + (row as u32) * 4);
            for col in 0..32i16 {
                let bit = (row_data >> (31 - col)) & 1;
                if bit != 0 {
                    // Scale to destination rect
                    let dx = left + col * dst_w / 32;
                    let dy = top + row * dst_h / 32;
                    Self::fb_set_pixel(
                        bus,
                        screen_base,
                        row_bytes,
                        pixel_size,
                        screen_width,
                        screen_height,
                        dx,
                        dy,
                        true,
                    );
                }
            }
        }
    }

    /// Invert the pixels within a button rect (for flash animation).
    pub(crate) fn invert_button_rect(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
    ) {
        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
            self.get_screen_params();

        let y_start = (top as i32 + 1).clamp(0, screen_height as i32);
        let y_end = (bottom as i32 - 1).clamp(0, screen_height as i32);
        let x_start = (left as i32 + 1).clamp(0, screen_width as i32);
        let x_end = (right as i32 - 1).clamp(0, screen_width as i32);
        if y_start >= y_end || x_start >= x_end {
            return;
        }

        for y in y_start..y_end {
            for x in x_start..x_end {
                if pixel_size == 8 {
                    let addr = screen_base + (y as u32) * row_bytes + (x as u32);
                    let b = bus.read_byte(addr);
                    bus.write_byte(addr, 255 - b);
                } else {
                    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;
                    let b = bus.read_byte(addr);
                    bus.write_byte(addr, b ^ (1 << bit));
                }
            }
        }
    }

    fn invert_rect_exact(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
    ) {
        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
            self.get_screen_params();

        let y_start = (top as i32).clamp(0, screen_height as i32);
        let y_end = (bottom as i32).clamp(0, screen_height as i32);
        let x_start = (left as i32).clamp(0, screen_width as i32);
        let x_end = (right as i32).clamp(0, screen_width as i32);
        if y_start >= y_end || x_start >= x_end {
            return;
        }

        for y in y_start..y_end {
            for x in x_start..x_end {
                if pixel_size == 8 {
                    let addr = screen_base + (y as u32) * row_bytes + (x as u32);
                    let b = bus.read_byte(addr);
                    bus.write_byte(addr, 255 - b);
                } else {
                    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;
                    let b = bus.read_byte(addr);
                    bus.write_byte(addr, b ^ (1 << bit));
                }
            }
        }
    }

    /// Hit-test a point against dialog items. Returns 1-based item number or 0.
    fn dialog_item_hit_test(
        &self,
        bus: &MacMemoryBus,
        items: &[DialogItem],
        bounds: (i16, i16, i16, i16),
        screen_v: i16,
        screen_h: i16,
        popup_original_rects: &std::collections::HashMap<(u32, i16), (i16, i16, i16, i16)>,
        dialog_ptr: u32,
    ) -> i16 {
        let (top, left, _, _) = bounds;
        let local_v = screen_v - top;
        let local_h = screen_h - left;
        for (i, item) in items.iter().enumerate() {
            let item_no = (i + 1) as i16;
            // For popup userItems, use the original DITL rect (before SetDItem
            // narrowed it) so clicks on the full popup area register.
            let mut rect = popup_original_rects
                .get(&(dialog_ptr, item_no))
                .copied()
                .unwrap_or(item.rect);
            let base_type = item.item_type & 0x7F;
            if (4..=7).contains(&base_type) {
                if let Some(ctrl_handle) = self.dialog_control_handle_for_item(dialog_ptr, item_no)
                {
                    let ctrl_ptr = bus.read_long(ctrl_handle);
                    if ctrl_ptr != 0 && bus.read_byte(ctrl_ptr + 16) != 0 {
                        rect = (
                            bus.read_word(ctrl_ptr + 8) as i16,
                            bus.read_word(ctrl_ptr + 10) as i16,
                            bus.read_word(ctrl_ptr + 12) as i16,
                            bus.read_word(ctrl_ptr + 14) as i16,
                        );
                    }
                }
            }
            let (it, il, ib, ir) = rect;
            if local_v >= it && local_v < ib && local_h >= il && local_h < ir {
                return item_no;
            }
        }
        0
    }

    fn dialog_button_hit_test(
        items: &[DialogItem],
        bounds: (i16, i16, i16, i16),
        screen_v: i16,
        screen_h: i16,
    ) -> i16 {
        for (i, item) in items.iter().enumerate() {
            let base_type = item.item_type & 0x7F;
            let is_disabled = (item.item_type & 0x80) != 0;
            if base_type != 4 || is_disabled {
                continue;
            }
            let (top, left, bottom, right) = Self::dialog_item_screen_rect(bounds, item.rect);
            if screen_v >= top && screen_v < bottom && screen_h >= left && screen_h < right {
                return (i + 1) as i16;
            }
        }
        0
    }

    fn is_plain_modal_user_item(
        &self,
        tracking: &DialogTrackingState,
        item_no: i16,
        item: &DialogItem,
    ) -> bool {
        let base_type = item.item_type & 0x7F;
        let is_disabled = (item.item_type & 0x80) != 0;
        base_type == 0
            && !is_disabled
            && item.proc_ptr == 0
            && !tracking.game_managed
            && !self
                .dialog_item_popup_menus
                .contains_key(&(tracking.dialog_ptr, item_no))
            && !self
                .dialog_popup_candidate_items
                .contains(&(tracking.dialog_ptr, item_no))
    }

    fn dialog_plain_user_item_hit_test(
        &self,
        tracking: &DialogTrackingState,
        screen_v: i16,
        screen_h: i16,
    ) -> i16 {
        for (i, item) in tracking.items.iter().enumerate() {
            let item_no = (i + 1) as i16;
            if !self.is_plain_modal_user_item(tracking, item_no, item) {
                continue;
            }
            let (top, left, bottom, right) =
                Self::dialog_item_screen_rect(tracking.bounds, item.rect);
            if screen_v >= top && screen_v < bottom && screen_h >= left && screen_h < right {
                return item_no;
            }
        }
        0
    }

    pub(crate) fn pending_dialog_plain_user_item_mouse_down(&self) -> bool {
        let Some(tracking) = self.dialog_tracking.as_ref() else {
            return false;
        };
        if tracking.active_user_item.is_some() {
            return true;
        }
        let Some(event) = self
            .event_queue
            .iter()
            .find(|event| matches!(event.what, 1 | 2 | 3 | 4 | 6))
        else {
            return false;
        };
        event.what == 1
            && self.dialog_plain_user_item_hit_test(tracking, event.where_v, event.where_h) > 0
    }

    pub(crate) fn mouse_down_over_dialog_button(&self) -> bool {
        if !self.mouse_button {
            return false;
        }
        let Some(tracking) = self.dialog_tracking.as_ref() else {
            return false;
        };
        Self::dialog_button_hit_test(
            &tracking.items,
            tracking.bounds,
            self.mouse_pos.0,
            self.mouse_pos.1,
        ) > 0
    }

    pub(crate) fn mouse_down_over_dialog_plain_user_item(&self) -> bool {
        if !self.mouse_button {
            return false;
        }
        let Some(tracking) = self.dialog_tracking.as_ref() else {
            return false;
        };
        tracking.active_user_item.is_some()
            || self.dialog_plain_user_item_hit_test(tracking, self.mouse_pos.0, self.mouse_pos.1)
                > 0
    }

    fn read_guest_event_record(bus: &MacMemoryBus, event_ptr: u32) -> (u16, u32, i16, i16, u16) {
        if event_ptr == 0 {
            return (0, 0, 0, 0, 0);
        }
        (
            bus.read_word(event_ptr),
            bus.read_long(event_ptr + 2),
            bus.read_word(event_ptr + 10) as i16,
            bus.read_word(event_ptr + 12) as i16,
            bus.read_word(event_ptr + 14),
        )
    }

    fn front_dialog_ptr(&self) -> Option<u32> {
        let dialog_ptr = self.front_window;
        if dialog_ptr != 0 && self.dialog_items.contains_key(&dialog_ptr) {
            Some(dialog_ptr)
        } else {
            None
        }
    }

    fn front_retained_modal_dialog(
        &self,
        bus: &MacMemoryBus,
    ) -> Option<(u32, (i16, i16, i16, i16))> {
        if self.dialog_tracking.is_some() {
            return None;
        }
        let dialog_ptr = self.front_dialog_ptr()?;
        // This retained-click path is only for dialogs whose event handling
        // has already entered our ModalDialog HLE and returned with the
        // dialog still visible. Dialogs created by GetNewDialog/NewDialog
        // but handled by the application through WaitNextEvent, DialogSelect,
        // or custom Control/TextEdit/Window Manager code must keep receiving
        // their queued events so the app can decide how to respond.
        // Macintosh Toolbox Essentials 1992, pp. 6-136, 6-138..6-141.
        if !self.dialog_modal_entered.contains(&dialog_ptr) {
            return None;
        }
        if !self.window_visible(bus, dialog_ptr) {
            return None;
        }
        let proc_id = self.dialog_window_proc_id(bus, dialog_ptr);
        // Dialog-box WDEFs 1/2/3 are modal-box variants; 5 is the
        // movable modal dialog. noGrowDocProc (4) is modeless and must
        // continue to pass events to the application.
        // Inside Macintosh Volume I, I-273; Macintosh Toolbox Essentials
        // 1992, p. 6-137.
        if !matches!(proc_id, 1 | 2 | 3 | 5) {
            return None;
        }
        Some((dialog_ptr, Self::dialog_screen_bounds(bus, dialog_ptr)))
    }

    fn front_app_owned_modal_dialog(
        &self,
        bus: &MacMemoryBus,
    ) -> Option<(u32, (i16, i16, i16, i16))> {
        if self.dialog_tracking.is_some() || self.retained_modal_dialog_click.is_some() {
            return None;
        }
        let dialog_ptr = self.front_dialog_ptr()?;
        if self.dialog_modal_entered.contains(&dialog_ptr) || !self.window_visible(bus, dialog_ptr)
        {
            return None;
        }
        let proc_id = self.dialog_window_proc_id(bus, dialog_ptr);
        if !matches!(proc_id, 1 | 2 | 3 | 5) {
            return None;
        }
        Some((dialog_ptr, Self::dialog_screen_bounds(bus, dialog_ptr)))
    }

    pub(crate) fn begin_app_owned_modal_dialog_button_tracking(
        &mut self,
        bus: &mut MacMemoryBus,
        event: &QueuedEvent,
    ) {
        if event.what != 1 {
            return;
        }
        let Some((dialog_ptr, bounds)) = self.front_app_owned_modal_dialog(bus) else {
            return;
        };
        if !Self::dialog_contains_screen_point(bounds, event.where_v, event.where_h) {
            return;
        }

        let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() else {
            return;
        };
        Self::refresh_ditl_proc_ptrs(bus, dialog_ptr, &mut items);
        let mut hit = self.dialog_item_hit_test(
            bus,
            &items,
            bounds,
            event.where_v,
            event.where_h,
            &self.dialog_popup_original_rects,
            dialog_ptr,
        );
        if hit <= 0 {
            hit = Self::dialog_button_hit_test(&items, bounds, event.where_v, event.where_h);
        }
        self.dialog_items.insert(dialog_ptr, items.clone());
        if hit <= 0 {
            return;
        }

        let item = &items[(hit - 1) as usize];
        let base_type = item.item_type & 0x7F;
        let is_disabled = (item.item_type & 0x80) != 0;
        if base_type != 4 || is_disabled {
            return;
        }

        // Some games create a modal DLOG and run their own WaitNextEvent
        // loop. They still expect the Dialog Manager's standard push-button
        // press/release affordance for modal WDEFs, but the mouseDown must
        // remain deliverable to app code. Track only enabled DITL buttons and
        // finish the press on mouseUp; DialogSelect cancels this state if the
        // app explicitly asks the Dialog Manager to handle the event.
        // Macintosh Toolbox Essentials 1992, pp. 6-136, 6-138..6-141.
        let rect = Self::dialog_item_screen_rect(bounds, item.rect);
        self.invert_button_rect(bus, rect.0, rect.1, rect.2, rect.3);
        self.retained_modal_dialog_click = Some(RetainedModalDialogClickState {
            dialog_ptr,
            item_no: hit,
            rect: item.rect,
            title: item.text.clone(),
            is_default: false,
            highlighted: true,
            delivered_to_app: true,
        });
    }

    fn cancel_app_owned_modal_dialog_button_tracking(
        &mut self,
        bus: &mut MacMemoryBus,
        dialog_ptr: u32,
    ) {
        let should_cancel = self
            .retained_modal_dialog_click
            .as_ref()
            .is_some_and(|click| click.dialog_ptr == dialog_ptr && click.delivered_to_app);
        if !should_cancel {
            return;
        }
        let Some(click) = self.retained_modal_dialog_click.take() else {
            return;
        };
        if click.item_no > 0 && click.highlighted {
            let bounds = Self::dialog_screen_bounds(bus, click.dialog_ptr);
            let rect = Self::dialog_item_screen_rect(bounds, click.rect);
            self.invert_button_rect(bus, rect.0, rect.1, rect.2, rect.3);
        }
    }

    fn dialog_from_window_event(&self, what: u16, message: u32) -> Option<u32> {
        match what {
            6 | 8 if self.dialog_items.contains_key(&message) => Some(message),
            _ => self.front_dialog_ptr(),
        }
    }

    fn dialog_contains_screen_point(bounds: (i16, i16, i16, i16), v: i16, h: i16) -> bool {
        v >= bounds.0 && v < bounds.2 && h >= bounds.1 && h < bounds.3
    }

    fn point_in_screen_rect(v: i16, h: i16, rect: (i16, i16, i16, i16)) -> bool {
        v >= rect.0 && v < rect.2 && h >= rect.1 && h < rect.3
    }

    fn close_dialog_window<C: CpuOps>(
        &mut self,
        bus: &mut MacMemoryBus,
        cpu: &mut C,
        dialog_ptr: u32,
        dispose_storage: bool,
    ) {
        let was_front = self.front_window == dialog_ptr;
        let exposed_rect = self.window_bounds;
        let initial_draw_deferred = self.dialog_initial_draw_deferred.remove(&dialog_ptr);

        if was_front && !initial_draw_deferred {
            if let Some(saved) = self.dialog_saved_pixels.remove(&dialog_ptr) {
                self.restore_dialog_pixels(bus, self.window_bounds, &saved);
            }
        } else {
            self.dialog_saved_pixels.remove(&dialog_ptr);
        }

        self.dialog_visible_snapshots.remove(&dialog_ptr);
        self.dialog_modal_entered.remove(&dialog_ptr);
        if self
            .retained_modal_dialog_click
            .as_ref()
            .is_some_and(|click| click.dialog_ptr == dialog_ptr)
        {
            self.retained_modal_dialog_click = None;
        }

        self.dispose_dialog_owned_items(bus, dialog_ptr);
        if dispose_storage {
            self.dispose_dialog_record_and_item_list(bus, dialog_ptr);
        }

        self.untrack_window(bus, dialog_ptr);

        if was_front {
            if let Some((prev_window, prev_bounds, prev_proc_id, prev_title)) =
                self.window_stack.pop()
            {
                self.set_current_port_state(bus, cpu, prev_window, None);
                self.front_window = prev_window;
                self.window_bounds = prev_bounds;
                self.window_proc_id = prev_proc_id;
                self.window_title = prev_title;
                if prev_window != 0 {
                    bus.write_byte(prev_window + 111, 0xFF);
                    if !self.event_queue.iter().any(|event| {
                        event.what == 8
                            && event.message == prev_window
                            && (event.modifiers & 1) != 0
                    }) {
                        self.event_queue
                            .push_back(crate::trap::dispatch::QueuedEvent {
                                what: 8,
                                message: prev_window,
                                where_v: 0,
                                where_h: 0,
                                modifiers: 1,
                            });
                    }
                    self.draw_single_window_chrome_inline(bus, prev_window, true);
                    let exposed_local = (
                        exposed_rect.0.saturating_sub(prev_bounds.0),
                        exposed_rect.1.saturating_sub(prev_bounds.1),
                        exposed_rect.2.saturating_sub(prev_bounds.0),
                        exposed_rect.3.saturating_sub(prev_bounds.1),
                    );
                    self.invalidate_window_rect(bus, prev_window, exposed_local);
                    self.queue_window_update_event(prev_window);
                }
            }
        }
    }

    pub(crate) fn consume_retained_modal_dialog_event<C: CpuOps>(
        &mut self,
        cpu: &mut C,
        bus: &mut MacMemoryBus,
        event: &QueuedEvent,
    ) -> bool {
        match event.what {
            1 => {
                let Some((dialog_ptr, bounds)) = self.front_retained_modal_dialog(bus) else {
                    return false;
                };

                if !Self::dialog_contains_screen_point(bounds, event.where_v, event.where_h) {
                    // Modal dialogs own the mouse while visible. A real Dialog
                    // Manager click outside the box beeps and does not pass
                    // through to windows behind it.
                    // Inside Macintosh Volume I, I-418; Macintosh Toolbox
                    // Essentials 1992, p. 6-136.
                    self.retained_modal_dialog_click = Some(RetainedModalDialogClickState {
                        dialog_ptr,
                        item_no: 0,
                        rect: (0, 0, 0, 0),
                        title: String::new(),
                        is_default: false,
                        highlighted: false,
                        delivered_to_app: false,
                    });
                    return true;
                }

                let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() else {
                    return false;
                };
                Self::refresh_ditl_proc_ptrs(bus, dialog_ptr, &mut items);
                let mut hit = self.dialog_item_hit_test(
                    bus,
                    &items,
                    bounds,
                    event.where_v,
                    event.where_h,
                    &self.dialog_popup_original_rects,
                    dialog_ptr,
                );
                if hit <= 0 {
                    hit =
                        Self::dialog_button_hit_test(&items, bounds, event.where_v, event.where_h);
                }
                self.dialog_items.insert(dialog_ptr, items.clone());

                if hit <= 0 {
                    self.retained_modal_dialog_click = Some(RetainedModalDialogClickState {
                        dialog_ptr,
                        item_no: 0,
                        rect: (0, 0, 0, 0),
                        title: String::new(),
                        is_default: false,
                        highlighted: false,
                        delivered_to_app: false,
                    });
                    return true;
                }

                let item = &items[(hit - 1) as usize];
                let base_type = item.item_type & 0x7F;
                let is_disabled = (item.item_type & 0x80) != 0;
                if base_type == 4 && !is_disabled {
                    let rect = Self::dialog_item_screen_rect(bounds, item.rect);
                    let is_default = hit == bus.read_word(dialog_ptr + 168) as i16;
                    self.draw_dialog_button_highlight_state(
                        bus, rect, &item.text, is_default, true,
                    );
                    self.retained_modal_dialog_click = Some(RetainedModalDialogClickState {
                        dialog_ptr,
                        item_no: hit,
                        rect: item.rect,
                        title: item.text.clone(),
                        is_default,
                        highlighted: true,
                        delivered_to_app: false,
                    });
                } else {
                    self.retained_modal_dialog_click = Some(RetainedModalDialogClickState {
                        dialog_ptr,
                        item_no: 0,
                        rect: (0, 0, 0, 0),
                        title: String::new(),
                        is_default: false,
                        highlighted: false,
                        delivered_to_app: false,
                    });
                }
                true
            }
            2 => {
                let Some(click) = self.retained_modal_dialog_click.take() else {
                    return false;
                };
                if click.item_no > 0 {
                    let bounds = Self::dialog_screen_bounds(bus, click.dialog_ptr);
                    let rect = Self::dialog_item_screen_rect(bounds, click.rect);
                    if click.highlighted {
                        self.draw_dialog_button_highlight_state(
                            bus,
                            rect,
                            &click.title,
                            click.is_default,
                            false,
                        );
                    }
                    if self.front_window == click.dialog_ptr
                        && Self::point_in_screen_rect(event.where_v, event.where_h, rect)
                    {
                        self.close_dialog_window(bus, cpu, click.dialog_ptr, true);
                        self.capture_gui_frame(
                            bus,
                            &format!("retained_modal_dialog_button_{}", click.item_no),
                        );
                    }
                }
                !click.delivered_to_app
            }
            _ => false,
        }
    }

    fn consume_dialog_mouse_up(&mut self) {
        if let Some(idx) = self.event_queue.iter().position(|e| e.what == 2) {
            self.event_queue.remove(idx);
        }
    }

    fn dialog_item_screen_rect(
        bounds: (i16, i16, i16, i16),
        rect: (i16, i16, i16, i16),
    ) -> (i16, i16, i16, i16) {
        let (it, il, ib, ir) = rect;
        (bounds.0 + it, bounds.1 + il, bounds.0 + ib, bounds.1 + ir)
    }

    fn rects_intersect(a: (i16, i16, i16, i16), b: (i16, i16, i16, i16)) -> bool {
        a.0 < b.2 && a.2 > b.0 && a.1 < b.3 && a.3 > b.1
    }

    fn dialog_item_intersects_bounds(bounds: (i16, i16, i16, i16), item: &DialogItem) -> bool {
        Self::rects_intersect(Self::dialog_item_screen_rect(bounds, item.rect), bounds)
    }

    fn dialog_is_game_managed(bounds: (i16, i16, i16, i16), items: &[DialogItem]) -> bool {
        let mut has_visible_item = false;
        for item in items {
            if !Self::dialog_item_intersects_bounds(bounds, item) {
                continue;
            }
            has_visible_item = true;
            if (item.item_type & 0x7F) != 0 {
                return false;
            }
        }
        has_visible_item
    }

    fn start_dialog_button_flash(
        &mut self,
        bus: &mut MacMemoryBus,
        bounds: (i16, i16, i16, i16),
        item_no: i16,
        rect: (i16, i16, i16, i16),
        title: &str,
        is_default: bool,
        highlighted: bool,
    ) {
        if !highlighted {
            let screen_rect = Self::dialog_item_screen_rect(bounds, rect);
            self.draw_dialog_button_highlight_state(bus, screen_rect, title, is_default, true);
        }
        if let Some(t) = self.dialog_tracking.as_mut() {
            t.flash_remaining = 6;
            t.flash_delay = 3;
            t.flash_item = item_no;
        }
    }

    fn dialog_control_handle_for_item(&self, dialog_ptr: u32, item_no: i16) -> Option<u32> {
        self.dialog_control_handles
            .iter()
            .find_map(|(&handle, &(dlg, item))| {
                if dlg == dialog_ptr && item == item_no {
                    Some(handle)
                } else {
                    None
                }
            })
    }

    fn dialog_control_inactive(&self, bus: &MacMemoryBus, dialog_ptr: u32, item_no: i16) -> bool {
        self.dialog_control_handle_for_item(dialog_ptr, item_no)
            .map(|handle| bus.read_long(handle))
            .is_some_and(|ctrl_ptr| ctrl_ptr != 0 && bus.read_byte(ctrl_ptr + 17) == 255)
    }

    fn begin_dialog_popup_tracking(
        &mut self,
        bus: &mut MacMemoryBus,
        dialog_ptr: u32,
        item_no: i16,
    ) -> bool {
        let Some(ctrl_handle) = self.dialog_control_handle_for_item(dialog_ptr, item_no) else {
            return false;
        };
        let ctrl_ptr = bus.read_long(ctrl_handle);
        if ctrl_ptr == 0 {
            return false;
        }
        let proc_id = self.control_proc_ids.get(&ctrl_ptr).copied().unwrap_or(0);
        if !Self::is_popup_menu_proc_id(proc_id) {
            return false;
        }
        let menu_id =
            self.popup_control_menu_id(bus, ctrl_ptr, bus.read_word(ctrl_ptr + 20) as i16);
        let Some(menu_idx) = self.menus.iter().rposition(|menu| menu.id == menu_id) else {
            return false;
        };

        let dropdown_rect = self.popup_control_dropdown_rect(bus, ctrl_ptr, menu_idx);
        let saved_pixels = self.save_dropdown_pixels(bus, dropdown_rect);
        self.draw_menu_dropdown(bus, menu_idx, dropdown_rect);

        if let Some(tracking) = self.dialog_tracking.as_mut() {
            tracking.active_popup = Some(DialogPopupTrackingState {
                item_no,
                ctrl_handle,
                ctrl_ptr,
                active_menu: menu_idx,
                highlighted_item: 0,
                saved_pixels,
                dropdown_rect,
            });
            true
        } else {
            false
        }
    }

    fn dialog_popup_item_at_point(&self, bus: &MacMemoryBus, mouse_x: i16, mouse_y: i16) -> i16 {
        let Some(popup) = self
            .dialog_tracking
            .as_ref()
            .and_then(|tracking| tracking.active_popup.as_ref())
        else {
            return 0;
        };
        let (top, left, bottom, right) = popup.dropdown_rect;
        if mouse_x < left || mouse_x >= right || mouse_y < top || mouse_y >= bottom {
            return 0;
        }
        let Some(menu) = self.menus.get(popup.active_menu) else {
            return 0;
        };
        let mut item_top = top + 1;
        for (item_idx, item) in menu.items.iter().enumerate() {
            let item_bottom = item_top + self.menu_item_height(bus, item);
            if mouse_y >= item_top && mouse_y < item_bottom {
                if item.text == "-" || !item.enabled {
                    return 0;
                }
                return item_idx as i16 + 1;
            }
            item_top = item_bottom;
        }
        0
    }

    fn handle_dialog_popup_tracking<C: CpuOps>(&mut self, cpu: &mut C, bus: &mut MacMemoryBus) {
        if self.mouse_button {
            let (mv, mh) = self.mouse_pos;
            let new_item = self.dialog_popup_item_at_point(bus, mh, mv);
            let old_item = self
                .dialog_tracking
                .as_ref()
                .and_then(|tracking| tracking.active_popup.as_ref())
                .map(|popup| popup.highlighted_item)
                .unwrap_or(0);
            if new_item != old_item {
                let popup_state = self
                    .dialog_tracking
                    .as_ref()
                    .and_then(|tracking| tracking.active_popup.as_ref())
                    .map(|popup| (popup.active_menu, popup.dropdown_rect));
                if let Some((active_menu, dropdown_rect)) = popup_state {
                    if old_item > 0 {
                        self.invert_dropdown_item_rect(bus, active_menu, dropdown_rect, old_item);
                    }
                    if let Some(popup) = self
                        .dialog_tracking
                        .as_mut()
                        .and_then(|tracking| tracking.active_popup.as_mut())
                    {
                        popup.highlighted_item = new_item;
                    }
                    if new_item > 0 {
                        self.invert_dropdown_item_rect(bus, active_menu, dropdown_rect, new_item);
                    }
                }
            }
            return;
        }

        let Some(popup) = self
            .dialog_tracking
            .as_mut()
            .and_then(|tracking| tracking.active_popup.take())
        else {
            return;
        };
        self.restore_dropdown_pixels(bus, popup.dropdown_rect, &popup.saved_pixels);
        self.consume_dialog_mouse_up();

        let selected_item = popup.highlighted_item;
        if selected_item <= 0 {
            return;
        }

        self.write_control_value(bus, popup.ctrl_handle, selected_item);
        self.draw_control(cpu, bus, popup.ctrl_ptr);

        if self.dialog_tracking.is_some() {
            let (dialog_ptr, edit_item, edit_text, items) = {
                let tracking = self.dialog_tracking.as_mut().unwrap();
                Self::sync_tracking_active_edit_item(tracking);
                (
                    tracking.dialog_ptr,
                    tracking.edit_item,
                    tracking.edit_text.clone(),
                    tracking.items.clone(),
                )
            };
            self.flush_dialog_edit_item_texts(bus, dialog_ptr, &items, edit_item, &edit_text);
        }
        if let Some((bounds, game_managed)) = self
            .dialog_tracking
            .as_ref()
            .map(|tracking| (tracking.bounds, tracking.game_managed))
        {
            if !game_managed {
                let rendered = self.save_dialog_pixels(bus, bounds);
                if let Some(tracking) = self.dialog_tracking.as_mut() {
                    tracking.rendered_pixels = rendered;
                    tracking.rendered_pixels_final = true;
                }
            }
        }

        let Some(saved) = self.dialog_tracking.take() else {
            return;
        };
        self.persist_visible_dialog_snapshot(bus, &saved);
        self.dialog_saved_pixels
            .insert(saved.dialog_ptr, saved.saved_pixels);
        if saved.item_hit_ptr != 0 {
            bus.write_word(saved.item_hit_ptr, popup.item_no as u16);
        }
        cpu.write_reg(Register::A7, saved.stack_ptr + 8);
    }

    fn handle_dialog_button_tracking(&mut self, bus: &mut MacMemoryBus) {
        let Some((dialog_ptr, bounds, item_no, rect, title, is_default, highlighted, item_type)) =
            self.dialog_tracking.as_ref().and_then(|tracking| {
                tracking.active_button.as_ref().map(|button| {
                    (
                        tracking.dialog_ptr,
                        tracking.bounds,
                        button.item_no,
                        button.rect,
                        button.title.clone(),
                        button.is_default,
                        button.highlighted,
                        tracking
                            .items
                            .get(button.item_no.saturating_sub(1) as usize)
                            .map(|item| item.item_type)
                            .unwrap_or(4),
                    )
                })
            })
        else {
            return;
        };

        let (top, left, bottom, right) = Self::dialog_item_screen_rect(bounds, rect);
        let (mouse_v, mouse_h) = self.mouse_pos;
        let inside = mouse_v >= top && mouse_v < bottom && mouse_h >= left && mouse_h < right;

        if self.mouse_button {
            if inside != highlighted {
                self.draw_dialog_button_highlight_state(
                    bus,
                    (top, left, bottom, right),
                    &title,
                    is_default,
                    inside,
                );
                if let Some(button) = self
                    .dialog_tracking
                    .as_mut()
                    .and_then(|tracking| tracking.active_button.as_mut())
                {
                    button.highlighted = inside;
                }
                self.record_modal_dialog_input_trace(
                    "tracking_update",
                    dialog_ptr,
                    bounds,
                    item_no,
                    Some(item_type),
                    Some(inside),
                    "pending",
                    if inside {
                        "button_highlighted"
                    } else {
                        "button_unhighlighted"
                    },
                );
            }
            return;
        }

        self.consume_dialog_mouse_up();
        if let Some(t) = self.dialog_tracking.as_mut() {
            t.active_button = None;
        }

        if inside {
            self.record_modal_dialog_input_trace(
                "release",
                dialog_ptr,
                bounds,
                item_no,
                Some(item_type),
                Some(true),
                "pending",
                "start_flash",
            );
            self.start_dialog_button_flash(
                bus,
                bounds,
                item_no,
                rect,
                &title,
                is_default,
                highlighted,
            );
        } else if highlighted {
            self.draw_dialog_button_highlight_state(
                bus,
                (top, left, bottom, right),
                &title,
                is_default,
                false,
            );
            self.record_modal_dialog_input_trace(
                "release",
                dialog_ptr,
                bounds,
                item_no,
                Some(item_type),
                Some(false),
                "pending",
                "button_no_selection",
            );
        } else {
            self.record_modal_dialog_input_trace(
                "release",
                dialog_ptr,
                bounds,
                item_no,
                Some(item_type),
                Some(false),
                "pending",
                "button_no_selection",
            );
        }
    }

    fn handle_dialog_user_item_tracking<C: CpuOps>(&mut self, cpu: &mut C, bus: &mut MacMemoryBus) {
        let Some((bounds, item_no, rect)) = self.dialog_tracking.as_ref().and_then(|tracking| {
            tracking
                .active_user_item
                .as_ref()
                .map(|user_item| (tracking.bounds, user_item.item_no, user_item.rect))
        }) else {
            return;
        };

        if self.mouse_button {
            return;
        }

        self.consume_dialog_mouse_up();
        if let Some(t) = self.dialog_tracking.as_mut() {
            t.active_user_item = None;
        }

        let (top, left, bottom, right) = Self::dialog_item_screen_rect(bounds, rect);
        let (mouse_v, mouse_h) = self.mouse_pos;
        let inside = mouse_v >= top && mouse_v < bottom && mouse_h >= left && mouse_h < right;
        if !inside {
            return;
        }

        let (dlg_ptr, edit_item, edit_text, items) = {
            let tracking = self.dialog_tracking.as_mut().unwrap();
            Self::sync_tracking_active_edit_item(tracking);
            (
                tracking.dialog_ptr,
                tracking.edit_item,
                tracking.edit_text.clone(),
                tracking.items.clone(),
            )
        };
        self.flush_dialog_edit_item_texts(bus, dlg_ptr, &items, edit_item, &edit_text);
        let saved = self.dialog_tracking.take().unwrap();
        self.persist_visible_dialog_snapshot(bus, &saved);
        self.dialog_saved_pixels
            .insert(saved.dialog_ptr, saved.saved_pixels);
        if saved.item_hit_ptr != 0 {
            bus.write_word(saved.item_hit_ptr, item_no as u16);
        }
        cpu.write_reg(Register::A7, saved.stack_ptr + 8);
    }

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

            // InitDialogs ($A97B)
            // PROCEDURE InitDialogs(resumeProc: ProcPtr);
            // Inside Macintosh Volume I, I-411.
            //
            // Performs the 3 documented Dialog Manager init steps:
            //   1. Stores resumeProc into the ResumeProc low-mem
            //      global at $0A8C, for later access by the System
            //      Error Handler when a fatal system error occurs
            //      (NIL is the documented default — no resume).
            //   2. Installs the standard sound procedure (we store
            //      NIL into DABeeper at $0A9C since Systemless's HLE
            //      doesn't model menu-bar-blink sound; ErrorSound
            //      ($A98C) can override later).
            //   3. Passes empty strings to ParamText — implemented
            //      as zeroing the 4-handle DAStrings array at
            //      $0AA0 (16 bytes = 4 × Handle, all NIL meaning
            //      "no substitution" so dialog/alert text rendered
            //      with `^0`..`^3` escapes prints the raw escape).
            //
            // Also resets AlertStage at $0A9A to 0 so the first
            // Alert*-family call starts at stage 1 (per IM:I I-417
            // — InitDialogs is the canonical place to ensure
            // alert state is fresh per app launch). ANumber at
            // $0A98 is left untouched since IM:I I-423 only
            // documents it as "the resource ID of the last alert
            // that occurred" with no init contract.
            //
            // Pop = 4 bytes (resumeProc ProcPtr).
            // InitDialogs ($A97B): Per IM:I I-411: stores resumeProc at $0A8C (ResumeProc global), zeros DABeeper at $0A9C ("no sound" default per HLE), zeros AlertStage at $0A9A (so first Alert call starts at stage 1), zeros 16-byte DAStrings array at $0AA0..$0AAF (4 NIL ParamText handles); pops 4 bytes
            (true, 0x17B) => {
                let sp = cpu.read_reg(Register::A7);
                let resume_proc = bus.read_long(sp);
                use crate::memory::globals::addr;
                bus.write_long(addr::RESUME_PROC, resume_proc);
                bus.write_long(addr::DA_BEEPER, 0);
                bus.write_word(addr::ALERT_STAGE, 0);
                // Zero the 4-handle DAStrings array (16 bytes).
                for i in 0..4u32 {
                    bus.write_long(addr::DA_STRINGS + i * 4, 0);
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // GetNewDialog ($A97C)
            // Creates a dialog from DLOG and DITL resources.
            // FUNCTION GetNewDialog (dialogID: INTEGER; dStorage: Ptr;
            //     behind: WindowPtr) : DialogPtr;
            // Inside Macintosh Volume I, I-424
            //
            // Stack: SP+0 behind(4), SP+4 dStorage(4), SP+8 dialogID(2),
            //        SP+10 result(4).
            // Honors `behind` at SP+0 via finish_dialog_creation —
            // see NewDialog/NewCDialog and window.rs apply_behind_parameter.
            // GetNewDialog ($A97C): Parses DLOG + DITL resources, creates DialogRecord, stores parsed items for ModalDialog/GetDItem; saves background pixels before drawing so ModalDialog can restore clean background on dismiss
            (true, 0x17C) => {
                let sp = cpu.read_reg(Register::A7);
                let behind = bus.read_long(sp);
                let storage_ptr = bus.read_long(sp + 4);
                let dialog_id = bus.read_word(sp + 8) as i16;
                eprintln!("[TRAP] GetNewDialog({})", dialog_id);

                // Look up DLOG resource
                let dlog_ptr = self
                    .find_resource_any(*b"DLOG", dialog_id)
                    .map(|(_, ptr)| ptr);

                if let Some(dlog_data) = dlog_ptr {
                    let dlog_len = bus.get_alloc_size(dlog_data).unwrap_or(0);
                    let (raw_bounds, proc_id, visible, items_id, title, position) =
                        Self::parse_dlog(bus, dlog_data, dlog_len);
                    let mut bounds = raw_bounds;

                    // Look up DITL resource
                    let ditl_info = self
                        .find_resource_any(*b"DITL", items_id)
                        .map(|(_, ptr)| ptr);

                    let Some(ditl_data) = ditl_info else {
                        eprintln!(
                            "[TRAP] GetNewDialog({}): DITL {} not found",
                            dialog_id, items_id
                        );
                        // MTE 1992 p. 6-114: GetNewDialog returns NIL if
                        // either the DLOG or item-list resource cannot be
                        // read. Resource Manager ResError reports
                        // resNotFound (-192) for missing resources.
                        bus.write_word(0x0A60, Self::RES_NOT_FOUND as u16);
                        bus.write_long(sp + 10, 0);
                        cpu.write_reg(Register::A7, sp + 10);
                        return Some(Ok(()));
                    };
                    bus.write_word(0x0A60, 0);
                    let ditl_len = bus.get_alloc_size(ditl_data).unwrap_or(0);
                    let items = Self::parse_ditl(bus, ditl_data, ditl_len);

                    // Apply System 7 DLOG positioning constants.
                    // Macintosh Toolbox Essentials 1992, pp. 4-125 to 4-126.
                    bounds = self.positioned_dialog_bounds(bounds, position);

                    eprintln!(
                        "[TRAP] GetNewDialog({}) bounds=({},{},{},{}) raw_bounds=({},{},{},{}) position=${:04X} len={} procID={} items={} title=\"{}\"",
                        dialog_id, bounds.0, bounds.1, bounds.2, bounds.3,
                        raw_bounds.0, raw_bounds.1, raw_bounds.2, raw_bounds.3,
                        position,
                        dlog_len,
                        proc_id, items.len(), title
                    );
                    for (i, item) in items.iter().enumerate() {
                        let base_type = item.item_type & 0x7F;
                        let type_name = match base_type {
                            4 => "button",
                            5 => "checkbox",
                            6 => "radio",
                            7 => "resCtrl",
                            8 => "statText",
                            16 => "editText",
                            32 => "icon",
                            64 => "picture",
                            0 => "userItem",
                            _ => "unknown",
                        };
                        if base_type == 32 || base_type == 64 {
                            eprintln!(
                                "[TRAP]   item {}: type={}({}) rect=({},{},{},{}) resID={}",
                                i + 1,
                                type_name,
                                item.item_type,
                                item.rect.0,
                                item.rect.1,
                                item.rect.2,
                                item.rect.3,
                                item.resource_id
                            );
                        } else {
                            eprintln!(
                                "[TRAP]   item {}: type={}({}) rect=({},{},{},{}) text=\"{}\"",
                                i + 1,
                                type_name,
                                item.item_type,
                                item.rect.0,
                                item.rect.1,
                                item.rect.2,
                                item.rect.3,
                                item.text
                            );
                        }
                    }

                    let items_handle = if let Some((_, ditl_handle_ptr)) =
                        self.find_resource_any(*b"DITL", items_id)
                    {
                        let handle = bus.alloc(4);
                        bus.write_long(handle, ditl_handle_ptr);
                        Self::duplicate_handle_data(bus, handle)
                    } else {
                        0
                    };
                    // Honor the DLOG resource's visible flag per IM:I I-424.
                    let dlg_ptr = self.finish_dialog_creation(
                        bus,
                        cpu,
                        storage_ptr,
                        bounds,
                        &title,
                        visible,
                        proc_id,
                        false,
                        0,
                        items_handle,
                        items,
                    );
                    // Install any 'pltt' resource whose id matches the
                    // dialog id onto the freshly-created window. This
                    // mirrors what GetNewWindow / NewCWindow do in
                    // window.rs and is what the real Window Manager
                    // does: a Window with an associated palette gets
                    // its palette activated when the window is created
                    // and made active. Without this hook, a dialog
                    // whose PICT items rely on an auto-installed
                    // palette (e.g. EV's landing-scene dialog id 1000)
                    // renders through whatever canonical CLUT happens
                    // to be live, producing palette-mismatched colour
                    // noise.  Inside Macintosh Volume VI, 20-12 to
                    // 20-13 (palette association and activation).
                    if dlg_ptr != 0 {
                        let palette = self.copy_palette_resource(bus, dialog_id);
                        if palette != 0 {
                            self.set_window_palette_association(dlg_ptr, palette, -0x2000);
                            self.activate_palette_for_window(bus, dlg_ptr);
                        }
                        self.apply_behind_parameter(bus, dlg_ptr, behind);
                    }
                    bus.write_long(sp + 10, dlg_ptr);
                } else {
                    eprintln!("[TRAP] GetNewDialog({}): DLOG not found -> NIL", dialog_id);
                    // MTE 1992 p. 6-114: GetNewDialog returns NIL if the
                    // DLOG resource cannot be read. Resource Manager ResError
                    // reports resNotFound (-192) for missing resources.
                    bus.write_word(0x0A60, Self::RES_NOT_FOUND as u16);
                    bus.write_long(sp + 10, 0);
                }
                cpu.write_reg(Register::A7, sp + 10);
                Ok(())
            }

            // Alert ($A985) / StopAlert ($A986) / NoteAlert ($A987)
            // / CautionAlert ($A988)
            //
            // FUNCTION Alert(alertID: INTEGER; filterProc: ProcPtr): INTEGER;
            // (and identical signatures for Stop/Note/Caution Alert)
            //
            // The four alert variants differ only in the ICON they
            // display (none / Stop hand / Note speaker / Caution
            // triangle); their dispatch into the ALRT template +
            // ALRT stages logic is identical. OK-only alerts collapse
            // to "return the bold/default item for the current stage,
            // increment AlertStage." Multi-button alerts enter the
            // normal dialog tracking loop so a script or user can
            // choose a non-default button before the trap returns.
            //
            // ALRT template per IM:I I-422:
            //   +0  boundsRect:   Rect (8 bytes)
            //   +8  itemsID:      INTEGER (DITL resource ID)
            //   +10 stages:       INTEGER (16-bit, 4 nibbles, 1
            //                     per stage 1..4 from low nibble
            //                     to high). Each nibble:
            //                       bit 3:    boldItmNum flag
            //                                 (0 = item 1 bold/
            //                                 default, 1 = item 2
            //                                 bold/default)
            //                       bit 2:    boxDrawn flag
            //                                 (alert is shown if
            //                                 set; suppressed if
            //                                 clear)
            //                       bits 0-1: soundNum (0..3 —
            //                                 0=silent, 1=note,
            //                                 2=caution, 3=stop)
            //
            // The current alert stage lives in the AlertStage
            // low-mem global at $0A9A (1 byte, holds 0..3 mapping
            // to stages 1..4). After each Alert*-family call the
            // byte is incremented and capped at 3, so the fourth
            // and subsequent calls all use the stage-4 nibble.
            // ResetAlrtStage ($A98B) zeros the byte; GetAlrtStage
            // ($A9B7) returns it.
            //
            // HLE compromise: filterProc is NOT invoked (no guest-
            // fn dispatch infrastructure for the ProcPtr argument
            // — same compromise as Pack1 LSearch and other guest-
            // ProcPtr-taking traps).
            // The missing-ALRT path is a defensive probe guard:
            // return -1 but leave AlertStage and ANumber exactly as
            // the caller left them.
            //
            // Inside Macintosh Volume I, I-417..I-422 (Alert
            // family + ALRT template); Macintosh Toolbox Essentials
            // 1992, 6-105..6-119 (System 7 alert dispatch).
            // Alert ($A985): Looks up ALRT, parses stages word at +10 per IM:I I-422, returns bold item (1 or 2) for current visible AlertStage ($0A9A) or -1 if ALRT missing / boxDrwn is clear; multi-button alerts track user/script button hits before returning; increments AlertStage capped at 3; filterProc NOT invoked
            // StopAlert ($A986): Same as Alert with Stop icon; identical dispatch path
            // NoteAlert ($A987): Same as Alert with Note icon; identical dispatch path
            // CautionAlert ($A988): Same as Alert with Caution icon; identical dispatch path
            (true, 0x185) | (true, 0x186) | (true, 0x187) | (true, 0x188) => {
                const ALERT_MISSING_RESOURCE_RESULT: i16 = -1;
                let sp = cpu.read_reg(Register::A7);
                if self.dialog_tracking.is_some() {
                    self.handle_interactive_alert_refire(cpu, bus);
                    return Some(Ok(()));
                }
                let filter_proc = bus.read_long(sp);
                let alert_id = bus.read_word(sp + 4) as i16;
                let trap_name = match trap_num {
                    0x185 => "Alert",
                    0x186 => "StopAlert",
                    0x187 => "NoteAlert",
                    0x188 => "CautionAlert",
                    _ => "Alert?",
                };
                let alert_stage_before = bus.read_word(crate::memory::globals::addr::ALERT_STAGE);
                let anumber_before = bus.read_word(crate::memory::globals::addr::ANUMBER);
                let mut alert_trace_stage: Option<(u16, u16, u32, u32)> = None;
                // Look up the ALRT resource and, if present, also pull
                // the referenced DITL's static-text items so the trap
                // log surfaces the alert message before we silently
                // dismiss it. Mid-90s titles (Bumbler, Steel Fighters,
                // etc.) emit StopAlert+ExitToShell on incompatibility;
                // without the message text in the log there's no way
                // to know *which* check failed without RE'ing the
                // binary. Inside Macintosh Volume I, I-422 (ALRT
                // template) and I-426 (DITL).
                let alrt_ptr = self
                    .find_resource_any(*b"ALRT", alert_id)
                    .map(|(_, ptr)| ptr);
                let result: i16 = if let Some(alrt_data) = alrt_ptr {
                    let alrt_len = bus.get_alloc_size(alrt_data).unwrap_or(0);
                    let (bounds, items_id, stages, position) =
                        Self::parse_alrt(bus, alrt_data, alrt_len);
                    // AlertStage / ACount lives at $0A9A as a
                    // 16-bit WORD (NOT a byte) per IM:I I-423 +
                    // MTb 1992 22620 `#define GetAlertStage()
                    // (* (short*) 0x0A9A)`. On big-endian 68k a
                    // word value 0..3 stores byte 0 at $0A9A and
                    // value at $0A9B, so reading as a byte at
                    // $0A9A would always return 0 — must use
                    // read_word/write_word.
                    let stage_word = bus.read_word(crate::memory::globals::addr::ALERT_STAGE);
                    let (stage_idx, nibble, default_item) =
                        Self::alert_stage_default_item(stages, stage_word);
                    alert_trace_stage = Some((stages, stage_word, stage_idx, nibble));
                    // bit 3 (MSB) of nibble = boldItm per StageList
                    // PACKED RECORD layout (IM:I I-422): boldItm,
                    // boxDrwn, sound[2]. Assembly mask okDismissal=8
                    // and alBit=4 confirm bit masks. MTE 1992 p. 6-106
                    // says Alert returns -1 when boxDrwn is clear.
                    // Increment AlertStage, capped at 3, so the
                    // next call uses the next stage's nibble.
                    let next_stage = ((stage_word as u32) + 1).min(3) as u16;
                    bus.write_word(crate::memory::globals::addr::ALERT_STAGE, next_stage);
                    // ANumber records the resource ID of the last
                    // alert that occurred (IM:I I-423). Apps that
                    // probe ANumber after a sequence of Alert
                    // calls expect this to reflect the most-recent
                    // ID — used by some defensive resume logic.
                    bus.write_word(crate::memory::globals::addr::ANUMBER, alert_id as u16);

                    if let Some(default_item) = default_item {
                        // Until Alert filterProc dispatch is implemented, keep
                        // non-NIL filterProc calls on the old default-item
                        // path. Entering local button tracking would ignore
                        // caller-supplied filter decisions.
                        if filter_proc == 0
                            && self.begin_interactive_alert(
                                cpu,
                                bus,
                                sp,
                                alert_id,
                                filter_proc,
                                bounds,
                                items_id,
                                position,
                                default_item,
                            )
                        {
                            if super::dispatch::trace_dialog_traps_enabled() {
                                eprintln!(
                                    "[TRAP] {} id={} -> interactive dialog (PC=${:08X}) stages=${:04X} alertStage={} stageIdx={} nibble=${:X} defaultItem={}",
                                    trap_name,
                                    alert_id,
                                    cpu.read_reg(Register::PC),
                                    stages,
                                    stage_word,
                                    stage_idx,
                                    nibble,
                                    default_item
                                );
                            }
                            return Some(Ok(()));
                        }
                        default_item
                    } else {
                        ALERT_MISSING_RESOURCE_RESULT
                    }
                } else {
                    bus.write_word(
                        crate::memory::globals::addr::ALERT_STAGE,
                        alert_stage_before,
                    );
                    bus.write_word(crate::memory::globals::addr::ANUMBER, anumber_before);
                    ALERT_MISSING_RESOURCE_RESULT
                };

                if super::dispatch::trace_dialog_traps_enabled() {
                    let mut detail = format!(
                        "[TRAP] {} id={} -> item {} (PC=${:08X})",
                        trap_name,
                        alert_id,
                        result,
                        cpu.read_reg(Register::PC)
                    );
                    if let Some((stages, stage_word, stage_idx, nibble)) = alert_trace_stage {
                        detail.push_str(&format!(
                            " stages=${stages:04X} alertStage={stage_word} stageIdx={stage_idx} nibble=${nibble:X}"
                        ));
                    }
                    if let Some(alrt_data) = alrt_ptr {
                        // ALRT template: bounds(8) + itemsID(2) + stages(2).
                        // Inside Macintosh Volume I, I-422. Some titles
                        // (Bumbler Bee-Luxe in particular) ship ALRT data
                        // whose first 4 bytes don't decode as a Rect — most
                        // likely a non-standard prologue prepended by the
                        // build's PowerPC fragment or resource compiler.
                        // Detect via implausible bounds (top > bottom or
                        // wildly negative coordinates) and suppress the
                        // usual itemsID lookup so the trace reflects that
                        // the resource isn't in the documented format.
                        let alrt_len = bus.get_alloc_size(alrt_data).unwrap_or(0);
                        let ((top, left, bottom, right), items_id, _, _) =
                            Self::parse_alrt(bus, alrt_data, alrt_len);
                        let bounds_plausible = (-32..1024).contains(&top)
                            && (-32..2048).contains(&left)
                            && bottom > top
                            && right > left
                            && (bottom - top) < 1024
                            && (right - left) < 2048;
                        if !bounds_plausible {
                            detail.push_str(&format!(
                                " bounds=({},{},{},{}) — implausible, skipping DITL lookup",
                                top, left, bottom, right
                            ));
                        } else {
                            let ditl_match = self.find_resource_any(*b"DITL", items_id);
                            detail.push_str(&format!(
                                " bounds=({},{},{},{}) itemsID={} ditl={}",
                                top,
                                left,
                                bottom,
                                right,
                                items_id,
                                if ditl_match.is_some() {
                                    "found"
                                } else {
                                    "missing"
                                }
                            ));
                            if let Some((_, ditl_data)) = ditl_match {
                                let ditl_len = bus.get_alloc_size(ditl_data).unwrap_or(0);
                                let items = Self::parse_ditl(bus, ditl_data, ditl_len);
                                for (i, item) in items.iter().enumerate() {
                                    if !item.text.is_empty() {
                                        detail.push_str(&format!(
                                            "\n[TRAP]   item {} (type=${:02X}): {:?}",
                                            i + 1,
                                            item.item_type,
                                            item.text
                                        ));
                                    }
                                }
                            }
                        }
                    }
                    eprintln!("{}", detail);
                }
                bus.write_word(sp + 6, result as u16);
                cpu.write_reg(Register::A7, sp + 6);
                Ok(())
            }

            // IsDialogEvent ($A97F)
            // Tests whether an event should be handled as part of an active
            // modeless or movable modal dialog.
            // FUNCTION IsDialogEvent(theEvent: EventRecord): Boolean;
            // Inside Macintosh Volume I, I-416; Macintosh Toolbox Essentials 1992, 6-138
            (true, 0x17F) => {
                let sp = cpu.read_reg(Register::A7);
                let event_ptr = bus.read_long(sp);
                let (what, message, where_v, where_h, _modifiers) =
                    Self::read_guest_event_record(bus, event_ptr);
                let target_dialog = self.dialog_from_window_event(what, message);
                let result = if let Some(dialog_ptr) = target_dialog {
                    match what {
                        6 | 8 => message == dialog_ptr,
                        1 => Self::dialog_contains_screen_point(
                            Self::dialog_screen_bounds(bus, dialog_ptr),
                            where_v,
                            where_h,
                        ),
                        _ => true,
                    }
                } else {
                    false
                };
                self.record_dialog_input_trace(
                    "A97F",
                    event_ptr,
                    what,
                    message,
                    where_v,
                    where_h,
                    _modifiers,
                    target_dialog,
                    result,
                    "outcome=is_dialog_event",
                );
                bus.write_word(sp + 4, if result { 0xFFFF } else { 0 });
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // DialogSelect ($A980)
            // Handles events for modeless and movable modal dialogs.
            // FUNCTION DialogSelect(theEvent: EventRecord; VAR theDialog: DialogPtr; VAR itemHit: INTEGER): BOOLEAN;
            // Inside Macintosh Volume I, I-417; Macintosh Toolbox Essentials 1992, 6-139
            (true, 0x180) => {
                let sp = cpu.read_reg(Register::A7);
                let item_hit_ptr = bus.read_long(sp);
                let dialog_out_ptr = bus.read_long(sp + 4);
                let event_ptr = bus.read_long(sp + 8);
                let (what, message, where_v, where_h, _modifiers) =
                    Self::read_guest_event_record(bus, event_ptr);
                let mut result = false;
                let mut trace_detail = String::from("outcome=no_dialog_target");

                let target_dialog = self.dialog_from_window_event(what, message);
                if let Some(dialog_ptr) = target_dialog {
                    let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
                    trace_detail = format!(
                        "bounds=({},{},{},{}) outcome=no_item",
                        bounds.0, bounds.1, bounds.2, bounds.3
                    );
                    if let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() {
                        Self::refresh_ditl_proc_ptrs(bus, dialog_ptr, &mut items);
                        match what {
                            6 => {
                                // MTE 1992 p. 6-141: DialogSelect wraps the
                                // update redraw in BeginUpdate/EndUpdate,
                                // calls DrawDialog, and returns FALSE.
                                let update_rect =
                                    Self::region_handle_rect(bus, bus.read_long(dialog_ptr + 122));
                                self.begin_update_window(bus, dialog_ptr);
                                self.update_dialog_window_contents(
                                    bus,
                                    cpu,
                                    dialog_ptr,
                                    update_rect,
                                );
                                self.end_update_window(bus, dialog_ptr);
                                trace_detail = format!(
                                    "bounds=({},{},{},{}) outcome=update_redraw",
                                    bounds.0, bounds.1, bounds.2, bounds.3
                                );
                            }
                            1 if Self::dialog_contains_screen_point(bounds, where_v, where_h) => {
                                let hit = self.dialog_item_hit_test(
                                    bus,
                                    &items,
                                    bounds,
                                    where_v,
                                    where_h,
                                    &self.dialog_popup_original_rects,
                                    dialog_ptr,
                                );
                                if hit > 0 {
                                    let item = &items[(hit - 1) as usize];
                                    let is_disabled = (item.item_type & 0x80) != 0;
                                    trace_detail = format!(
                                        "bounds=({},{},{},{}) item_hit={} item_type=${:02X} disabled={} outcome={}",
                                        bounds.0,
                                        bounds.1,
                                        bounds.2,
                                        bounds.3,
                                        hit,
                                        item.item_type,
                                        if is_disabled { "true" } else { "false" },
                                        if is_disabled {
                                            "disabled_item"
                                        } else {
                                            "enabled_item"
                                        },
                                    );
                                    if trace_dialog_items_enabled() {
                                        eprintln!(
                                            "[DIALOG-SELECT] mouseDown dialog=${:08X} where=({},{}) bounds=({},{},{},{}) hit={} type=${:02X} disabled={}",
                                            dialog_ptr,
                                            where_v,
                                            where_h,
                                            bounds.0,
                                            bounds.1,
                                            bounds.2,
                                            bounds.3,
                                            hit,
                                            item.item_type,
                                            is_disabled,
                                        );
                                    }
                                    if !is_disabled {
                                        self.cancel_app_owned_modal_dialog_button_tracking(
                                            bus, dialog_ptr,
                                        );
                                        if (item.item_type & 0x7F) == 16 {
                                            // MTE 1992 p. 6-139 / IM:I I-417:
                                            // mouseDown in an enabled editText item makes
                                            // that item the active edit field before
                                            // reporting the item hit. TEClick's pixel-to-
                                            // caret mapping remains the documented HLE
                                            // compromise in the TEClick trap.
                                            self.activate_dialog_edit_item(
                                                bus, cpu, dialog_ptr, &items, hit,
                                            );
                                        }
                                        if dialog_out_ptr != 0 {
                                            bus.write_long(dialog_out_ptr, dialog_ptr);
                                        }
                                        if item_hit_ptr != 0 {
                                            bus.write_word(item_hit_ptr, hit as u16);
                                        }
                                        result = true;
                                    }
                                } else {
                                    trace_detail = format!(
                                        "bounds=({},{},{},{}) item_hit=0 outcome=no_item",
                                        bounds.0, bounds.1, bounds.2, bounds.3
                                    );
                                    if trace_dialog_items_enabled() {
                                        eprintln!(
                                            "[DIALOG-SELECT] mouseDown dialog=${:08X} where=({},{}) bounds=({},{},{},{}) hit=0",
                                            dialog_ptr,
                                            where_v,
                                            where_h,
                                            bounds.0,
                                            bounds.1,
                                            bounds.2,
                                            bounds.3,
                                        );
                                    }
                                }
                            }
                            0 => {
                                let (_edit_text, edit_item, _default_item) =
                                    Self::dialog_edit_state(bus, dialog_ptr, &items);
                                trace_detail = format!(
                                    "bounds=({},{},{},{}) edit_item={} outcome=teidle",
                                    bounds.0, bounds.1, bounds.2, bounds.3, edit_item
                                );
                                if edit_item > 0 {
                                    // MTE 1992 p. 6-139 / IM:I I-417:
                                    // DialogSelect calls TEIdle for null events when an
                                    // editText item is present, letting TextEdit advance
                                    // the insertion-caret blink without changing text or
                                    // selection fields.
                                    let text_handle = bus.read_long(dialog_ptr + 160);
                                    self.textedit_idle(cpu, bus, text_handle);
                                }
                            }
                            1 => {
                                trace_detail = format!(
                                    "bounds=({},{},{},{}) item_hit=0 outcome=outside_dialog",
                                    bounds.0, bounds.1, bounds.2, bounds.3
                                );
                                if trace_dialog_items_enabled() {
                                    eprintln!(
                                        "[DIALOG-SELECT] mouseDown dialog=${:08X} where=({},{}) outside bounds=({},{},{},{})",
                                        dialog_ptr,
                                        where_v,
                                        where_h,
                                        bounds.0,
                                        bounds.1,
                                        bounds.2,
                                        bounds.3,
                                    );
                                }
                            }
                            3 | 5 => {
                                let (_edit_text, edit_item, _default_item) =
                                    Self::dialog_edit_state(bus, dialog_ptr, &items);
                                trace_detail = format!(
                                    "bounds=({},{},{},{}) edit_item={} outcome=no_enabled_edittext",
                                    bounds.0, bounds.1, bounds.2, bounds.3, edit_item
                                );
                                if edit_item > 0 {
                                    // IM:I I-417: keyDown/autoKey dialog handling applies to
                                    // editable text items. If no enabled editText item is
                                    // active, DialogSelect returns FALSE.
                                    if let Some(item) = items.get((edit_item - 1) as usize) {
                                        let item_type = item.item_type;
                                        let base_type = item_type & 0x7F;
                                        let is_disabled = (item_type & 0x80) != 0;
                                        trace_detail = format!(
                                            "bounds=({},{},{},{}) edit_item={} item_type=${:02X} disabled={} outcome={}",
                                            bounds.0,
                                            bounds.1,
                                            bounds.2,
                                            bounds.3,
                                            edit_item,
                                            item_type,
                                            if is_disabled { "true" } else { "false" },
                                            if base_type == 16 && !is_disabled {
                                                "enabled_edittext"
                                            } else {
                                                "no_enabled_edittext"
                                            },
                                        );
                                        if base_type == 16 && !is_disabled {
                                            let char_code = (message & 0xFF) as u8;
                                            // MTE 1992 p. 6-139: DialogSelect uses TextEdit
                                            // to handle key-down and auto-key events in
                                            // editable text items before reporting itemHit.
                                            self.apply_dialog_select_key_to_edit_item(
                                                bus, dialog_ptr, &mut items, edit_item, char_code,
                                            );
                                            if dialog_out_ptr != 0 {
                                                bus.write_long(dialog_out_ptr, dialog_ptr);
                                            }
                                            if item_hit_ptr != 0 {
                                                bus.write_word(item_hit_ptr, edit_item as u16);
                                            }
                                            result = true;
                                        }
                                    }
                                }
                            }
                            _ => {}
                        }
                        self.dialog_items.insert(dialog_ptr, items);
                    } else {
                        trace_detail = format!(
                            "bounds=({},{},{},{}) outcome=no_dialog_items",
                            bounds.0, bounds.1, bounds.2, bounds.3
                        );
                        if trace_dialog_items_enabled() {
                            eprintln!(
                                "[DIALOG-SELECT] event what={} dialog=${:08X} has no dialog_items",
                                what, dialog_ptr
                            );
                        }
                    }
                } else if trace_dialog_items_enabled() {
                    eprintln!(
                        "[DIALOG-SELECT] event what={} message=${:08X} where=({},{}) has no dialog target front=${:08X}",
                        what, message, where_v, where_h, self.front_window
                    );
                }
                self.record_dialog_input_trace(
                    "A980",
                    event_ptr,
                    what,
                    message,
                    where_v,
                    where_h,
                    _modifiers,
                    target_dialog,
                    result,
                    &trace_detail,
                );

                bus.write_word(sp + 12, if result { 0xFFFF } else { 0 });
                cpu.write_reg(Register::A7, sp + 12);
                Ok(())
            }

            // DrawDialog ($A981)
            // Draws the entire contents of the specified dialog box.
            // PROCEDURE DrawDialog (theDialog: DialogPtr);
            // Inside Macintosh Volume I, I-417; Macintosh Toolbox Essentials 1992, 6-142
            // DrawDialog ($A981): Renders all dialog items with full DITL support
            (true, 0x181) => {
                let sp = cpu.read_reg(Register::A7);
                let dialog_ptr = bus.read_long(sp);
                if let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() {
                    Self::refresh_ditl_proc_ptrs(bus, dialog_ptr, &mut items);
                    let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
                    let proc_id = self.dialog_window_proc_id(bus, dialog_ptr);
                    let (edit_text, edit_item, default_item) =
                        Self::dialog_edit_state(bus, dialog_ptr, &items);
                    self.dialog_initial_draw_deferred.remove(&dialog_ptr);
                    self.draw_dialog_preserving_user_items(
                        bus,
                        bounds,
                        proc_id,
                        "",
                        &items,
                        default_item,
                        &edit_text,
                        edit_item,
                        false,
                        dialog_ptr,
                        true,
                        false,
                        false,
                    );
                    self.dialog_items.insert(dialog_ptr, items);
                    // MTE 1992 p. 6-142: DrawDialog calls
                    // application-defined item draw procs for userItem
                    // records. Queue them after the HLE redraw so the
                    // runner trampoline executes guest drawing before the
                    // caller resumes.
                    self.queue_modeless_dialog_draw_procs_intersecting(bus, dialog_ptr, None);
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // DisposDialog ($A983)
            // Removes a dialog and frees its storage.
            // PROCEDURE DisposDialog (theDialog: DialogPtr);
            // Inside Macintosh Volume I, I-425
            //
            // DisposDialog internally calls CloseWindow, which on real
            // Mac OS invalidates the window's frame and the Window
            // Manager's PaintBehind / CalcVisBehind restores the
            // content beneath via update events to the back windows.
            //
            // In HLE we emulate PaintBehind by blitting the saved-under
            // pixels captured immediately before the dialog first draws to
            // the visible screen. Without this restore, any app that skips
            // ModalDialog and runs its own event loop leaves a dialog-shaped
            // hole over the window behind.
            //
            // Only restore when `was_front` holds: a non-front dialog
            // would require the saved bounds to still be valid, but
            // by the time we'd get here the window stack has already
            // moved on. In practice modal dialogs are always the front
            // window, so this covers every real case.
            //
            // Inside Macintosh Volume I, I-283 (CloseWindow), I-425 (DisposDialog)
            //
            // Regression coverage:
            //   src/trap/dialog.rs::tests::disposdialog_restores_saved_background_pixels
            //   src/trap/dialog.rs::tests::disposdialog_without_saved_pixels_noop
            //   src/trap/dialog.rs::tests::disposdialog_non_front_does_not_restore
            // DisposDialog ($A983): Frees dialog, cleans up dialog_items, restores saved background pixels (IM:I I-425 PaintBehind)
            (true, 0x183) => {
                let sp = cpu.read_reg(Register::A7);
                let requested_dialog_ptr = bus.read_long(sp);
                let dialog_ptr = requested_dialog_ptr;
                eprintln!("[TRAP] DisposDialog(${:08X})", requested_dialog_ptr);
                self.close_dialog_window(bus, cpu, dialog_ptr, true);
                self.capture_gui_frame(bus, &format!("dispos_dialog_{:08X}", dialog_ptr));
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // ParamText ($A98B)
            // Saves the four strings for ^0..^3 substitution into any
            // subsequently drawn dialog or alert static-text item.
            // PROCEDURE ParamText(param0, param1, param2, param3: Str255);
            // Inside Macintosh Volume I, I-422
            // ParamText ($A98B): Saves all 4 Pascal strings for ^0..^3 dialog/alert text substitution
            (true, 0x18B) => {
                let sp = cpu.read_reg(Register::A7);
                let trap_pc = cpu.read_reg(Register::PC).wrapping_sub(2);
                // Stack layout (top-down): SP+0:param3, +4:param2, +8:param1, +12:param0.
                // Per Inside Macintosh Volume I, I-422, passing NIL for any
                // parameter leaves that slot's previous value unchanged —
                // it's a "set this slot, leave others alone" idiom. Clearing
                // on NIL would erase ^N output that an earlier ParamText
                // had legitimately staged.
                let offsets: [u32; 4] = [12, 8, 4, 0];
                for (i, &off) in offsets.iter().enumerate() {
                    let ptr = bus.read_long(sp + off);
                    if ptr == 0 {
                        continue;
                    }
                    let s = bus.read_pstring(ptr);
                    self.param_text[i] = s.clone();
                    // Write to DAStrings low-memory global ($0AA0 + i*4).
                    // The ROM stores each param string as a StringHandle so
                    // C fixtures can read *(StringHandle*)0x0AA0 to verify
                    // the substitution was stored.
                    // Inside Macintosh Volume I, I-422 (DAStrings global).
                    use crate::memory::globals::addr;
                    let data_len = 1u32 + s.len() as u32;
                    let data_ptr = bus.alloc(data_len);
                    if data_ptr != 0 {
                        bus.write_byte(data_ptr, s.len() as u8);
                        for (j, &b) in s.iter().enumerate() {
                            bus.write_byte(data_ptr + 1 + j as u32, b);
                        }
                        let handle = bus.alloc(4);
                        if handle != 0 {
                            bus.write_long(handle, data_ptr);
                            bus.write_long(addr::DA_STRINGS + i as u32 * 4, handle);
                        }
                    }
                }
                eprintln!(
                    "[TRAP] ParamText pc=${:08X} ^0=\"{}\" ^1=\"{}\" ^2=\"{}\" ^3=\"{}\"",
                    trap_pc,
                    String::from_utf8_lossy(&self.param_text[0]),
                    String::from_utf8_lossy(&self.param_text[1]),
                    String::from_utf8_lossy(&self.param_text[2]),
                    String::from_utf8_lossy(&self.param_text[3]),
                );
                cpu.write_reg(Register::A7, sp + 16);
                Ok(())
            }

            // GetDItem ($A98D)
            // Returns information about a dialog item.
            // PROCEDURE GetDItem (theDialog: DialogPtr; itemNo: INTEGER;
            //     VAR itemType: INTEGER; VAR item: Handle; VAR box: Rect);
            // Inside Macintosh Volume I, I-421
            // GetDItem ($A98D): Returns real item type, handle (with text for statText/editText, proc_ptr for userItem), and rect from dialog_items
            (true, 0x18D) => {
                let sp = cpu.read_reg(Register::A7);
                let box_ptr = bus.read_long(sp);
                let item_handle_ptr = bus.read_long(sp + 4);
                let type_ptr = bus.read_long(sp + 8);
                let item_no = bus.read_word(sp + 12) as i16;
                let dialog_ptr = bus.read_long(sp + 14);

                // Look up real item data
                let found = self.dialog_items.get(&dialog_ptr).and_then(|items| {
                    if item_no > 0 && (item_no as usize) <= items.len() {
                        Some(&items[(item_no - 1) as usize])
                    } else {
                        None
                    }
                });

                if let Some(item) = found {
                    if trace_dialog_items_enabled() && (item.item_type & 0x7F) == 0 {
                        eprintln!(
                            "[DIALOG-ITEM] GetDItem pc=${:08X} dialog=${:08X} item={} type={} proc=${:08X} out_type=${:08X} out_item=${:08X} out_box=${:08X} rect=({},{},{},{})",
                            cpu.read_reg(Register::PC),
                            dialog_ptr,
                            item_no,
                            item.item_type,
                            item.proc_ptr,
                            type_ptr,
                            item_handle_ptr,
                            box_ptr,
                            item.rect.0,
                            item.rect.1,
                            item.rect.2,
                            item.rect.3,
                        );
                    }
                    if type_ptr != 0 {
                        bus.write_word(type_ptr, item.item_type as u16);
                    }
                    if item_handle_ptr != 0 {
                        let current_handle = Self::dialog_item_handle(bus, dialog_ptr, item_no);
                        let base_type = item.item_type & 0x7F;
                        if current_handle != 0 || !(4..=6).contains(&base_type) {
                            bus.write_long(item_handle_ptr, current_handle);
                        } else {
                            // Create a full ControlRecord so draw_control can render it.
                            // ControlRecord layout:
                            //   +0: nextControl (4)
                            //   +4: contrlOwner (4) = window ptr
                            //   +8: contrlRect (8) = top, left, bottom, right
                            //  +16: contrlVis (1) = 255 (visible)
                            //  +17: contrlHilite (1) = 0
                            //  +18: contrlValue (2)
                            //  +20: contrlMin (2)
                            //  +22: contrlMax (2) = 1
                            //  +24: contrlDefProc (4) = procID encoding
                            //  +40: contrlTitle (pascal string)
                            let title_len = item.text.len().min(255);
                            let ctrl_rec = bus.alloc(42 + title_len as u32);
                            bus.write_long(ctrl_rec, 0); // nextControl
                            bus.write_long(ctrl_rec + 4, dialog_ptr); // contrlOwner
                                                                      // contrlRect: dialog-local coordinates (draw_control gets
                                                                      // screen offset from the owner window's PixMap bounds)
                            bus.write_word(ctrl_rec + 8, item.rect.0 as u16);
                            bus.write_word(ctrl_rec + 10, item.rect.1 as u16);
                            bus.write_word(ctrl_rec + 12, item.rect.2 as u16);
                            bus.write_word(ctrl_rec + 14, item.rect.3 as u16);
                            bus.write_byte(ctrl_rec + 16, 255); // contrlVis = visible
                            bus.write_byte(ctrl_rec + 17, 0); // contrlHilite
                            let value = self
                                .dialog_control_values
                                .get(&(dialog_ptr, item_no))
                                .copied()
                                .unwrap_or(0);
                            bus.write_word(ctrl_rec + 18, value as u16);
                            bus.write_word(ctrl_rec + 20, 0); // contrlMin
                            bus.write_word(ctrl_rec + 22, 1); // contrlMax
                                                              // Write title as pascal string at offset 40
                            bus.write_byte(ctrl_rec + 40, title_len as u8);
                            for (i, &ch) in item.text.as_bytes().iter().take(title_len).enumerate()
                            {
                                bus.write_byte(ctrl_rec + 41 + i as u32, ch);
                            }
                            // Map DITL item type to Control Manager procID
                            let proc_id: i16 = match base_type {
                                4 => 0, // btnCtrl → pushButProc
                                5 => 1, // chkCtrl → checkBoxProc
                                6 => 2, // radCtrl → radioButProc
                                _ => 0,
                            };
                            self.control_proc_ids.insert(ctrl_rec, proc_id);
                            let handle = bus.alloc(4);
                            bus.write_long(handle, ctrl_rec);
                            bus.write_long(item_handle_ptr, handle);
                            self.dialog_control_handles
                                .insert(handle, (dialog_ptr, item_no));
                            // Also update the DITL item handle storage
                            Self::set_dialog_item_handle(bus, dialog_ptr, item_no, handle);
                        }
                    }
                    if box_ptr != 0 {
                        bus.write_word(box_ptr, item.rect.0 as u16);
                        bus.write_word(box_ptr + 2, item.rect.1 as u16);
                        bus.write_word(box_ptr + 4, item.rect.2 as u16);
                        bus.write_word(box_ptr + 6, item.rect.3 as u16);
                    }
                    // Some apps use InsertMenu -> GetDItem -> SetDItem to
                    // build a custom popup control backed by a userItem. Do
                    // not promote on GetDItem alone: plain userItem grids also
                    // query their item rectangles and must not inherit a stale
                    // popup-menu association.
                    if let Some(menu_id) = self.last_inserted_menu_id.take() {
                        let enabled_user_item =
                            (item.item_type & 0x7F) == 0 && (item.item_type & 0x80) == 0;
                        if enabled_user_item {
                            if item.proc_ptr != 0 {
                                self.dialog_item_popup_menus
                                    .insert((dialog_ptr, item_no), menu_id);
                                self.dialog_popup_original_rects
                                    .insert((dialog_ptr, item_no), item.rect);
                                self.pending_dialog_popup_menu = None;
                            } else {
                                self.pending_dialog_popup_menu = Some(PendingDialogPopupMenu {
                                    dialog_ptr,
                                    item_no,
                                    menu_id,
                                    rect: item.rect,
                                });
                            }
                        } else {
                            self.pending_dialog_popup_menu = None;
                        }
                    }
                } else {
                    if type_ptr != 0 {
                        bus.write_word(type_ptr, 0);
                    }
                    if item_handle_ptr != 0 {
                        bus.write_long(item_handle_ptr, 0);
                    }
                    if box_ptr != 0 {
                        bus.write_long(box_ptr, 0);
                        bus.write_long(box_ptr + 4, 0);
                    }
                }
                cpu.write_reg(Register::A7, sp + 18);
                Ok(())
            }

            // SetDItem ($A98E)
            // Sets information about a dialog item.
            // PROCEDURE SetDItem (theDialog: DialogPtr; itemNo: INTEGER;
            //     itemType: INTEGER; item: Handle; box: Rect);
            // Inside Macintosh Volume I, I-421
            //
            // Stack layout (box passed as pointer-to-Rect, not inline):
            //   SP+0..3:  &box   (pointer to Rect — MPW compiler passes large
            //                     value params by reference)
            //   SP+4..7:  item   (Handle / ProcPtr)
            //   SP+8..9:  itemType
            //   SP+10..11: itemNo
            //   SP+12..15: theDialog
            // SetDItem ($A98E): Stores item type, rect, and proc_ptr (for userItem); updates both dialog_items and active tracking state
            (true, 0x18E) => {
                let sp = cpu.read_reg(Register::A7);
                let box_ptr = bus.read_long(sp);
                let item_handle = bus.read_long(sp + 4);
                let item_type = bus.read_word(sp + 8) as u8;
                let item_no = bus.read_word(sp + 10) as i16;
                let dialog_ptr = bus.read_long(sp + 12);
                let (box_top, box_left, box_bottom, box_right) = if box_ptr != 0 {
                    (
                        bus.read_word(box_ptr) as i16,
                        bus.read_word(box_ptr + 2) as i16,
                        bus.read_word(box_ptr + 4) as i16,
                        bus.read_word(box_ptr + 6) as i16,
                    )
                } else {
                    (0, 0, 0, 0)
                };

                let base_type = item_type & 0x7F;
                let previous_handle = Self::dialog_item_handle(bus, dialog_ptr, item_no);
                let previous_item = self
                    .dialog_items
                    .get(&dialog_ptr)
                    .and_then(|items| {
                        (item_no > 0)
                            .then(|| items.get((item_no - 1) as usize))
                            .flatten()
                    })
                    .cloned();
                if trace_dialog_items_enabled() {
                    eprintln!(
                        "[DIALOG-ITEM] SetDItem pc=${:08X} sp=${:08X} rawbytes={:02X?} dialog=${:08X} item={} type={} proc=${:08X} rect=({},{},{},{})",
                        cpu.read_reg(Register::PC),
                        sp,
                        (0..24u32).map(|i| bus.read_byte(sp + i)).collect::<Vec<u8>>(),
                        dialog_ptr,
                        item_no,
                        item_type,
                        item_handle,
                        box_top,
                        box_left,
                        box_bottom,
                        box_right,
                    );
                }

                if previous_handle != 0 {
                    self.dialog_item_handles.remove(&previous_handle);
                    self.dialog_control_handles.remove(&previous_handle);
                }
                if let Some(item_handle_addr) =
                    Self::dialog_item_handle_addr(bus, dialog_ptr, item_no)
                {
                    bus.write_long(item_handle_addr, item_handle);
                    bus.write_word(item_handle_addr + 4, box_top as u16);
                    bus.write_word(item_handle_addr + 6, box_left as u16);
                    bus.write_word(item_handle_addr + 8, box_bottom as u16);
                    bus.write_word(item_handle_addr + 10, box_right as u16);
                    bus.write_byte(item_handle_addr + 12, item_type);
                }
                if let Some(items) = self.dialog_items.get_mut(&dialog_ptr) {
                    if item_no > 0 && (item_no as usize) <= items.len() {
                        let item = &mut items[(item_no - 1) as usize];
                        item.item_type = item_type;
                        item.rect = (box_top, box_left, box_bottom, box_right);
                        if base_type == 0 {
                            // userItem: the "item" parameter is a ProcPtr
                            // Inside Macintosh Volume I, I-405
                            item.proc_ptr = item_handle;
                        } else if base_type == 8 || base_type == 16 {
                            item.text = Self::text_item_string_from_handle(bus, item_handle);
                        }
                    }
                }

                if let Some(pending) = self.pending_dialog_popup_menu {
                    if pending.dialog_ptr == dialog_ptr && pending.item_no == item_no {
                        if base_type == 0 && (item_type & 0x80) == 0 && item_handle != 0 {
                            self.dialog_item_popup_menus
                                .insert((dialog_ptr, item_no), pending.menu_id);
                            self.dialog_popup_original_rects
                                .insert((dialog_ptr, item_no), pending.rect);
                        }
                        self.pending_dialog_popup_menu = None;
                    }
                }

                if let Some(previous_item) = previous_item {
                    let key = (dialog_ptr, item_no);
                    let previous_base_type = previous_item.item_type & 0x7F;
                    let previous_enabled_user_item =
                        previous_base_type == 0 && (previous_item.item_type & 0x80) == 0;
                    let current_enabled_user_item = base_type == 0 && (item_type & 0x80) == 0;
                    let old_width = previous_item.rect.3 - previous_item.rect.1;
                    let old_height = previous_item.rect.2 - previous_item.rect.0;
                    let new_width = box_right - box_left;
                    let new_height = box_bottom - box_top;
                    let narrowed_to_popup_indicator = previous_enabled_user_item
                        && current_enabled_user_item
                        && item_handle == 0
                        && old_width >= 40
                        && new_width > 0
                        && new_width <= 24
                        && old_height > 0
                        && new_height > 0
                        && (old_height - new_height).abs() <= 4;
                    if narrowed_to_popup_indicator {
                        self.dialog_popup_original_rects
                            .entry(key)
                            .or_insert(previous_item.rect);
                        self.dialog_popup_candidate_items.insert(key);
                    } else if !current_enabled_user_item {
                        self.dialog_popup_candidate_items.remove(&key);
                        if !self.dialog_item_popup_menus.contains_key(&key) {
                            self.dialog_popup_original_rects.remove(&key);
                        }
                    }
                }

                if base_type == 8 || base_type == 16 {
                    if item_handle != 0 {
                        self.dialog_item_handles
                            .insert(item_handle, (dialog_ptr, (item_no - 1) as usize));
                    }
                } else if (base_type == 4 || base_type == 5 || base_type == 6 || base_type == 7)
                    && item_handle != 0
                {
                    self.dialog_control_handles
                        .insert(item_handle, (dialog_ptr, item_no));
                    let ctrl_ptr = bus.read_long(item_handle);
                    if ctrl_ptr != 0 {
                        bus.write_word(ctrl_ptr + 8, box_top as u16);
                        bus.write_word(ctrl_ptr + 10, box_left as u16);
                        bus.write_word(ctrl_ptr + 12, box_bottom as u16);
                        bus.write_word(ctrl_ptr + 14, box_right as u16);
                        self.dialog_control_values
                            .insert((dialog_ptr, item_no), bus.read_word(ctrl_ptr + 18) as i16);
                    }
                }

                // Also update tracking state if dialog is currently active
                if let Some(ref mut tracking) = self.dialog_tracking {
                    if tracking.dialog_ptr == dialog_ptr
                        && item_no > 0
                        && (item_no as usize) <= tracking.items.len()
                    {
                        let item = &mut tracking.items[(item_no - 1) as usize];
                        item.item_type = item_type;
                        item.rect = (box_top, box_left, box_bottom, box_right);
                        if base_type == 0 {
                            item.proc_ptr = item_handle;
                        } else if base_type == 8 || base_type == 16 {
                            item.text = Self::text_item_string_from_handle(bus, item_handle);
                            if tracking.edit_item == item_no {
                                tracking.edit_text = item.text.clone();
                            }
                        }
                    }
                }

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

            // ModalDialog ($A991)
            // Handles events in a modal dialog until an enabled item is hit.
            // PROCEDURE ModalDialog (filterProc: ProcPtr; VAR itemHit: INTEGER);
            // Inside Macintosh Volume I, I-415
            // ModalDialog ($A991): Re-fire pattern: draws dialog (including resCtrl/popup controls, type 7), handles button clicks, keyboard input (Return/Escape/text), button flash animation, userItem draw proc callbacks
            (true, 0x191) => {
                // Check if draw procs need to finish before entering event loop.
                if let Some(ref tracking) = self.dialog_tracking {
                    if !tracking.draw_procs_done {
                        return Some(Ok(()));
                    }
                }
                // Re-snapshot rendered_pixels after draw procs or filter proc completes.
                // rendered_pixels_final is cleared when either is injected; the snapshot
                // here captures whatever they drew before redraw_chrome can restore it.
                if let Some(ref tracking) = self.dialog_tracking {
                    if !tracking.rendered_pixels_final {
                        let bounds = tracking.bounds;
                        let items = tracking.items.clone();
                        let default_item = tracking.default_item;
                        let edit_text = tracking.edit_text.clone();
                        let edit_item = tracking.edit_item;
                        let dialog_ptr = tracking.dialog_ptr;
                        let popup_draws = tracking.popup_draws.clone();
                        if !tracking.game_managed {
                            if self.front_window == dialog_ptr {
                                self.blit_window_to_screen(bus);
                            }
                            self.redraw_standard_dialog_items(
                                bus,
                                bounds,
                                &items,
                                default_item,
                                &edit_text,
                                edit_item,
                                dialog_ptr,
                            );
                        }
                        self.redraw_dialog_popup_controls(bus, &popup_draws);
                        let rendered = self.save_dialog_pixels(bus, bounds);
                        let t = self.dialog_tracking.as_mut().unwrap();
                        t.rendered_pixels = rendered;
                        t.rendered_pixels_final = true;
                    }
                }

                if let Some(ref tracking) = self.dialog_tracking {
                    if tracking.active_button.is_some() {
                        self.handle_dialog_button_tracking(bus);
                        return Some(Ok(()));
                    }

                    if tracking.active_popup.is_some() {
                        self.handle_dialog_popup_tracking(cpu, bus);
                        return Some(Ok(()));
                    }

                    if tracking.active_user_item.is_some() {
                        self.handle_dialog_user_item_tracking(cpu, bus);
                        return Some(Ok(()));
                    }

                    // Fast path — when nothing can produce an item hit or visible
                    // update on this step (no filter proc, no flash animation, no
                    // pending event, no queued events), return Ok without running
                    // any of the re-fire body. Any of these flags being non-default
                    // routes through the full handler below.
                    if tracking.filter_proc == 0
                        && tracking.flash_remaining == 0
                        && tracking.active_button.is_none()
                        && tracking.active_popup.is_none()
                        && tracking.active_user_item.is_none()
                        && self.event_queue.is_empty()
                    {
                        return Some(Ok(()));
                    }
                    // Re-fire: dialog tracking is active
                    let dialog_ptr = tracking.dialog_ptr;
                    let bounds = tracking.bounds;
                    let item_hit_ptr = tracking.item_hit_ptr;
                    let stack_ptr = tracking.stack_ptr;
                    let filter_proc = tracking.filter_proc;
                    let flash_remaining = tracking.flash_remaining;
                    // Items_clone is built lazily in the mouseDown branch below to
                    // avoid cloning Vec<DialogItem> on every ModalDialog refire.
                    let mut pending_event = None;

                    // For any dialog with a filter proc, check whether the filter
                    // handled the most recent event. Per Inside Macintosh Volume I, I-415:
                    // TRUE means the filter handled the event and set itemHit;
                    // FALSE means ModalDialog should process the event itself.
                    let mut waiting_for_filter_proc_event = false;
                    if filter_proc != 0 {
                        let result_addr = self.dialog_filter_result_addr;
                        let tracking_dialog_ptr = tracking.dialog_ptr;
                        let filter_returned_true = if result_addr != 0 {
                            bus.read_word(result_addr) != 0
                        } else {
                            false
                        };

                        let mut hit = 0i16;
                        if filter_returned_true && item_hit_ptr != 0 {
                            hit = bus.read_word(item_hit_ptr) as i16;
                        }
                        let filter_event = self
                            .dialog_tracking
                            .as_mut()
                            .and_then(|t| t.last_filter_event.take());
                        if trace_dialog_filter_enabled() {
                            eprintln!(
                                "[DIALOG-FILTER] result dialog=${:08X} returned_true={} item_hit={} item_hit_ptr=${:08X}",
                                tracking_dialog_ptr, filter_returned_true, hit, item_hit_ptr
                            );
                        }

                        if hit > 0 || filter_returned_true {
                            let handled_mouse_down =
                                filter_event.as_ref().is_some_and(|e| e.what == 1);
                            let (dialog_ptr, edit_item, edit_text, items) = {
                                let tracking = self.dialog_tracking.as_mut().unwrap();
                                Self::sync_tracking_active_edit_item(tracking);
                                (
                                    tracking.dialog_ptr,
                                    tracking.edit_item,
                                    tracking.edit_text.clone(),
                                    tracking.items.clone(),
                                )
                            };
                            let keep_dialog_visible = items
                                .get((hit - 1) as usize)
                                .map(|item| (item.item_type & 0x7F) != 4)
                                .unwrap_or(true);
                            let item_type =
                                items.get((hit - 1) as usize).map(|item| item.item_type);
                            self.flush_dialog_edit_item_texts(
                                bus, dialog_ptr, &items, edit_item, &edit_text,
                            );
                            // Filter handled the event — end tracking and return.
                            let saved = self.dialog_tracking.take().unwrap();
                            let saved_dialog_ptr = saved.dialog_ptr;
                            let saved_bounds = saved.bounds;
                            if keep_dialog_visible {
                                self.persist_visible_dialog_snapshot(bus, &saved);
                            }
                            self.dialog_saved_pixels
                                .insert(saved_dialog_ptr, saved.saved_pixels);
                            if handled_mouse_down {
                                self.consume_dialog_mouse_up();
                            }
                            if trace_dialog_filter_enabled() {
                                let actual_hit = bus.read_word(item_hit_ptr) as i16;
                                eprintln!(
                                    "[DIALOG-FILTER] ModalDialog returning: filter hit={} actual_item_hit_ptr=${:08X} actual_hit={} handled_mouseDown={} stack_ptr=${:08X} queue_len={}",
                                    hit, item_hit_ptr, actual_hit, handled_mouse_down, stack_ptr, self.event_queue.len()
                                );
                            }
                            let outcome = if hit > 0 {
                                if keep_dialog_visible {
                                    "filter_item_hit_retained"
                                } else {
                                    "filter_item_hit_dismissed"
                                }
                            } else {
                                "filter_true_zero_hit"
                            };
                            self.record_modal_dialog_filter_input_trace(
                                "filter_result",
                                saved_dialog_ptr,
                                saved_bounds,
                                filter_proc,
                                filter_event.as_ref(),
                                hit,
                                item_type,
                                handled_mouse_down,
                                keep_dialog_visible,
                                "returned",
                                outcome,
                            );
                            cpu.write_reg(Register::A7, stack_ptr + 8);
                            return Some(Ok(()));
                        }
                        pending_event = filter_event;
                        if let Some(ref event) = pending_event {
                            self.record_modal_dialog_filter_input_trace(
                                "filter_result",
                                tracking_dialog_ptr,
                                bounds,
                                filter_proc,
                                Some(event),
                                0,
                                None,
                                false,
                                true,
                                "passed",
                                "filter_declined",
                            );
                        }
                        waiting_for_filter_proc_event = pending_event.is_none();
                    }

                    if flash_remaining > 0 {
                        // Button flash animation
                        let flash_item = self.dialog_tracking.as_ref().unwrap().flash_item;
                        let (remaining, button_draw) = {
                            let t = self.dialog_tracking.as_mut().unwrap();
                            if t.flash_delay > 0 {
                                t.flash_delay -= 1;
                                return Some(Ok(()));
                            }
                            t.flash_remaining -= 1;
                            t.flash_delay = 3;
                            let remaining = t.flash_remaining;
                            let button_draw = if remaining > 0
                                && flash_item > 0
                                && (flash_item as usize) <= t.items.len()
                            {
                                let item = &t.items[(flash_item - 1) as usize];
                                Some((
                                    Self::dialog_item_screen_rect(bounds, item.rect),
                                    item.text.clone(),
                                    flash_item == t.default_item,
                                    remaining % 2 == 0,
                                ))
                            } else {
                                None
                            };
                            (remaining, button_draw)
                        };

                        if let Some((screen_rect, title, is_default, highlighted)) = button_draw {
                            // Toggle button highlight.
                            self.draw_dialog_button_highlight_state(
                                bus,
                                screen_rect,
                                &title,
                                is_default,
                                highlighted,
                            );
                        }

                        if remaining == 0 {
                            // Flash complete — write all editText item handles
                            // back before returning the hit item to the app.
                            let (dialog_ptr, edit_item, edit_text, items) = {
                                let tracking = self.dialog_tracking.as_mut().unwrap();
                                Self::sync_tracking_active_edit_item(tracking);
                                (
                                    tracking.dialog_ptr,
                                    tracking.edit_item,
                                    tracking.edit_text.clone(),
                                    tracking.items.clone(),
                                )
                            };
                            self.flush_dialog_edit_item_texts(
                                bus, dialog_ptr, &items, edit_item, &edit_text,
                            );

                            let saved = self.dialog_tracking.take().unwrap();
                            let saved_dialog_ptr = saved.dialog_ptr;
                            let saved_bounds = saved.bounds;
                            let item_type = saved
                                .items
                                .get(flash_item.saturating_sub(1) as usize)
                                .map(|item| item.item_type);
                            self.persist_visible_dialog_snapshot(bus, &saved);
                            self.dialog_saved_pixels
                                .insert(saved_dialog_ptr, saved.saved_pixels);

                            // ModalDialog button clicks should consume the
                            // matching mouseUp before returning to the app.
                            // If we leave it queued, the release can leak into
                            // the underlying game view after the dialog closes.
                            self.consume_dialog_mouse_up();

                            if item_hit_ptr != 0 {
                                bus.write_word(item_hit_ptr, flash_item as u16);
                            }
                            self.record_modal_dialog_input_trace(
                                "finish",
                                saved_dialog_ptr,
                                saved_bounds,
                                flash_item,
                                item_type,
                                None,
                                "returned",
                                "button_item_hit",
                            );
                            cpu.write_reg(Register::A7, stack_ptr + 8);
                        }
                        return Some(Ok(()));
                    }

                    // With a non-NIL filterProc, ModalDialog passes each event
                    // to the filter before Dialog Manager default handling.
                    // Leave queued events alone here; runner.rs will inject the
                    // filter callback, and this handler will process the returned
                    // event only if that callback returns FALSE. MTE 1992 6-136.
                    if waiting_for_filter_proc_event {
                        return Some(Ok(()));
                    }

                    let event = if let Some(e) = pending_event {
                        Some(e)
                    } else if !self.event_queue.is_empty() {
                        // Drain events looking for actionable ones
                        let mut event = None;
                        while let Some(e) = self.event_queue.pop_front() {
                            match e.what {
                                1 | 2 | 3 | 6 => {
                                    event = Some(e);
                                    break;
                                }
                                _ => {} // discard other events
                            }
                        }
                        event
                    } else {
                        None
                    };

                    if let Some(mut e) = event {
                        if e.what == 0 && self.mouse_button {
                            e.what = 1;
                            e.where_v = self.mouse_pos.0;
                            e.where_h = self.mouse_pos.1;
                        }
                        match e.what {
                            // updateEvt — re-snapshot rendered_pixels.
                            // Redraw HLE popup controls first, since the game
                            // may have drawn narrow indicator boxes that would
                            // taint the snapshot.
                            6 => {
                                // MTE 1992 pp. 6-135 and 6-141: ModalDialog
                                // handles dialog update events through the same
                                // DialogSelect path that brackets DrawDialog
                                // with BeginUpdate/EndUpdate and makes the
                                // dialog the current graphics port.
                                self.begin_update_window(bus, dialog_ptr);
                                self.set_current_port_state(bus, cpu, dialog_ptr, None);
                                let previous_rendered = self
                                    .dialog_tracking
                                    .as_ref()
                                    .filter(|t| t.rendered_pixels_final)
                                    .map(|t| t.rendered_pixels.clone())
                                    .unwrap_or_default();
                                if !previous_rendered.is_empty() {
                                    self.restore_dialog_pixels(bus, bounds, &previous_rendered);
                                }
                                if let Some(ref t) = self.dialog_tracking {
                                    if !t.game_managed {
                                        self.redraw_standard_dialog_items(
                                            bus,
                                            bounds,
                                            &t.items,
                                            t.default_item,
                                            &t.edit_text,
                                            t.edit_item,
                                            t.dialog_ptr,
                                        );
                                    }
                                    let popup_draws = t.popup_draws.clone();
                                    self.redraw_dialog_popup_controls(bus, &popup_draws);
                                }
                                let rendered = self.save_dialog_pixels(bus, bounds);
                                let t = self.dialog_tracking.as_mut().unwrap();
                                t.rendered_pixels = rendered;
                                t.rendered_pixels_final = true;
                                self.end_update_window(bus, dialog_ptr);
                            }
                            // mouseDown
                            1 => {
                                // Clone items lazily — only when actually processing
                                // a mouseDown. Hot path (no events) skips this entirely.
                                let items_clone: Vec<DialogItem> = self
                                    .dialog_tracking
                                    .as_ref()
                                    .map(|t| t.items.clone())
                                    .unwrap_or_default();
                                let mut hit = self.dialog_item_hit_test(
                                    bus,
                                    &items_clone,
                                    bounds,
                                    e.where_v,
                                    e.where_h,
                                    &self.dialog_popup_original_rects,
                                    dialog_ptr,
                                );
                                if hit <= 0 {
                                    hit = Self::dialog_button_hit_test(
                                        &items_clone,
                                        bounds,
                                        e.where_v,
                                        e.where_h,
                                    );
                                }
                                if hit > 0 {
                                    let item = &items_clone[(hit - 1) as usize];
                                    let base_type = item.item_type & 0x7F;
                                    let is_disabled = (item.item_type & 0x80) != 0;

                                    if !is_disabled {
                                        match base_type {
                                            // Button click: start flash
                                            4 => {
                                                let (abs_top, abs_left, abs_bottom, abs_right) =
                                                    Self::dialog_item_screen_rect(
                                                        bounds, item.rect,
                                                    );
                                                let is_default = hit
                                                    == self
                                                        .dialog_tracking
                                                        .as_ref()
                                                        .map(|tracking| tracking.default_item)
                                                        .unwrap_or(0);
                                                self.draw_dialog_button_highlight_state(
                                                    bus,
                                                    (abs_top, abs_left, abs_bottom, abs_right),
                                                    &item.text,
                                                    is_default,
                                                    true,
                                                );
                                                if self.mouse_button {
                                                    let t = self.dialog_tracking.as_mut().unwrap();
                                                    t.active_button = Some(
                                                        super::dispatch::DialogButtonTrackingState {
                                                            item_no: hit,
                                                            rect: item.rect,
                                                            title: item.text.clone(),
                                                            is_default,
                                                            highlighted: true,
                                                        },
                                                    );
                                                    self.record_modal_dialog_input_trace(
                                                        "mouse_down",
                                                        dialog_ptr,
                                                        bounds,
                                                        hit,
                                                        Some(item.item_type),
                                                        Some(true),
                                                        "pending",
                                                        "button_tracking_started",
                                                    );
                                                } else {
                                                    self.start_dialog_button_flash(
                                                        bus, bounds, hit, item.rect, &item.text,
                                                        is_default, true,
                                                    );
                                                    self.record_modal_dialog_input_trace(
                                                        "mouse_down",
                                                        dialog_ptr,
                                                        bounds,
                                                        hit,
                                                        Some(item.item_type),
                                                        Some(true),
                                                        "pending",
                                                        "start_flash",
                                                    );
                                                }
                                            }
                                            // Checkbox click: return item number immediately
                                            // The dialog stays on screen — the app toggles
                                            // the checkbox value and calls ModalDialog again.
                                            // Inside Macintosh Volume I, I-415
                                            5 => {
                                                let (dlg_ptr, edit_item, edit_text, items) = {
                                                    let tracking =
                                                        self.dialog_tracking.as_mut().unwrap();
                                                    Self::sync_tracking_active_edit_item(tracking);
                                                    (
                                                        tracking.dialog_ptr,
                                                        tracking.edit_item,
                                                        tracking.edit_text.clone(),
                                                        tracking.items.clone(),
                                                    )
                                                };
                                                self.flush_dialog_edit_item_texts(
                                                    bus, dlg_ptr, &items, edit_item, &edit_text,
                                                );
                                                // Preserve saved background pixels for re-entry
                                                let saved = self.dialog_tracking.take().unwrap();
                                                let saved_dialog_ptr = saved.dialog_ptr;
                                                let saved_bounds = saved.bounds;
                                                self.persist_visible_dialog_snapshot(bus, &saved);
                                                self.dialog_saved_pixels
                                                    .insert(saved_dialog_ptr, saved.saved_pixels);
                                                self.consume_dialog_mouse_up();
                                                // Don't restore pixels — dialog stays visible
                                                if item_hit_ptr != 0 {
                                                    bus.write_word(item_hit_ptr, hit as u16);
                                                }
                                                self.record_modal_dialog_input_trace(
                                                    "mouse_down",
                                                    saved_dialog_ptr,
                                                    saved_bounds,
                                                    hit,
                                                    Some(item.item_type),
                                                    None,
                                                    "returned",
                                                    "checkbox_item_hit_retained",
                                                );
                                                cpu.write_reg(Register::A7, stack_ptr + 8);
                                            }
                                            // EditText click: set as active
                                            16 => {
                                                let (dlg_ptr, edit_item, edit_text, items) = {
                                                    let tracking =
                                                        self.dialog_tracking.as_mut().unwrap();
                                                    Self::sync_tracking_active_edit_item(tracking);
                                                    tracking.edit_item = hit;
                                                    tracking.edit_text = tracking
                                                        .items
                                                        .get((hit - 1) as usize)
                                                        .map(|item| item.text.clone())
                                                        .unwrap_or_default();
                                                    tracking.edit_text_modified = false;
                                                    (
                                                        tracking.dialog_ptr,
                                                        tracking.edit_item,
                                                        tracking.edit_text.clone(),
                                                        tracking.items.clone(),
                                                    )
                                                };
                                                // IM:I I-415: mouseDown in an enabled editText
                                                // item is TextEdit-handled and ModalDialog
                                                // returns that item. TEClick's pixel-to-caret
                                                // mapping remains the documented HLE compromise,
                                                // but the active editField/TERecord mirror is
                                                // still guest-visible Dialog Manager state.
                                                self.activate_dialog_edit_item(
                                                    bus, cpu, dlg_ptr, &items, edit_item,
                                                );
                                                self.flush_dialog_edit_item_texts(
                                                    bus, dlg_ptr, &items, edit_item, &edit_text,
                                                );
                                                let saved = self.dialog_tracking.take().unwrap();
                                                let saved_dialog_ptr = saved.dialog_ptr;
                                                let saved_bounds = saved.bounds;
                                                self.persist_visible_dialog_snapshot(bus, &saved);
                                                self.dialog_saved_pixels
                                                    .insert(saved_dialog_ptr, saved.saved_pixels);
                                                self.consume_dialog_mouse_up();
                                                if item_hit_ptr != 0 {
                                                    bus.write_word(item_hit_ptr, hit as u16);
                                                }
                                                self.record_modal_dialog_input_trace(
                                                    "mouse_down",
                                                    saved_dialog_ptr,
                                                    saved_bounds,
                                                    hit,
                                                    Some(item.item_type),
                                                    None,
                                                    "returned",
                                                    "edittext_item_hit_retained",
                                                );
                                                cpu.write_reg(Register::A7, stack_ptr + 8);
                                            }
                                            // resCtrl popup controls are tracked by
                                            // ModalDialog itself, matching the standard
                                            // Dialog Manager control-item path.
                                            7 if self.begin_dialog_popup_tracking(
                                                bus, dialog_ptr, hit,
                                            ) => {}
                                            // UserItem with popup menu: return item number.
                                            // Dialog stays on screen — the app calls
                                            // PopUpMenuSelect to show the popup dropdown.
                                            // Macintosh Toolbox Essentials 1992, 5-26.
                                            // Original Marathon-style compact popup candidates
                                            // use the same app-owned tracking path after
                                            // InsertMenu/GetDItem/SetDItem narrows the DITL rect.
                                            0 if self
                                                .dialog_item_popup_menus
                                                .contains_key(&(dialog_ptr, hit))
                                                || self
                                                    .dialog_popup_candidate_items
                                                    .contains(&(dialog_ptr, hit)) =>
                                            {
                                                let (dlg_ptr, edit_item, edit_text, items) = {
                                                    let tracking =
                                                        self.dialog_tracking.as_mut().unwrap();
                                                    Self::sync_tracking_active_edit_item(tracking);
                                                    (
                                                        tracking.dialog_ptr,
                                                        tracking.edit_item,
                                                        tracking.edit_text.clone(),
                                                        tracking.items.clone(),
                                                    )
                                                };
                                                self.flush_dialog_edit_item_texts(
                                                    bus, dlg_ptr, &items, edit_item, &edit_text,
                                                );
                                                let saved = self.dialog_tracking.take().unwrap();
                                                self.persist_visible_dialog_snapshot(bus, &saved);
                                                self.dialog_saved_pixels
                                                    .insert(saved.dialog_ptr, saved.saved_pixels);
                                                self.consume_dialog_mouse_up();
                                                if item_hit_ptr != 0 {
                                                    bus.write_word(item_hit_ptr, hit as u16);
                                                }
                                                cpu.write_reg(Register::A7, stack_ptr + 8);
                                            }
                                            // Plain userItems in standard dialogs are custom
                                            // hit targets. Track the physical click until
                                            // release so the application receives the item
                                            // hit after the completed click, matching the
                                            // standard control paths above.
                                            0 if self.dialog_tracking.as_ref().is_some_and(
                                                |tracking| {
                                                    self.is_plain_modal_user_item(
                                                        tracking, hit, item,
                                                    )
                                                },
                                            ) && self.mouse_button =>
                                            {
                                                let active_rect = self
                                                    .dialog_tracking
                                                    .as_ref()
                                                    .and_then(|tracking| {
                                                        self.dialog_popup_original_rects
                                                            .get(&(tracking.dialog_ptr, hit))
                                                            .copied()
                                                    })
                                                    .unwrap_or(item.rect);
                                                if let Some(t) = self.dialog_tracking.as_mut() {
                                                    t.active_user_item =
                                                        Some(DialogUserItemTrackingState {
                                                            item_no: hit,
                                                            rect: active_rect,
                                                        });
                                                }
                                            }
                                            // Any other enabled item: return item number immediately.
                                            // Inside Macintosh Volume I, I-428
                                            _ => {
                                                let (dlg_ptr, edit_item, edit_text, items) = {
                                                    let tracking =
                                                        self.dialog_tracking.as_mut().unwrap();
                                                    Self::sync_tracking_active_edit_item(tracking);
                                                    (
                                                        tracking.dialog_ptr,
                                                        tracking.edit_item,
                                                        tracking.edit_text.clone(),
                                                        tracking.items.clone(),
                                                    )
                                                };
                                                self.flush_dialog_edit_item_texts(
                                                    bus, dlg_ptr, &items, edit_item, &edit_text,
                                                );
                                                let saved = self.dialog_tracking.take().unwrap();
                                                self.persist_visible_dialog_snapshot(bus, &saved);
                                                self.dialog_saved_pixels
                                                    .insert(saved.dialog_ptr, saved.saved_pixels);
                                                self.consume_dialog_mouse_up();
                                                if item_hit_ptr != 0 {
                                                    bus.write_word(item_hit_ptr, hit as u16);
                                                }
                                                cpu.write_reg(Register::A7, stack_ptr + 8);
                                            }
                                        }
                                    }
                                }
                            }
                            // keyDown
                            3 => {
                                let char_code = (e.message & 0xFF) as u8;
                                let key_code = ((e.message >> 8) & 0xFF) as u8;
                                let command_period =
                                    char_code == b'.' && (e.modifiers & 0x0100) != 0;
                                let command_select_all = char_code.eq_ignore_ascii_case(&b'a')
                                    && (e.modifiers & 0x0100) != 0;
                                match char_code {
                                    // Return or Enter: trigger default button
                                    0x0D | 0x03 => {
                                        let target =
                                            self.dialog_tracking.as_ref().and_then(|tracking| {
                                                let def = tracking.default_item;
                                                if def <= 0 {
                                                    return None;
                                                }
                                                tracking
                                                    .items
                                                    .get(def.saturating_sub(1) as usize)
                                                    .map(|item| {
                                                        (def, item.rect, item.text.clone(), true)
                                                    })
                                            });
                                        if let Some((def, rect, title, is_default)) = target {
                                            let screen_rect =
                                                Self::dialog_item_screen_rect(bounds, rect);
                                            self.draw_dialog_button_highlight_state(
                                                bus,
                                                screen_rect,
                                                &title,
                                                is_default,
                                                true,
                                            );
                                            let t = self.dialog_tracking.as_mut().unwrap();
                                            t.flash_remaining = 6;
                                            t.flash_delay = 3;
                                            t.flash_item = def;
                                        }
                                    }
                                    // Escape or Command-period: trigger cancel button.
                                    // MTE 1992 p. 6-138 maps Esc and Command-period
                                    // to Cancel before dialog-select style handling.
                                    0x1B | b'.' if char_code == 0x1B || command_period => {
                                        let target =
                                            self.dialog_tracking.as_ref().and_then(|tracking| {
                                                let cancel = tracking.cancel_item;
                                                if cancel <= 0 {
                                                    return None;
                                                }
                                                tracking
                                                    .items
                                                    .get(cancel.saturating_sub(1) as usize)
                                                    .map(|item| {
                                                        (
                                                            cancel,
                                                            item.rect,
                                                            item.text.clone(),
                                                            cancel == tracking.default_item,
                                                        )
                                                    })
                                            });
                                        if let Some((cancel, rect, title, is_default)) = target {
                                            let screen_rect =
                                                Self::dialog_item_screen_rect(bounds, rect);
                                            self.draw_dialog_button_highlight_state(
                                                bus,
                                                screen_rect,
                                                &title,
                                                is_default,
                                                true,
                                            );
                                            let t = self.dialog_tracking.as_mut().unwrap();
                                            t.flash_remaining = 6;
                                            t.flash_delay = 3;
                                            t.flash_item = cancel;
                                        }
                                    }
                                    // Tab: move to the next editText item, wrapping.
                                    0x09 => {
                                        let mut switched = false;
                                        if let Some(tracking) = self.dialog_tracking.as_mut() {
                                            if tracking.edit_item > 0 {
                                                Self::sync_tracking_active_edit_item(tracking);
                                            }
                                            if !tracking.items.is_empty() {
                                                let start = tracking.edit_item.max(0) as usize;
                                                let next =
                                                    (0..tracking.items.len()).find_map(|offset| {
                                                        let idx =
                                                            (start + offset) % tracking.items.len();
                                                        if (tracking.items[idx].item_type & 0x7F)
                                                            == 16
                                                        {
                                                            Some(idx)
                                                        } else {
                                                            None
                                                        }
                                                    });
                                                if let Some(idx) = next {
                                                    tracking.edit_item = (idx + 1) as i16;
                                                    tracking.edit_text =
                                                        tracking.items[idx].text.clone();
                                                    tracking.edit_text_modified = false;
                                                    bus.write_word(dialog_ptr + 164, idx as u16);
                                                    switched = true;
                                                }
                                            }
                                        }
                                        if switched {
                                            self.refresh_dialog_tracking_snapshot(bus);
                                        }
                                    }
                                    // Backspace/Delete or printable ASCII.
                                    _ if command_select_all => {
                                        let mut select_trace = None;
                                        let mut modified_key_to_clear = None;
                                        if let Some(tracking) = self.dialog_tracking.as_mut() {
                                            let text_before = tracking.edit_text.clone();
                                            if tracking.edit_item > 0 {
                                                let text_len = tracking.edit_text.len();
                                                tracking.edit_text_modified = false;
                                                Self::set_tracking_active_edit_selection(
                                                    tracking, 0, text_len,
                                                );
                                                Self::sync_tracking_active_edit_item(tracking);

                                                let edit_item = tracking.edit_item;
                                                let item_type = tracking
                                                    .items
                                                    .get((edit_item - 1) as usize)
                                                    .map(|item| item.item_type);
                                                let enabled_edit_text = item_type
                                                    .map(|ty| (ty & 0x7F) == 16 && (ty & 0x80) == 0)
                                                    .unwrap_or(false);
                                                let text_after = tracking.edit_text.clone();
                                                select_trace = Some((
                                                    edit_item,
                                                    item_type,
                                                    text_before,
                                                    text_after,
                                                    enabled_edit_text,
                                                ));
                                                modified_key_to_clear =
                                                    Some((tracking.dialog_ptr, edit_item));
                                            }
                                        }
                                        if let Some(key) = modified_key_to_clear {
                                            self.dialog_edit_text_modified_items.remove(&key);
                                        }
                                        if let Some((
                                            edit_item,
                                            item_type,
                                            text_before,
                                            text_after,
                                            enabled_edit_text,
                                        )) = select_trace
                                        {
                                            self.refresh_dialog_tracking_snapshot(bus);
                                            let outcome = if enabled_edit_text {
                                                "enabled_edittext_select_all"
                                            } else {
                                                "edittext_select_all"
                                            };
                                            if enabled_edit_text {
                                                let (dlg_ptr, active_edit_item, edit_text, items) = {
                                                    let tracking =
                                                        self.dialog_tracking.as_mut().unwrap();
                                                    Self::sync_tracking_active_edit_item(tracking);
                                                    (
                                                        tracking.dialog_ptr,
                                                        tracking.edit_item,
                                                        tracking.edit_text.clone(),
                                                        tracking.items.clone(),
                                                    )
                                                };
                                                self.flush_dialog_edit_item_texts(
                                                    bus,
                                                    dlg_ptr,
                                                    &items,
                                                    active_edit_item,
                                                    &edit_text,
                                                );
                                                let saved = self.dialog_tracking.take().unwrap();
                                                let saved_dialog_ptr = saved.dialog_ptr;
                                                self.persist_visible_dialog_snapshot(bus, &saved);
                                                self.dialog_saved_pixels
                                                    .insert(saved_dialog_ptr, saved.saved_pixels);
                                                if item_hit_ptr != 0 {
                                                    bus.write_word(
                                                        item_hit_ptr,
                                                        active_edit_item as u16,
                                                    );
                                                }
                                                cpu.write_reg(Register::A7, stack_ptr + 8);
                                                self.record_modal_dialog_text_input_trace(
                                                    "key_down",
                                                    dialog_ptr,
                                                    bounds,
                                                    edit_item,
                                                    item_type,
                                                    key_code,
                                                    char_code,
                                                    &text_before,
                                                    &text_after,
                                                    "returned",
                                                    outcome,
                                                );
                                            } else {
                                                self.record_modal_dialog_text_input_trace(
                                                    "key_down",
                                                    dialog_ptr,
                                                    bounds,
                                                    edit_item,
                                                    item_type,
                                                    key_code,
                                                    char_code,
                                                    &text_before,
                                                    &text_after,
                                                    "pending",
                                                    outcome,
                                                );
                                            }
                                        }
                                    }
                                    0x08 | 0x20..=0x7E => {
                                        let mut text_trace = None;
                                        let mut modified_key_to_set = None;
                                        if let Some(tracking) = self.dialog_tracking.as_mut() {
                                            let text_before = tracking.edit_text.clone();
                                            if tracking.edit_item > 0 {
                                                if char_code == 0x08 {
                                                    if !tracking.edit_text_modified {
                                                        // First backspace clears selection
                                                        tracking.edit_text.clear();
                                                        tracking.edit_text_modified = true;
                                                    } else if !tracking.edit_text.is_empty() {
                                                        tracking.edit_text.pop();
                                                    }
                                                } else {
                                                    if !tracking.edit_text_modified {
                                                        // First keypress replaces selection
                                                        tracking.edit_text.clear();
                                                        tracking.edit_text_modified = true;
                                                    }
                                                    tracking.edit_text.push(char_code as char);
                                                }
                                                let cursor = tracking.edit_text.len();
                                                Self::set_tracking_active_edit_selection(
                                                    tracking, cursor, cursor,
                                                );
                                                Self::sync_tracking_active_edit_item(tracking);
                                                modified_key_to_set =
                                                    Some((tracking.dialog_ptr, tracking.edit_item));

                                                let edit_item = tracking.edit_item;
                                                let item_type = tracking
                                                    .items
                                                    .get((edit_item - 1) as usize)
                                                    .map(|item| item.item_type);
                                                let enabled_edit_text = item_type
                                                    .map(|ty| (ty & 0x7F) == 16 && (ty & 0x80) == 0)
                                                    .unwrap_or(false);
                                                let text_after = tracking.edit_text.clone();
                                                text_trace = Some((
                                                    edit_item,
                                                    item_type,
                                                    text_before,
                                                    text_after,
                                                    enabled_edit_text,
                                                ));
                                            }
                                        }
                                        if let Some(key) = modified_key_to_set {
                                            self.dialog_edit_text_modified_items.insert(key);
                                        }
                                        if let Some((
                                            edit_item,
                                            item_type,
                                            text_before,
                                            text_after,
                                            enabled_edit_text,
                                        )) = text_trace
                                        {
                                            self.refresh_dialog_tracking_snapshot(bus);
                                            let outcome = if enabled_edit_text {
                                                "enabled_edittext_item_hit"
                                            } else {
                                                "edittext_updated"
                                            };
                                            if enabled_edit_text {
                                                let (dlg_ptr, active_edit_item, edit_text, items) = {
                                                    let tracking =
                                                        self.dialog_tracking.as_mut().unwrap();
                                                    Self::sync_tracking_active_edit_item(tracking);
                                                    (
                                                        tracking.dialog_ptr,
                                                        tracking.edit_item,
                                                        tracking.edit_text.clone(),
                                                        tracking.items.clone(),
                                                    )
                                                };
                                                self.flush_dialog_edit_item_texts(
                                                    bus,
                                                    dlg_ptr,
                                                    &items,
                                                    active_edit_item,
                                                    &edit_text,
                                                );
                                                let saved = self.dialog_tracking.take().unwrap();
                                                let saved_dialog_ptr = saved.dialog_ptr;
                                                self.persist_visible_dialog_snapshot(bus, &saved);
                                                self.dialog_saved_pixels
                                                    .insert(saved_dialog_ptr, saved.saved_pixels);
                                                if item_hit_ptr != 0 {
                                                    bus.write_word(
                                                        item_hit_ptr,
                                                        active_edit_item as u16,
                                                    );
                                                }
                                                cpu.write_reg(Register::A7, stack_ptr + 8);
                                                self.record_modal_dialog_text_input_trace(
                                                    "key_down",
                                                    dialog_ptr,
                                                    bounds,
                                                    edit_item,
                                                    item_type,
                                                    key_code,
                                                    char_code,
                                                    &text_before,
                                                    &text_after,
                                                    "returned",
                                                    outcome,
                                                );
                                            } else {
                                                self.record_modal_dialog_text_input_trace(
                                                    "key_down",
                                                    dialog_ptr,
                                                    bounds,
                                                    edit_item,
                                                    item_type,
                                                    key_code,
                                                    char_code,
                                                    &text_before,
                                                    &text_after,
                                                    "pending",
                                                    outcome,
                                                );
                                            }
                                        }
                                    }
                                    _ => {}
                                }
                            }
                            _ => {}
                        }
                    }
                } else {
                    // First call: initialize dialog tracking
                    let sp = cpu.read_reg(Register::A7);
                    let item_hit_ptr = bus.read_long(sp);
                    let filter_proc = bus.read_long(sp + 4);
                    if item_hit_ptr != 0 {
                        bus.write_word(item_hit_ptr, 0);
                    }

                    // Find the modal dialog's items. Most callers keep the
                    // dialog as the front window, but games can temporarily
                    // select another port/window between ModalDialog returns
                    // and re-entry (for example while tracking a popup menu).
                    // A real modal dialog remains the Dialog Manager target
                    // until DisposDialog, so prefer the retained visible
                    // modal snapshot when the current front window is not a
                    // known dialog.
                    let mut dialog_ptr = self.front_window;
                    if !self.dialog_items.contains_key(&dialog_ptr) {
                        if let Some((&retained_dialog_ptr, snapshot)) =
                            self.dialog_visible_snapshots.iter().next()
                        {
                            dialog_ptr = retained_dialog_ptr;
                            self.front_window = retained_dialog_ptr;
                            self.window_bounds = snapshot.bounds;
                            self.window_proc_id = self
                                .window_proc_ids
                                .get(&retained_dialog_ptr)
                                .copied()
                                .unwrap_or(self.window_proc_id);
                            self.window_title.clear();
                        }
                    }
                    if let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() {
                        if trace_dialog_filter_enabled() {
                            eprintln!(
                                "[DIALOG-FILTER] init dialog=${:08X} items={} filter_proc=${:08X} item_hit_ptr=${:08X}",
                                dialog_ptr,
                                items.len(),
                                filter_proc,
                                item_hit_ptr
                            );
                        }
                        // Re-read userItem proc pointers from guest memory.
                        // The game may have written them directly to the DITL
                        // handle data after GetNewDialog returned.
                        Self::refresh_ditl_proc_ptrs(bus, dialog_ptr, &mut items);
                        let bounds = self.window_bounds;
                        let proc_id = self.window_proc_id;
                        let title = self.window_title.clone();

                        let (edit_text, edit_item, default_item) =
                            Self::dialog_edit_state(bus, dialog_ptr, &items);
                        let cancel_item = self
                            .dialog_cancel_items
                            .get(&dialog_ptr)
                            .copied()
                            .unwrap_or(2);
                        let edit_text_modified = edit_item > 0
                            && self
                                .dialog_edit_text_modified_items
                                .contains(&(dialog_ptr, edit_item));

                        // If in-bounds items are all userItems, the game
                        // manages drawing itself; offscreen placeholders do
                        // not make this a standard dialog.
                        let game_managed = Self::dialog_is_game_managed(bounds, &items);

                        if trace_dialog_procs_enabled() {
                            for (i, item) in items.iter().enumerate() {
                                eprintln!(
                                    "[DIALOG-PROC] dialog=${:08X} item={} type={} proc=${:08X} rect=({},{},{},{}) text={:?}",
                                    dialog_ptr, i + 1, item.item_type, item.proc_ptr,
                                    item.rect.0, item.rect.1, item.rect.2, item.rect.3,
                                    item.text,
                                );
                                if (item.item_type & 0x7F) == 0 {
                                    eprintln!(
                                        "[DIALOG-PROC] dialog=${:08X} item={} type={} proc=${:08X}",
                                        dialog_ptr,
                                        i + 1,
                                        item.item_type,
                                        item.proc_ptr,
                                    );
                                }
                            }
                        }

                        // Save pixels under dialog area (background to restore on dismiss).
                        // If we have preserved pixels from a previous non-dismissing return
                        // (e.g., popup click), reuse those instead of capturing the
                        // currently visible dialog as "background."
                        let is_reentry = self.dialog_modal_entered.contains(&dialog_ptr);
                        let preserved_visible_snapshot =
                            self.dialog_visible_snapshots.remove(&dialog_ptr);
                        let reused_retained_visible_snapshot =
                            is_reentry && preserved_visible_snapshot.is_some();
                        let preserved_saved_pixels =
                            self.dialog_saved_pixels.get(&dialog_ptr).cloned();
                        let restored_visible_snapshot = preserved_visible_snapshot.is_some();
                        let saved_pixels = preserved_saved_pixels
                            .unwrap_or_else(|| self.save_dialog_pixels(bus, bounds));
                        if let Some(snapshot) = preserved_visible_snapshot {
                            self.restore_dialog_pixels(bus, snapshot.bounds, &snapshot.pixels);
                        }
                        if !game_managed && !is_reentry {
                            // First entry: draw the dialog chrome and controls.
                            // Before draw_dialog fills the dialog area white, save the
                            // pixel content of every userItem rect when those pixels
                            // come from a visible-dialog snapshot. Games (e.g.
                            // Marathon) often draw custom controls (popup buttons,
                            // sliders) into userItem rects via QuickDraw before
                            // calling ModalDialog. Saved-under background pixels are
                            // only for dismissal restore and must not be treated as
                            // application-owned userItem drawing.
                            // Inside Macintosh Volume I, I-405
                            self.dialog_initial_draw_deferred.remove(&dialog_ptr);
                            self.draw_dialog_preserving_user_items(
                                bus,
                                bounds,
                                proc_id,
                                &title,
                                &items,
                                default_item,
                                &edit_text,
                                edit_item,
                                false,
                                dialog_ptr,
                                restored_visible_snapshot,
                                true,
                                true,
                            );
                        }

                        // HLE-draw popup controls for type-0 userItems that were
                        // associated with MENU resources via the InsertMenu → GetDItem
                        // pattern used by games (e.g. Marathon) to set up popup
                        // controls in dialogs.  We find the checked item (mark=0x12)
                        // in each menu and draw a standard popup button for it.
                        // Inside Macintosh Volume I, I-405 (userItem draw responsibilities)
                        let implicit_popup_candidate_count = items
                            .iter()
                            .enumerate()
                            .filter(|(i, item)| {
                                let item_no = (*i + 1) as i16;
                                (item.item_type & 0x7F) == 0
                                    && !self
                                        .dialog_item_popup_menus
                                        .contains_key(&(dialog_ptr, item_no))
                                    && self
                                        .dialog_popup_candidate_items
                                        .contains(&(dialog_ptr, item_no))
                            })
                            .count();
                        let mut implicit_popup_menu_ids: Vec<i16> = self
                            .menus
                            .iter()
                            .filter(|menu| {
                                menu.in_menu_bar
                                    && !menu.visible_in_menu_bar
                                    && menu.items.iter().any(|item| item.mark == 0x12)
                            })
                            .map(|menu| menu.id)
                            .collect();
                        if implicit_popup_menu_ids.len() < implicit_popup_candidate_count {
                            implicit_popup_menu_ids = self
                                .menus
                                .iter()
                                .filter(|menu| menu.items.iter().any(|item| item.mark == 0x12))
                                .map(|menu| menu.id)
                                .collect();
                        }
                        let mut implicit_popup_menu_index = 0usize;
                        let popup_draws: Vec<DialogPopupDraw> = items
                            .iter()
                            .enumerate()
                            .filter_map(|(i, item)| {
                                let item_no = (i + 1) as i16;
                                if (item.item_type & 0x7F) != 0 {
                                    return None;
                                }
                                let key = (dialog_ptr, item_no);
                                let (menu_id, original_rect) = if let Some(&menu_id) =
                                    self.dialog_item_popup_menus.get(&key)
                                {
                                    let rect = self
                                        .dialog_popup_original_rects
                                        .get(&key)
                                        .copied()
                                        .unwrap_or(item.rect);
                                    (menu_id, rect)
                                } else if self.dialog_popup_candidate_items.contains(&key) {
                                    let menu_id =
                                        *implicit_popup_menu_ids.get(implicit_popup_menu_index)?;
                                    implicit_popup_menu_index += 1;
                                    let rect = self
                                        .dialog_popup_original_rects
                                        .get(&key)
                                        .copied()
                                        .unwrap_or(item.rect);
                                    (menu_id, rect)
                                } else {
                                    return None;
                                };
                                let checked_text = self
                                    .menus
                                    .iter()
                                    .find(|m| m.id == menu_id)
                                    .and_then(|m| {
                                        m.items
                                            .iter()
                                            .find(|mi| mi.mark == 0x12)
                                            .map(|mi| mi.text.clone())
                                    })
                                    .unwrap_or_default();
                                // Use the original DITL rect (before SetDItem narrowed it)
                                let (it_t, it_l, it_b, it_r) = original_rect;
                                Some(DialogPopupDraw {
                                    rect: (
                                        bounds.0 + it_t,
                                        bounds.1 + it_l,
                                        bounds.0 + it_b,
                                        bounds.1 + it_r,
                                    ),
                                    title: checked_text,
                                    enabled: (item.item_type & 0x80) == 0,
                                    pressed: false,
                                })
                            })
                            .collect();
                        self.redraw_dialog_popup_controls(bus, &popup_draws);

                        // Snapshot the fully rendered dialog (including PICTs) so
                        // redraw_chrome can restore it without re-parsing pictures.
                        // If there are userItem draw procs, this will be re-snapshotted
                        // after they execute.
                        let rendered_pixels = self.save_dialog_pixels(bus, bounds);

                        // Collect userItem draw procs to call. ShowWindow can
                        // create a visible snapshot before the first
                        // ModalDialog entry, and that first entry still needs
                        // the initial userItem update pass. Only skip the
                        // queue when the same retained modal dialog is
                        // re-entered after a non-dismissing return: that
                        // restored visible snapshot already contains the
                        // completed userItem output, and a real ModalDialog
                        // re-entry does not manufacture a fresh update pass
                        // just because the app called it again.
                        // Inside Macintosh Volume I, I-405 and I-415.
                        let mut draw_proc_queue = VecDeque::new();
                        if !reused_retained_visible_snapshot {
                            for (i, item) in items.iter().enumerate() {
                                let base_type = item.item_type & 0x7F;
                                if base_type == 0
                                    && item.proc_ptr != 0
                                    && Self::dialog_item_intersects_bounds(bounds, item)
                                {
                                    draw_proc_queue.push_back((item.proc_ptr, (i + 1) as i16));
                                }
                            }
                        }
                        let has_draw_procs = !draw_proc_queue.is_empty();

                        self.dialog_modal_entered.insert(dialog_ptr);
                        self.dialog_tracking = Some(super::dispatch::DialogTrackingState {
                            dialog_ptr,
                            bounds,
                            title,
                            proc_id,
                            items,
                            default_item,
                            cancel_item,
                            edit_text,
                            edit_item,
                            saved_pixels,
                            stack_ptr: sp,
                            item_hit_ptr,
                            rendered_pixels,
                            flash_remaining: 0,
                            flash_delay: 0,
                            flash_item: 0,
                            edit_text_modified,
                            draw_proc_queue,
                            draw_procs_done: !has_draw_procs,
                            rendered_pixels_final: !has_draw_procs,
                            filter_proc,
                            game_managed,
                            last_filter_event: None,
                            popup_draws,
                            active_popup: None,
                            active_button: None,
                            active_user_item: None,
                        });
                        self.record_modal_dialog_input_trace(
                            "start",
                            dialog_ptr,
                            bounds,
                            0,
                            None,
                            None,
                            "pending",
                            "open_modal_tracking",
                        );
                        // Don't pop stack or advance PC — re-fire pattern
                    } else {
                        // No items found — fall back to returning item 1
                        eprintln!("[TRAP] ModalDialog: no items found, returning 1");
                        if item_hit_ptr != 0 {
                            bus.write_word(item_hit_ptr, 1);
                        }
                        cpu.write_reg(Register::A7, sp + 8);
                    }
                }
                Ok(())
            }

            // ========== TextEdit Manager ==========
            // TEInit ($A9CC)
            // Initializes TextEdit's internal globals.
            // PROCEDURE TEInit;
            // Inside Macintosh Volume I, I-376 ("TEInit
            // initializes TextEdit by allocating a handle for
            // the TextEdit scrap. The scrap is initially empty.
            // Call this procedure once and only once at the
            // beginning of your program."). Also note from IM:I
            // I-376: "You should call TEInit even if your
            // application doesn't use TextEdit, so that desk
            // accessories and dialog and alert boxes will work
            // correctly."
            //
            // Per IM:I I-389 the scrap globals are TEScrpHandle
            // ($0AB4, 4-byte Handle to the empty/cut/copied
            // text block) and TEScrpLength ($0AB0, 2-byte
            // INTEGER byte count). TEInit must:
            //   1. Allocate a zero-length relocatable block
            //      and store its handle at TEScrpHandle.
            //   2. Set TEScrpLength to 0 (empty scrap).
            //
            // Idempotent: per IM the routine is documented as
            // "call once and only once" but defensive impls
            // check for an existing handle and skip the
            // re-allocation to avoid leaking the prior one. We
            // do the same — apps that violate the IM contract
            // and call TEInit twice get a stable handle (no
            // double-free).
            // TEInit ($A9CC): Per IM:I I-376 allocates a zero-length scrap handle and stores it at TEScrpHandle ($0AB4); zeros TEScrpLength ($0AB0). Idempotent — repeated calls reuse the existing handle to avoid leaking. TECopy / TECut / TEPaste subsequently resize the underlying block as needed. No args, no result.
            (true, 0x1CC) => {
                use crate::memory::globals::addr;
                // Idempotency: skip re-allocation if a prior
                // TEInit (or first-touch by TECopy / TECut /
                // TEPaste) already populated the handle.
                let existing = bus.read_long(addr::TE_SCRP_HANDLE);
                if existing == 0 {
                    // Allocate a handle whose master ptr is
                    // NIL (== empty scrap). Subsequent
                    // TECopy / TECut grow the underlying
                    // block via ensure_text_handle_size which
                    // tolerates the NIL master ptr by lazy-
                    // allocating on first non-empty write.
                    // This matches the existing
                    // TECopy / TECut first-touch pattern that
                    // calls allocate_handle_with_data(bus, 0).
                    let handle = Self::allocate_handle_with_data(bus, 0);
                    bus.write_long(addr::TE_SCRP_HANDLE, handle);
                }
                bus.write_word(addr::TE_SCRP_LENGTH, 0);
                Ok(())
            }

            // TEPinScroll ($A812)
            // Scrolls the text within the view rectangle by the
            // requested (dh, dv); stops scrolling when the last line
            // of text is scrolled into view.
            // PROCEDURE TEPinScroll(dh: INTEGER; dv: INTEGER; hTE: TEHandle);
            // Inside Macintosh: Text 1993, p. 2-91.
            //
            // IM:Text 1993 p. 2-91 verbatim:
            //   "The TEPinScroll procedure scrolls the text within the
            //    view rectangle of the specified edit record by the
            //    designated number of pixels. Scrolling stops when the
            //    last line of text is scrolled into view. ... The
            //    destination rectangle is offset by the amount
            //    scrolled. ... When the edit record is longer than the
            //    text it contains, TEPinScroll displays up to the last
            //    line of text inclusive, and not beyond it."
            //
            // Sign convention (IM:Text 1993 p. 2-91):
            //   dh > 0: text moves right → destRect.left/right += dh
            //   dh < 0: text moves left  → destRect.left/right += dh
            //   dv > 0: text moves down  → destRect.top/bottom += dv
            //   dv < 0: text moves up    → destRect.top/bottom += dv
            //
            // Pascal stack frame (args push left-to-right, first
            // source arg deepest):
            //   sp+0  hTE: TEHandle           (4) — last arg, shallowest
            //   sp+4  dv:  INTEGER            (2) — middle arg
            //   sp+6  dh:  INTEGER            (2) — first arg, deepest
            // Total pop = 8 bytes; no function-result slot.
            //
            // MPW Universal Headers TextEdit.h:
            //   EXTERN_API(void) TEPinScroll(short dh, short dv,
            //                                TEHandle hTE)
            //                                  ONEWORDINLINE(0xA812);
            //
            // Pin semantics: the dv arm clamps so dest_rect.top stays
            // within [view_top - (text_bottom - view_bottom), view_top]
            // — i.e. far enough that the last line of text remains
            // visible at the bottom of the view. The dh arm applies a
            // symmetric horizontal clamp. For in-range scrolls the
            // call behaves exactly like TEScroll ($A9DD) per the IM
            // "offset by the amount scrolled" guarantee.
            //
            // Runtime proof: a812_tepinscroll_strict
            //   B1: in-range dv=-3 with 8-line text exceeds 10-pixel view height
            //       → destRect.top/bottom shift by exactly -3 (Apple/BII agree)
            //   B2: dv=-1000 on empty TERec fitting in 200-pixel view
            //       → destRect unchanged (pin clamps to zero movement)
            //   B3: Pascal PROCEDURE stack sandwich balances StackSpace.
            //
            // Regression coverage:
            //   dialog::tests::te_pin_scroll_reads_handle_from_stack_top
            //   dialog::tests::tepinscroll_in_range_negative_dv_offsets_destrect_top_and_bottom_exactly_by_dv
            //   dialog::tests::tepinscroll_pascal_lr_stack_layout_reads_dh_dv_and_hte_from_correct_offsets
            //   dialog::tests::tepinscroll_clamps_overscroll_when_last_line_is_already_visible
            // TEPinScroll ($A812): Offsets `destRect` by the requested delta and pops 8 bytes
            (true, 0x012) => {
                let sp = cpu.read_reg(Register::A7);
                let te_handle = bus.read_long(sp);
                let mut dv = bus.read_word(sp + 4) as i16;
                let mut dh = bus.read_word(sp + 6) as i16;
                cpu.write_reg(Register::A7, sp + 8);

                let te_ptr = Self::te_record_ptr(bus, te_handle);
                if te_ptr != 0 {
                    let view_rect = Self::te_read_rect(bus, te_ptr + Self::TE_VIEW_RECT_OFFSET);
                    let dest_rect = Self::te_read_rect(bus, te_ptr + Self::TE_DEST_RECT_OFFSET);

                    if dv > 0 {
                        let max_down = view_rect.0.saturating_sub(dest_rect.0);
                        if dv > max_down {
                            dv = max_down;
                        }
                    } else if dv < 0 {
                        // Scrolling up (dv < 0) is bounded by the
                        // distance between the text's bottom and
                        // the view's bottom — pinning behaviour
                        // stops once the last line is visible per
                        // Text 1993, 2-91. max_up = view_bottom -
                        // text_bottom: if text already fits (value
                        // ≥ 0) there's nothing to scroll up to, so
                        // dv clamps to 0. Otherwise max_up < 0
                        // gives the amount of up-scroll still available; clamp dv
                        // upward to max_up so it can't exceed that.
                        let text_len = Self::te_text_length(bus, te_handle);
                        let (end_top, _) = self.te_char_to_point(bus, te_handle, text_len);
                        let end_line = Self::te_char_to_line_index(bus, te_handle, text_len);
                        let text_bottom = end_top
                            .saturating_add(Self::te_height_for_line(bus, te_handle, end_line));
                        let max_up = view_rect.2.saturating_sub(text_bottom);
                        dv = if max_up >= 0 {
                            0
                        } else if dv < max_up {
                            max_up
                        } else {
                            dv
                        };
                    }

                    if dh > 0 {
                        let max_right = view_rect.1.saturating_sub(dest_rect.1);
                        dh = if max_right > 0 { dh.min(max_right) } else { 0 };
                    } else if dh < 0 {
                        let max_left = view_rect.1.saturating_sub(dest_rect.1);
                        dh = if max_left < 0 { dh.max(max_left) } else { 0 };
                    }

                    if trace_textedit_enabled() {
                        let adjusted = (
                            dest_rect.0.saturating_add(dv),
                            dest_rect.1.saturating_add(dh),
                            dest_rect.2.saturating_add(dv),
                            dest_rect.3.saturating_add(dh),
                        );
                        eprintln!(
                            "[TE] TEPinScroll hTE=${:08X} dh={} dv={} dest=({},{},{},{})",
                            te_handle, dh, dv, adjusted.0, adjusted.1, adjusted.2, adjusted.3
                        );
                    }
                    self.te_scroll_contents(cpu, bus, te_handle, dh, dv);
                }
                Ok(())
            }

            // TEAutoView ($A813)
            // Enables or disables automatic scrolling for an edit record.
            // PROCEDURE TEAutoView(fAuto: Boolean; hTE: TEHandle);
            // Text 1993, 2-92
            //
            // hTE is the LAST parameter (Pascal left-to-right push), so it
            // sits at SP+0 above the BOOLEAN at SP+4.
            // TEAutoView ($A813): Tracks the auto-scroll feature bit per TEHandle
            (true, 0x013) => {
                let sp = cpu.read_reg(Register::A7);
                let te_handle = bus.read_long(sp);
                // Pascal BOOLEAN in high byte (MPW C convention).
                let enabled = bus.read_byte(sp + 4) != 0;
                self.set_te_feature_bit(te_handle, Self::TE_FEATURE_AUTO_SCROLL, enabled);
                if trace_textedit_enabled() {
                    eprintln!("[TE] TEAutoView hTE=${:08X} enabled={}", te_handle, enabled);
                }
                cpu.write_reg(Register::A7, sp + 6);
                Ok(())
            }

            // TESelView ($A811)
            // PROCEDURE TESelView(hTE: TEHandle);
            // Inside Macintosh: Text (1993), p. 2-92.
            //
            // Per IM:Text 1993 p. 2-92 verbatim: "Once automatic scrolling
            // has been enabled by a call to the TEAutoView procedure or
            // through the TEFeatureFlag function, the TESelView procedure
            // ensures that the selection range is visible and scrolls it
            // into the view rectangle if necessary. ... The top left part
            // of the selection range is scrolled into view. ... If
            // automatic scrolling is disabled, TESelView has no effect."
            //
            // MPW Universal Headers TextEdit.h:
            //   EXTERN_API(void) TESelView(TEHandle hTE)
            //                              ONEWORDINLINE(0xA811);
            //
            // Pascal stack frame:
            //   sp+0  hTE: TEHandle  (4)
            // Total pop = 4 bytes. No function result.
            //
            // Algorithm (matches Apple's documented contract):
            //   1. If TE_FEATURE_AUTO_SCROLL is OFF on this hTE → no-op.
            //   2. Read viewRect, destRect, and the current selection
            //      range from the TERec.
            //   3. Resolve sel_start and sel_end character offsets to
            //      pixel coordinates (top_left of selection range and
            //      bottom_right via line-height lookup).
            //   4. Compute dh, dv via te_getdelta — the per-axis shift
            //      that brings the selection rectangle inside viewRect
            //      (zero if the selection is already inside).
            //   5. Call te_scroll_contents which adds (dh, dv) to
            //      destRect.{top,left,bottom,right} and redraws.
            //
            // BasiliskII-vs-Apple divergence note:
            //   The strict bake at a811_teselview_strict
            //   witnesses three engines-agree cases (auto-scroll disabled,
            //   selection in view, Pascal stack discipline). BasiliskII
            //   System 7.5.3 ROM does NOT scroll destRect when auto-scroll
            //   is enabled and the selection lies below viewRect — pre and
            //   post destRect coincide at (0,0,200,30) in the diagnostic
            //   probe. Apple's IM:Text 1993 p. 2-92 says this case must
            //   scroll. Systemless implements the Apple-canonical semantic
            //   (declared via `hle_matches = "apple"` on the catalog row);
            //   the divergent rule is pinned by the contract test
            //   `teselview_apple_canonical_below_view_shifts_destrect_up`
            //   plus the existing assertion-bearing tests in this module.
            (true, 0x011) => {
                let sp = cpu.read_reg(Register::A7);
                let te_handle = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);
                if self.te_feature_bit(te_handle, Self::TE_FEATURE_AUTO_SCROLL) {
                    let te_ptr = Self::te_record_ptr(bus, te_handle);
                    if te_ptr != 0 {
                        let view_rect = Self::te_read_rect(bus, te_ptr + Self::TE_VIEW_RECT_OFFSET);
                        let dest_rect = Self::te_read_rect(bus, te_ptr + Self::TE_DEST_RECT_OFFSET);
                        let sel_start = bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize;
                        let sel_end = bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize;
                        let (start_top, start_left) =
                            self.te_char_to_point(bus, te_handle, sel_start);
                        let (stop_top, stop_left) = self.te_char_to_point(bus, te_handle, sel_end);
                        let stop_line = Self::te_char_to_line_index(bus, te_handle, sel_end);
                        let stop_bottom = stop_top
                            .saturating_add(Self::te_height_for_line(bus, te_handle, stop_line));
                        let dv =
                            Self::te_getdelta(start_top, stop_bottom, view_rect.0, view_rect.2);
                        let dh = Self::te_getdelta(start_left, stop_left, view_rect.1, view_rect.3);
                        if trace_textedit_enabled() {
                            let adjusted = (
                                dest_rect.0.saturating_add(dv),
                                dest_rect.1.saturating_add(dh),
                                dest_rect.2.saturating_add(dv),
                                dest_rect.3.saturating_add(dh),
                            );
                            eprintln!(
                                "[TE] TESelView hTE=${:08X} view=({},{},{},{}) dest=({},{},{},{}) adjusted=({},{},{},{})",
                                te_handle,
                                view_rect.0,
                                view_rect.1,
                                view_rect.2,
                                view_rect.3,
                                dest_rect.0,
                                dest_rect.1,
                                dest_rect.2,
                                dest_rect.3,
                                adjusted.0,
                                adjusted.1,
                                adjusted.2,
                                adjusted.3
                            );
                        }
                        self.te_scroll_contents(cpu, bus, te_handle, dh, dv);
                    }
                }
                Ok(())
            }

            // ========== Cursor Manager ==========

            // InitCursor ($A850) - Toolbox version
            // Resets to standard arrow cursor
            // InitCursor ($A850): Sets arrow cursor, resets cursor level to 0,
            // makes visible (IM:I I-167).
            (true, 0x050) => {
                self.cursor_data = Some(Self::default_arrow_cursor_image());
                self.cursor_level = 0;
                self.cursor_visible = true;
                Ok(())
            }

            // SetCursor ($A851)
            // PROCEDURE SetCursor(crsr: Cursor);
            // Cursor record: data[32] + mask[32] + hotSpot.v(2) + hotSpot.h(2) = 68 bytes
            // SetCursor ($A851): Reads 68-byte cursor record (16×16 data + mask + hotspot).
            // Per IM:I I-167, if the cursor is hidden it stays hidden and only
            // changes appearance when uncovered by matching ShowCursor calls.
            (true, 0x051) => {
                let sp = cpu.read_reg(Register::A7);
                let crsr_ptr = bus.read_long(sp);
                cpu.write_reg(Register::A7, sp + 4);

                // Read cursor bitmap (16x16 = 32 bytes)
                let mut data = [0u8; 32];
                for (i, byte) in data.iter_mut().enumerate() {
                    *byte = bus.read_byte(crsr_ptr + i as u32);
                }
                // Read cursor mask (16x16 = 32 bytes)
                let mut mask = [0u8; 32];
                for (i, byte) in mask.iter_mut().enumerate() {
                    *byte = bus.read_byte(crsr_ptr + 32 + i as u32);
                }
                // Read hotspot
                let hot_v = bus.read_word(crsr_ptr + 64) as i16;
                let hot_h = bus.read_word(crsr_ptr + 66) as i16;

                self.cursor_data = Some(CursorImage::mono(data, mask, hot_v, hot_h));
                self.cursor_visible = self.cursor_level == 0;
                Ok(())
            }

            // HideCursor ($A852)
            // HideCursor ($A852): Decrements cursor level and hides while level < 0
            // per IM:I I-168.
            (true, 0x052) => {
                self.cursor_level = self.cursor_level.saturating_sub(1);
                self.cursor_visible = self.cursor_level == 0;
                Ok(())
            }

            // ShowCursor ($A853)
            // ShowCursor ($A853): Increments cursor level toward 0; extra calls
            // at level 0 are no-op (IM:I I-168).
            (true, 0x053) => {
                if self.cursor_level < 0 {
                    self.cursor_level += 1;
                }
                self.cursor_visible = self.cursor_level == 0;
                Ok(())
            }

            // ObscureCursor ($A856)
            // PROCEDURE ObscureCursor;
            // Inside Macintosh Volume I, I-168
            // Imaging With QuickDraw 1994, p. 8-29
            //
            // MPW Universal Headers (Quickdraw.h):
            //
            //   EXTERN_API(void) ObscureCursor(void) ONEWORDINLINE(0xA856);
            //
            // Pascal PROCEDURE with no arguments and no result slot:
            // caller pushes 0 bytes; trap pops 0 bytes; SP unchanged.
            //
            // Per IM:I I-168: "ObscureCursor hides the cursor until
            // the next time the mouse is moved. It's normally
            // called when the user begins to type. Unlike
            // HideCursor, it has no effect on the cursor level and
            // must not be balanced by a call to ShowCursor."
            //
            // HLE compromise: Systemless synthesizes mouse-move events
            // every frame from the scripted event source (or
            // every interactive frame from systemless). Honouring
            // the "hide until next mouse move" semantic would keep
            // the cursor PERMANENTLY hidden because each
            // synthesized mouse-move arrives before any "is the
            // mouse stationary?" check can materialise the cursor
            // (every frame produces both the obscure-trigger and
            // the un-obscure-trigger simultaneously). Treating it
            // as a no-op preserves cursor visibility — HideCursor
            // ($A852) / ShowCursor ($A853) still operate the
            // level-counter hide/show stack for explicit pairs in
            // apps that need them. Per IM:I I-168 explicit
            // "must not be balanced by a call to ShowCursor"
            // means apps universally call ObscureCursor without a
            // matching ShowCursor — so the no-op contract leaves
            // them in the same observable state (cursor visible,
            // level unchanged) regardless of dispatch.
            //
            // Engines-agree subset (witnessed by the strict bake
            // `a855_a856_shieldcursor_obscurecursor_strict`):
            //   - 0-byte pop balanced across single + 5-call
            //     StackSpace sandwiches; SP unchanged externally.
            // The Apple-canonical "hides until mouse move" and
            // "must not be balanced by ShowCursor" rules are pinned
            // in-Rust via `obscure_cursor_noop_preserves_cursor_level_visibility_and_stack`
            // and declared `witness_kind = "contract"` in the row
            // since BII and Systemless HLE diverge on the LowMem CrsrVis
            // side-effect (BII System 7.5.3 ROM writes CrsrVis;
            // Systemless HLE keeps cursor state internal).
            //
            // ObscureCursor ($A856): No args / no result per IM:I I-168 MPW C declaration ObscureCursor(void) ONEWORDINLINE(0xA856) — HLE no-op; SP unchanged across calls.
            (true, 0x056) => Ok(()),

            // GetCursor ($A9B9)
            // FUNCTION GetCursor(cursorID: INTEGER): CursHandle;
            // Inside Macintosh Volume I, I-474
            //
            // "GetCursor returns a handle to the cursor having the
            // given resource ID, reading it from the resource file if
            // necessary. It calls the Resource Manager function
            // GetResource('CURS', cursorID). If the resource can't be
            // read, GetCursor returns NIL." — IM:I I-474.
            //
            // HLE compromise: Systemless doesn't load the System file's
            // resource fork, so the four standard system cursor IDs
            // documented at IM:I I-475..I-477 are synthesized here
            // via [`Self::synthesize_system_cursor`] (cached for
            // handle stability — apps cache the GetCursor result at
            // boot and pass it to SetCursor every frame). Any other
            // ID falls through to the IM-correct NIL miss path.
            //
            // The previous Stub allocated a fresh 68-byte zero-filled
            // block on every miss and returned a handle to it —
            // strictly worse than NIL since callers that defensively
            // check `if handle = NIL` got a non-NIL pointer to an
            // empty/white cursor and SetCursor'd a blank cursor onto
            // the screen. Same fallback issue as the GetIcon ($A9BB)
            // 128-byte uninitialised-heap stub closed by the
            // family-level resource fallback audit.
            //
            // Pop = 2 (cursorID INTEGER), result CursHandle at new SP+0.
            // GetCursor ($A9B9): Per IM:I I-474 calls GetResource('CURS', cursorID); on hit returns stable handle via get_or_create_resource_handle; on miss synthesizes built-in cursor 1..4 (iBeam/cross/plus/watch per IM:I I-475..I-477) via cached synthesize_system_cursor; otherwise returns NIL. Pops 2 bytes (cursorID), 4-byte CursHandle result at new SP+0.
            (true, 0x1B9) => {
                let sp = cpu.read_reg(Register::A7);
                let cursor_id = bus.read_word(sp) as i16;

                let handle = if let Some((refnum, ptr)) =
                    self.find_or_load_resource_any(bus, *b"CURS", cursor_id)
                {
                    self.get_or_create_resource_handle_in_file(
                        bus, *b"CURS", cursor_id, ptr, refnum,
                    )
                } else if let Some(ptr) = self.synthesize_system_cursor(bus, cursor_id) {
                    // Built-in cursor synthesised + cached. Use the
                    // resource-handle helper so subsequent GetCursor
                    // calls for the same ID return the same handle.
                    self.get_or_create_resource_handle(bus, *b"CURS", cursor_id, ptr)
                } else {
                    0
                };

                bus.write_long(sp + 2, handle);
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // GetPattern ($A9B8)
            // FUNCTION GetPattern(patID: INTEGER): PatHandle;
            // Inside Macintosh Volume I, I-473
            //
            // "GetPattern returns a handle to the pattern having the
            // given resource ID, reading it from the resource file if
            // necessary. It calls the Resource Manager function
            // GetResource('PAT ', patID). If the resource can't be
            // read, GetPattern returns NIL." — IM:I I-473.
            //
            // The previous Stub allocated a fresh 8-byte all-0xFF
            // (white) pattern on every miss and returned a handle to
            // it — strictly worse than NIL since callers that
            // defensively check `if handle = NIL then use_default
            // else FillRect(rect, handle^^)` got a non-NIL handle and
            // proceeded to FillRect with white instead of taking the
            // recovery branch. Same fallback issue as the GetIcon
            // ($A9BB) and GetCursor ($A9B9) fallbacks closed in this
            // family's audit pass.
            //
            // Pop = 2 (patID INTEGER), result PatHandle at new SP+0.
            // GetPattern ($A9B8): Per IM:I I-473 calls GetResource('PAT ', patID); on hit returns stable handle via get_or_create_resource_handle; on miss returns NIL (previously a fresh all-0xFF white pattern, which made callers branching on `handle = NIL` take the wrong path). Pops 2 bytes (patID), 4-byte PatHandle result at new SP+0.
            (true, 0x1B8) => {
                let sp = cpu.read_reg(Register::A7);
                let pat_id = bus.read_word(sp) as i16;

                let handle = if let Some((refnum, ptr)) =
                    self.find_or_load_resource_any(bus, *b"PAT ", pat_id)
                {
                    self.get_or_create_resource_handle_in_file(bus, *b"PAT ", pat_id, ptr, refnum)
                } else {
                    0
                };

                bus.write_long(sp + 2, handle);
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // GetIcon ($A9BB)
            // Returns a handle to the icon stored in the 'ICON' resource
            // with the given ID. Equivalent to GetResource('ICON', iconID).
            // The resource is a 128-byte black-and-white bitmap (32x32
            // pixels at 1 bit each).
            // FUNCTION GetIcon(iconID: INTEGER): Handle;
            // Inside Macintosh Volume I, I-473
            //
            // Mirrors GetPicture ($A9BC) exactly: look up the
            // resource via the dispatcher's resource search chain;
            // on hit, materialise (or reuse) a stable handle that
            // points at the loaded resource bytes; on miss, return
            // NIL per IM:I I-473 ("If the resource can't be read,
            // GetIcon returns NIL").
            //
            // The previous Stub allocated a fresh 128-byte block of
            // UNINITIALISED memory and returned a handle to it on
            // every call — strictly worse than NIL since callers
            // pass that handle to PlotIcon ($A94B) which CopyBits
            // the random bytes onto the framebuffer. Apps with a
            // missing 'ICON' that defensively check `if handle =
            // NIL` would crash on the dereference path; apps that
            // trust the result blindly would render a junk icon.
            // The proper Partial impl returns NIL on miss so both
            // branches behave correctly.
            //
            // Pop = 2 (iconID INTEGER), result Handle at new SP+0.
            // GetIcon ($A9BB): Per IM:I I-473 calls GetResource('ICON', iconID); returns handle to the loaded resource via get_or_create_resource_handle (stable handle reused across calls), or NIL if the ICON resource is missing. Pops 2 bytes (iconID), 4-byte Handle result at new SP+0. Mirrors GetPicture ($A9BC).
            (true, 0x1BB) => {
                let sp = cpu.read_reg(Register::A7);
                let icon_id = bus.read_word(sp) as i16;

                let handle = if let Some((refnum, ptr)) =
                    self.find_or_load_resource_any(bus, *b"ICON", icon_id)
                {
                    let h = self
                        .get_or_create_resource_handle_in_file(bus, *b"ICON", icon_id, ptr, refnum);
                    eprintln!(
                        "[TRAP] GetIcon({}) -> handle=${:08X} ptr=${:08X}",
                        icon_id, h, ptr
                    );
                    h
                } else {
                    eprintln!("[TRAP] GetIcon({}) -> NIL (not found)", icon_id);
                    0
                };

                bus.write_long(sp + 2, handle);
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // GetPicture ($A9BC)
            // Returns a handle to the picture stored in the 'PICT' resource
            // with the given ID. Equivalent to GetResource('PICT', picID).
            // FUNCTION GetPicture(picID: INTEGER): PicHandle;
            // Inside Macintosh Volume I, I-475
            // GetPicture ($A9BC): Loads PICT resource via GetResource, returns handle
            (true, 0x1BC) => {
                let sp = cpu.read_reg(Register::A7);
                let pic_id = bus.read_word(sp) as i16;

                let handle = if let Some((refnum, ptr)) =
                    self.find_or_load_resource_any(bus, *b"PICT", pic_id)
                {
                    let h = self
                        .get_or_create_resource_handle_in_file(bus, *b"PICT", pic_id, ptr, refnum);
                    eprintln!(
                        "[TRAP] GetPicture({}) -> handle=${:08X} ptr=${:08X}",
                        pic_id, h, ptr
                    );
                    h
                } else {
                    eprintln!("[TRAP] GetPicture({}) -> NIL (not found)", pic_id);
                    0
                };

                bus.write_long(sp + 2, handle);
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // GetString ($A9BA)
            // Returns a handle to the 'STR ' resource with the given ID.
            // FUNCTION GetString (stringID: INTEGER): StringHandle;
            // Text 1993, 5-49; Inside Macintosh Volume I, I-468
            // GetString ($A9BA): Returns the loaded `'STR '` resource handle or NIL when missing
            (true, 0x1BA) => {
                let sp = cpu.read_reg(Register::A7);
                let string_id = bus.read_word(sp) as i16;
                let handle = if let Some((refnum, ptr)) =
                    self.find_or_load_resource_any(bus, *b"STR ", string_id)
                {
                    self.get_or_create_resource_handle_in_file(
                        bus, *b"STR ", string_id, ptr, refnum,
                    )
                } else if let Some(ptr) = self.synthesize_system_str(bus, string_id) {
                    self.get_or_create_resource_handle(bus, *b"STR ", string_id, ptr)
                } else {
                    0
                };
                bus.write_long(sp + 2, handle);
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // ========== TextEdit Manager Stubs ==========

            // TENew ($A9D2)
            // FUNCTION TENew(destRect, viewRect: Rect): TEHandle;
            // Inside Macintosh Volume I (1985), p. I-373..I-374.
            // Text 1993, 2-85..2-86.
            //
            // IM:I I-374: TENew "creates and initializes the necessary
            // data structures, allocates an edit record, returns a
            // handle to it, and sets that handle's selection range,
            // view rectangle, destination rectangle, and other fields."
            //
            // Fresh TERec state per IM:I I-373:
            //   destRect, viewRect = caller-supplied
            //   selStart = selEnd = 0
            //   teLength = 0
            //   hText = handle to empty char buffer (non-NIL)
            //   txFont, txFace, txMode, txSize copied from current grafPort
            //   inPort = current grafPort
            //
            // Calling-convention duality. Classic Inside Macintosh
            // declares TENew with Pascal by-value Rect parameters
            // (16 bytes on the stack). MPW Universal Headers
            // (TextEdit.h) modernise it to pointer parameters:
            //   EXTERN_API(TEHandle) TENew(const Rect *destRect,
            //                              const Rect *viewRect)
            //                                  ONEWORDINLINE(0xA9D2);
            // BasiliskII System 7.5.3 ROM accepts the pointer-arg
            // convention (the bake fixture uses it). Systemless's HLE
            // sniffs which convention the caller used by inspecting
            // whether the first two long words on the stack point at
            // plausible Rects (te_new_rect_args at dialog.rs:312..348)
            // and pops either 8 bytes (pointer convention) or 16
            // bytes (by-value convention) accordingly.
            //
            // Strict bake witnesses 5 documented behaviors:
            //   * a9d2_tenew_strict
            //
            // Regression coverage (this file):
            //   tenew_pointer_arg_convention_initializes_destrect_viewrect_and_returns_non_nil_handle
            //   tenew_fresh_terec_has_zero_telength_and_empty_selection_per_im_i_373
            //   tenew_function_protocol_consumes_two_pointer_args_and_writes_4_byte_result
            //
            // TENew ($A9D2): Allocates and initializes a basic monostyled TERec plus empty `hText` handle; supports both pointer-arg and by-value-rect conventions per te_new_rect_args sniffing
            (true, 0x1D2) => {
                let sp = cpu.read_reg(Register::A7);
                let handle = Self::allocate_te_handle(bus);
                let (dest_rect, view_rect, stack_pop) = Self::te_new_rect_args(bus, sp);
                self.initialize_te_record(bus, handle, dest_rect, view_rect);
                bus.write_long(sp + stack_pop, handle);
                cpu.write_reg(Register::A7, sp + stack_pop);
                Ok(())
            }

            // TEStyleNew ($A83E)
            // Creates a multistyled edit record in the current port.
            // FUNCTION TEStyleNew(destRect: Rect; viewRect: Rect): TEHandle;
            // Inside Macintosh: Text (1993), p. 2-78.
            //
            // IM:Text 1993 p. 2-78 verbatim:
            //   "The TEStyleNew function creates a multistyled edit
            //    record and allocates a handle to it... TEStyleNew
            //    sets the txSize, lineHeight, and fontAscent fields
            //    of the edit record to -1, allocates a style record,
            //    and stores a handle to the style record in the
            //    txFont and txFace fields. The TEStyleNew function
            //    creates and initializes a null scrap that is used
            //    by TextEdit routines throughout the life of the
            //    edit record."
            //
            // MPW Universal Headers (TextEdit.h):
            //   EXTERN_API(TEHandle)
            //   TEStyleNew(const Rect *destRect,
            //              const Rect *viewRect)   ONEWORDINLINE(0xA83E);
            //
            // Calling convention: identical to TENew. Pascal pushes
            // left-to-right, so destRect (first arg) lands deepest
            // and viewRect (second arg) lands shallowest:
            //   sp+0..3   viewRect_ptr  (last pushed)
            //   sp+4..7   destRect_ptr  (first pushed)
            // Both pointer (8-byte) and by-value (16-byte) forms are
            // accepted via te_new_rect_args sniffing.
            //
            // Styled-record signature, per IM:Text 1993 p. 2-78
            // (initialize_styled_te_record at dialog.rs:843..):
            //   txSize     = -1   sentinel at offset 0x50
            //   lineHeight = -1   sentinel at offset 0x18
            //   fontAscent = -1   sentinel at offset 0x1A
            //   txFont/txFace (4-byte overlay at offset 0x4A) holds
            //                     the TEStyleHandle.
            //
            // Strict bake witnesses 5 documented behaviors:
            //   * a83e_testylenew_strict
            //
            // Regression coverage (this file):
            //   testylenew_returns_styled_handle_and_initializes_sentinel_fields
            //   testylenew_pointer_arg_convention_initializes_destrect_viewrect_and_styled_sentinels
            //   testylenew_function_protocol_consumes_two_pointer_args_and_writes_4_byte_result
            //
            // TEStyleNew ($A83E): Allocates a TEHandle and initializes a multistyled record (destRect + viewRect + txSize/lineHeight/fontAscent=-1 sentinels + non-NIL TEStyleHandle); style runs and null scrap allocated per Text 1993, 2-78
            (true, 0x03E) => {
                let sp = cpu.read_reg(Register::A7);
                let handle = Self::allocate_te_handle(bus);
                let (dest_rect, view_rect, stack_pop) = Self::te_new_rect_args(bus, sp);
                self.initialize_styled_te_record(bus, handle, dest_rect, view_rect);
                bus.write_long(sp + stack_pop, handle);
                cpu.write_reg(Register::A7, sp + stack_pop);
                Ok(())
            }

            // TEGetOffset ($A83C)
            // FUNCTION TEGetOffset(pt: Point; hTE: TEHandle): INTEGER;
            // Inside Macintosh Volume V (1986), p. V-172.
            //
            // IM:V V-172 verbatim: "TEGetOffset returns the character
            // position closest to the point pt. The point pt is in
            // local coordinates relative to the destination rectangle.
            // If pt is above the first line, TEGetOffset returns the
            // character offset of the start of the first line. If pt
            // is below the last line, TEGetOffset returns the
            // character offset of the end of the text."
            //
            // MPW Universal Headers (TextEdit.h):
            //   EXTERN_API(short)
            //   TEGetOffset(Point pt, TEHandle hTE) ONEWORDINLINE(0xA83C);
            //
            // Calling convention. `EXTERN_API` expands to `extern
            // pascal` on the 68k target, so the Pascal LR push order
            // applies: pt (the first arg) is pushed FIRST and lands
            // DEEPEST on the stack; hTE (the last arg) is pushed LAST
            // and lands SHALLOWEST. Point is a 4-byte record with
            // pt.v at the lower address and pt.h at the higher
            // address. Pascal FUNCTION pre-allocates the 2-byte
            // INTEGER result slot just above the args. Stack layout
            // at trap entry:
            //   sp+0..3   hTE      (4 bytes, last pushed)
            //   sp+4..5   pt.v     (2 bytes, first half of Point)
            //   sp+6..7   pt.h     (2 bytes, second half of Point)
            //   sp+8..9   function result slot (2 bytes)
            //
            // Pre-fix (commit ca6a0ebf — A9D2 te_new_rect_args
            // Pascal-LR fix only covered TENew + TEStyleNew sharing
            // the te_new_rect_args helper): this arm read te_handle
            // from sp+2 and pt.v/pt.h from sp+6/sp+8, off-by-2 versus
            // the canonical Pascal LR layout. The strict bake at
            // a83c_tegetoffset_strict exposed the
            // bug (B2 returned 0 instead of teLength=5 because the
            // off-by-2 read placed garbage in te_handle and the
            // te_point_to_char helper bailed via the NIL TERec
            // branch). Fixed by reading args at the canonical sp+0,
            // sp+4, sp+6 offsets.
            //
            // Strict bake witnesses 3 documented behaviors:
            //   * a83c_tegetoffset_strict
            //
            // Regression coverage (this file):
            //   tegetoffset_point_above_destrect_returns_zero
            //   tegetoffset_point_below_last_line_returns_telength
            //   tegetoffset_function_protocol_consumes_point_and_tehandle_args_writes_integer_result
            //
            // TEGetOffset ($A83C): Maps a point back to a character offset using the line-starts / per-line heights and primary-run advance widths per IM:V V-172. Pascal LR push order — sp+0 hTE (last pushed), sp+4 pt.v, sp+6 pt.h, sp+8 INTEGER result slot.
            (true, 0x03C) => {
                let sp = cpu.read_reg(Register::A7);
                let te_handle = bus.read_long(sp);
                let point_v = bus.read_word(sp + 4) as i16;
                let point_h = bus.read_word(sp + 6) as i16;
                let offset = self.te_point_to_char(bus, te_handle, (point_v, point_h));
                bus.write_word(sp + 8, offset as u16);
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // TEFindWord ($A0FE)
            // Register-based TextEdit hook:
            //   currentPos in D0.W, caller in D2.W, pTE in A3.L, hTE in A4.L.
            //   wordStart returns in D0.W and wordEnd in D1.W.
            // Inside Macintosh: Text (1993), pp. 2-60..2-61.
            (false, 0x0FE) => {
                let current_pos = (cpu.read_reg(Register::D0) as u16) as usize;
                let _caller = cpu.read_reg(Register::D2);
                let _p_te = cpu.read_reg(Register::A3);
                let h_te = cpu.read_reg(Register::A4);
                let (word_start, word_end) = self.te_find_word_bounds(bus, h_te, current_pos);
                cpu.write_reg(Register::D0, u32::from(word_start));
                cpu.write_reg(Register::D1, u32::from(word_end));
                Ok(())
            }

            // TEDispatch ($A83D)
            // Selector-based dispatcher for styled TextEdit routines.
            // FUNCTION/PROCEDURE TEDispatch(...); selector is the first stack word.
            // Inside Macintosh Volume VI, 15-22 (TEFeatureFlag),
            //                              15-25..15-43 (selector mapping),
            //                              15-34 (TEContinuousStyle).
            // Inside Macintosh: Text (1993), p. 2-102 (TEContinuousStyle),
            //                                p. 2-92 / 2-97 (autoscroll default).
            //
            // TEDispatch ($A83D) selectors: $0000 TEStylPaste,
            //   $0001 TESetStyle, $0002 TEReplaceStyle, $0003 TEGetStyle,
            //   $0004 GetStylHandle, $0005 SetStylHandle, $0006 GetStylScrap,
            //   $0007 TEStylInsert, $0008 TEGetPoint, $0009 TEGetHeight,
            //   $000A TEContinuousStyle, $000B TEUseStyleScrap,
            //   $000C TECustomHook, $000D TENumStyles, $000E TEFeatureFlag.
            //
            // MPW Universal Headers TextEdit.h declares each entry point via
            // THREEWORDINLINE(0x3F3C, <selector>, 0xA83D). The 0x3F3C is
            // `MOVE.W #imm,-(A7)` which pre-pushes the selector word at the
            // call site, immediately before the A-line trap. Pascal LR
            // calling convention pushes args left-to-right (first arg
            // deepest), so for any N-arg TEDispatch selector the stack
            // layout at trap entry is:
            //   sp+0           selector word (2 bytes, last pushed)
            //   sp+2           arg[N-1] (last Pascal arg, shallowest)
            //   ...
            //   sp+(2 + S_0+..+S_{N-2})  arg[0] (first Pascal arg, deepest)
            //   sp+(2 + Σ S_i) function result slot (for FUNCTION selectors)
            //
            // Selector $000A TEContinuousStyle is FUNCTION (Boolean result):
            //   EXTERN_API(Boolean) TEContinuousStyle(short *mode,
            //                                         TextStyle *aStyle,
            //                                         TEHandle hTE)
            //       THREEWORDINLINE(0x3F3C, 0x000A, 0xA83D);
            //   Stack: sp+0 selector, sp+2 hTE (4), sp+6 aStyle* (4),
            //          sp+10 mode* (4), sp+14 Boolean result slot.
            //   Per IM:Text 1993 p. 2-102: returns TRUE for unstyled edit
            //   records and reports the global style attributes for the
            //   mode bits requested by *mode (font=1, face=2, size=4,
            //   color=8).
            //
            // Selector $000E TEFeatureFlag is FUNCTION (short result):
            //   EXTERN_API(short) TEFeatureFlag(short feature, short action,
            //                                   TEHandle hTE)
            //       THREEWORDINLINE(0x3F3C, 0x000E, 0xA83D);
            //   Stack: sp+0 selector, sp+2 hTE (4), sp+6 action (2),
            //          sp+8 feature (2), sp+10 short result slot.
            //   Per IM:VI 15-22: turns features on/off or tests them;
            //   returns the PRIOR setting of the bit (which, for
            //   teBitTest=-1, coincides with the current setting since
            //   the bit is not mutated). Action codes are teBitClear=0,
            //   teBitSet=1, teBitTest=-1.
            //
            // Strict bake coverage:
            //   a83d_tedispatch_strict witnesses
            //   selectors $000A and $000E end-to-end against BasiliskII:
            //   teBitTest on a fresh TENew record returns 0 (auto-scroll
            //   defaults off per IM:Text 1993 p. 2-97); teBitSet returns
            //   the prior 0 and a follow-up teBitTest returns 1;
            //   teBitClear returns the prior 1 and a follow-up teBitTest
            //   returns 0; TEContinuousStyle returns TRUE on an unstyled
            //   record and overwrites the caller's poisoned aStyle fields;
            //   StackSpace round-trips zero across a TEFeatureFlag call,
            //   pinning the 10-byte arg pop + 2-byte short result Pascal
            //   FUNCTION protocol under THREEWORDINLINE dispatch.
            //
            // Contract test coverage (this module):
            //   te_dispatch_feature_flag_test_action_returns_current_state
            //   te_dispatch_feature_flag_tracks_auto_scroll_state
            //     (teBitSet on default-off feature returns prior 0 and mutates the bit)
            //   tefeatureflag_clear_action_returns_prior_one_state_and_clears_bit
            //     (teBitClear on previously-set feature returns prior 1 and clears the bit)
            //   te_dispatch_continuous_style_returns_unstyled_record_style
            //   tedispatch_function_protocol_consumes_threewordinline_stack_frame_for_tefeatureflag
            //   teautoview_and_tefeatureflag_observe_shared_autoscroll_state
            (true, 0x03D) => {
                let sp = cpu.read_reg(Register::A7);
                let selector = bus.read_word(sp);
                if trace_textedit_enabled() {
                    eprintln!("[TE] TEDispatch selector=${:04X} sp=${:08X}", selector, sp);
                }
                match selector {
                    0x0000 => {
                        // TEStylePaste ($A83D, selector $0000)
                        // PROCEDURE TEStylePaste(hTE: TEHandle);
                        cpu.write_reg(Register::A7, sp + 6);
                    }
                    0x0001 => {
                        // TESetStyle ($A83D, selector $0001)
                        // Sets the current selection's style in a styled edit record.
                        // PROCEDURE TESetStyle(mode: INTEGER; newStyle: TextStyle; redraw: BOOLEAN; hTE: TEHandle);
                        // Inside Macintosh Volume VI, 15-32
                        let te_handle = bus.read_long(sp + 2);
                        // Pascal BOOLEAN in high byte (MPW C convention).
                        let redraw = bus.read_byte(sp + 6) != 0;
                        let style_ptr = bus.read_long(sp + 8);
                        let mode = bus.read_word(sp + 12);
                        if trace_textedit_enabled() {
                            eprintln!(
                                "[TE] TESetStyle hTE=${:08X} mode=${:04X} redraw={} style_ptr=${:08X}",
                                te_handle, mode, redraw, style_ptr
                            );
                            if style_ptr != 0 {
                                eprintln!(
                                    "[TE] TESetStyle values font={} face=${:04X} size={} color=(${:04X},${:04X},${:04X})",
                                    bus.read_word(style_ptr) as i16,
                                    bus.read_word(style_ptr + 2),
                                    bus.read_word(style_ptr + 4) as i16,
                                    bus.read_word(style_ptr + 6),
                                    bus.read_word(style_ptr + 8),
                                    bus.read_word(style_ptr + 10),
                                );
                            }
                        }
                        if style_ptr != 0 {
                            let te_ptr = Self::te_record_ptr(bus, te_handle);
                            if te_ptr != 0 {
                                let style_handle = Self::te_style_handle(bus, te_handle);
                                if style_handle != 0 {
                                    let style_ptr_record = bus.read_long(style_handle);
                                    if style_ptr_record != 0 {
                                        let table_handle = bus.read_long(
                                            style_ptr_record + Self::TE_STYLE_STYLE_TABLE_OFFSET,
                                        );
                                        let table_ptr = if table_handle != 0 {
                                            bus.read_long(table_handle)
                                        } else {
                                            0
                                        };
                                        if table_ptr != 0 {
                                            if (mode & 0x0001) != 0 {
                                                bus.write_word(
                                                    table_ptr + Self::ST_ELEMENT_FONT_OFFSET,
                                                    bus.read_word(style_ptr),
                                                );
                                            }
                                            if (mode & 0x0002) != 0 {
                                                bus.write_word(
                                                    table_ptr + Self::ST_ELEMENT_FACE_OFFSET,
                                                    bus.read_word(style_ptr + 2),
                                                );
                                            }
                                            if (mode & 0x0004) != 0 {
                                                bus.write_word(
                                                    table_ptr + Self::ST_ELEMENT_SIZE_OFFSET,
                                                    bus.read_word(style_ptr + 4),
                                                );
                                            }
                                            if (mode & 0x0008) != 0 {
                                                bus.write_word(
                                                    table_ptr + Self::ST_ELEMENT_COLOR_OFFSET,
                                                    bus.read_word(style_ptr + 6),
                                                );
                                                bus.write_word(
                                                    table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 2,
                                                    bus.read_word(style_ptr + 8),
                                                );
                                                bus.write_word(
                                                    table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 4,
                                                    bus.read_word(style_ptr + 10),
                                                );
                                            }

                                            let resolved_font = bus
                                                .read_word(table_ptr + Self::ST_ELEMENT_FONT_OFFSET)
                                                as i16;
                                            let resolved_size = bus
                                                .read_word(table_ptr + Self::ST_ELEMENT_SIZE_OFFSET)
                                                as i16;
                                            let metrics = get_font_metrics(
                                                resolved_font,
                                                Self::font_lookup_size(resolved_size),
                                            );
                                            let line_height =
                                                metrics.ascent + metrics.descent + metrics.leading;
                                            bus.write_word(
                                                table_ptr + Self::ST_ELEMENT_HEIGHT_OFFSET,
                                                line_height as u16,
                                            );
                                            bus.write_word(
                                                table_ptr + Self::ST_ELEMENT_ASCENT_OFFSET,
                                                metrics.ascent as u16,
                                            );

                                            let lh_handle = bus.read_long(
                                                style_ptr_record + Self::TE_STYLE_LH_TABLE_OFFSET,
                                            );
                                            let lh_ptr = if lh_handle != 0 {
                                                bus.read_long(lh_handle)
                                            } else {
                                                0
                                            };
                                            if lh_ptr != 0 {
                                                bus.write_word(
                                                    lh_ptr + Self::LH_ELEMENT_HEIGHT_OFFSET,
                                                    line_height as u16,
                                                );
                                                bus.write_word(
                                                    lh_ptr + Self::LH_ELEMENT_ASCENT_OFFSET,
                                                    metrics.ascent as u16,
                                                );
                                            }
                                        }
                                    }
                                } else {
                                    if (mode & 0x0001) != 0 {
                                        bus.write_word(
                                            te_ptr + Self::TE_TX_FONT_OFFSET,
                                            bus.read_word(style_ptr),
                                        );
                                    }
                                    if (mode & 0x0002) != 0 {
                                        bus.write_word(
                                            te_ptr + Self::TE_TX_FACE_OFFSET,
                                            bus.read_word(style_ptr + 2),
                                        );
                                    }
                                    if (mode & 0x0004) != 0 {
                                        bus.write_word(
                                            te_ptr + Self::TE_TX_SIZE_OFFSET,
                                            bus.read_word(style_ptr + 4),
                                        );
                                    }

                                    let resolved_font =
                                        bus.read_word(te_ptr + Self::TE_TX_FONT_OFFSET) as i16;
                                    let resolved_size =
                                        bus.read_word(te_ptr + Self::TE_TX_SIZE_OFFSET) as i16;
                                    let metrics = get_font_metrics(
                                        resolved_font,
                                        Self::font_lookup_size(resolved_size),
                                    );
                                    bus.write_word(
                                        te_ptr + Self::TE_LINE_HEIGHT_OFFSET,
                                        (metrics.ascent + metrics.descent + metrics.leading) as u16,
                                    );
                                    bus.write_word(
                                        te_ptr + Self::TE_FONT_ASCENT_OFFSET,
                                        metrics.ascent as u16,
                                    );
                                }
                            }
                        }
                        let _ = redraw;
                        cpu.write_reg(Register::A7, sp + 14);
                    }
                    0x0002 => {
                        // TEReplaceStyle ($A83D, selector $0002)
                        // PROCEDURE TEReplaceStyle(mode: INTEGER;
                        //     oldStyle, newStyle: TextStyle;
                        //     redraw: BOOLEAN; hTE: TEHandle);
                        // Inside Macintosh Volume V, V-271..V-272.
                        // MPW C glue passes both TextStyle records by
                        // pointer (not by value), giving an 18-byte arg
                        // frame: selector(2) + hTE(4) + redraw(2) +
                        // newStyle ptr(4) + oldStyle ptr(4) + mode(2).
                        let te_handle = bus.read_long(sp + 2);
                        let _redraw = bus.read_byte(sp + 6) != 0;
                        let new_style_ptr = bus.read_long(sp + 8);
                        let old_style_ptr = bus.read_long(sp + 12);
                        let mode = bus.read_word(sp + 16);
                        if old_style_ptr != 0 && new_style_ptr != 0 {
                            let style_handle = Self::te_style_handle(bus, te_handle);
                            if style_handle != 0 {
                                let style_ptr_record = bus.read_long(style_handle);
                                if style_ptr_record != 0 {
                                    let table_handle = bus.read_long(
                                        style_ptr_record + Self::TE_STYLE_STYLE_TABLE_OFFSET,
                                    );
                                    let table_ptr = if table_handle != 0 {
                                        bus.read_long(table_handle)
                                    } else {
                                        0
                                    };
                                    if table_ptr != 0 {
                                        // Per IM:V V-270, replace only when the
                                        // existing style's selected attributes
                                        // match oldStyle exactly. With Systemless's
                                        // single-element style table this collapses
                                        // to one comparison.
                                        let mut matches_old = true;
                                        if (mode & 0x0001) != 0
                                            && bus
                                                .read_word(table_ptr + Self::ST_ELEMENT_FONT_OFFSET)
                                                != bus.read_word(old_style_ptr)
                                        {
                                            matches_old = false;
                                        }
                                        if (mode & 0x0002) != 0
                                            && bus
                                                .read_word(table_ptr + Self::ST_ELEMENT_FACE_OFFSET)
                                                != bus.read_word(old_style_ptr + 2)
                                        {
                                            matches_old = false;
                                        }
                                        if (mode & 0x0004) != 0
                                            && bus
                                                .read_word(table_ptr + Self::ST_ELEMENT_SIZE_OFFSET)
                                                != bus.read_word(old_style_ptr + 4)
                                        {
                                            matches_old = false;
                                        }
                                        if (mode & 0x0008) != 0
                                            && (bus.read_word(
                                                table_ptr + Self::ST_ELEMENT_COLOR_OFFSET,
                                            ) != bus.read_word(old_style_ptr + 6)
                                                || bus.read_word(
                                                    table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 2,
                                                ) != bus.read_word(old_style_ptr + 8)
                                                || bus.read_word(
                                                    table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 4,
                                                ) != bus.read_word(old_style_ptr + 10))
                                        {
                                            matches_old = false;
                                        }
                                        if matches_old {
                                            if (mode & 0x0001) != 0 {
                                                bus.write_word(
                                                    table_ptr + Self::ST_ELEMENT_FONT_OFFSET,
                                                    bus.read_word(new_style_ptr),
                                                );
                                            }
                                            if (mode & 0x0002) != 0 {
                                                bus.write_word(
                                                    table_ptr + Self::ST_ELEMENT_FACE_OFFSET,
                                                    bus.read_word(new_style_ptr + 2),
                                                );
                                            }
                                            if (mode & 0x0004) != 0 {
                                                bus.write_word(
                                                    table_ptr + Self::ST_ELEMENT_SIZE_OFFSET,
                                                    bus.read_word(new_style_ptr + 4),
                                                );
                                            }
                                            if (mode & 0x0008) != 0 {
                                                bus.write_word(
                                                    table_ptr + Self::ST_ELEMENT_COLOR_OFFSET,
                                                    bus.read_word(new_style_ptr + 6),
                                                );
                                                bus.write_word(
                                                    table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 2,
                                                    bus.read_word(new_style_ptr + 8),
                                                );
                                                bus.write_word(
                                                    table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 4,
                                                    bus.read_word(new_style_ptr + 10),
                                                );
                                            }
                                        }
                                    }
                                }
                            }
                        }
                        let _ = mode;
                        cpu.write_reg(Register::A7, sp + 18);
                    }
                    0x0003 => {
                        // TEGetStyle ($A83D, selector $0003)
                        // PROCEDURE TEGetStyle(sel: INTEGER; VAR attrs: TextStyle;
                        //     VAR lineHeight: INTEGER; VAR fontAscent: INTEGER; hTE: TEHandle);
                        let te_handle = bus.read_long(sp + 2);
                        let font_ascent_ptr = bus.read_long(sp + 6);
                        let line_height_ptr = bus.read_long(sp + 10);
                        let attrs_ptr = bus.read_long(sp + 14);
                        let _sel = bus.read_word(sp + 18) as i16;
                        let (font, face, size, color, line_height, font_ascent) =
                            self.te_primary_style(bus, te_handle);
                        if attrs_ptr != 0 {
                            bus.write_word(attrs_ptr, font as u16);
                            bus.write_word(attrs_ptr + 2, face as u16);
                            bus.write_word(attrs_ptr + 4, size as u16);
                            bus.write_word(attrs_ptr + 6, color.0);
                            bus.write_word(attrs_ptr + 8, color.1);
                            bus.write_word(attrs_ptr + 10, color.2);
                        }
                        if line_height_ptr != 0 {
                            bus.write_word(line_height_ptr, line_height as u16);
                        }
                        if font_ascent_ptr != 0 {
                            bus.write_word(font_ascent_ptr, font_ascent as u16);
                        }
                        cpu.write_reg(Register::A7, sp + 20);
                    }
                    0x0004 => {
                        // TEGetStyleHandle ($A83D, selector $0004)
                        // FUNCTION TEGetStyleHandle(hTE: TEHandle): TEStyleHandle;
                        let te_handle = bus.read_long(sp + 2);
                        bus.write_long(sp + 6, Self::te_style_handle(bus, te_handle));
                        cpu.write_reg(Register::A7, sp + 6);
                    }
                    0x0005 => {
                        // TESetStyleHandle ($A83D, selector $0005)
                        // PROCEDURE TESetStyleHandle(theHandle: TEStyleHandle; hTE: TEHandle);
                        let te_handle = bus.read_long(sp + 2);
                        let style_handle = bus.read_long(sp + 6);
                        Self::te_write_style_handle(bus, te_handle, style_handle);
                        cpu.write_reg(Register::A7, sp + 10);
                    }
                    0x0006 => {
                        // TEGetStyleScrapHandle ($A83D, selector $0006)
                        // FUNCTION TEGetStyleScrapHandle(hTE: TEHandle): STScrpHandle;
                        let te_handle = bus.read_long(sp + 2);
                        let style_handle = Self::te_style_handle(bus, te_handle);
                        let mut result = 0;
                        if style_handle != 0 {
                            let style_ptr = bus.read_long(style_handle);
                            if style_ptr != 0 {
                                let null_style_handle =
                                    bus.read_long(style_ptr + Self::TE_STYLE_NULL_STYLE_OFFSET);
                                if null_style_handle != 0 {
                                    let null_style_ptr = bus.read_long(null_style_handle);
                                    if null_style_ptr != 0 {
                                        let scrap_handle = bus.read_long(
                                            null_style_ptr + Self::NULL_STYLE_SCRAP_OFFSET,
                                        );
                                        result = Self::duplicate_handle_data(bus, scrap_handle);
                                    }
                                }
                            }
                        }
                        bus.write_long(sp + 6, result);
                        cpu.write_reg(Register::A7, sp + 6);
                    }
                    0x0007 => {
                        // TEStyleInsert ($A83D, selector $0007)
                        // PROCEDURE TEStyleInsert(text: Ptr; length: LONGINT; hST: StScrpHandle; hTE: TEHandle);
                        let te_handle = bus.read_long(sp + 2);
                        let style_scrap = bus.read_long(sp + 6);
                        let length = bus.read_long(sp + 10) as usize;
                        let text_ptr = bus.read_long(sp + 14);
                        if text_ptr != 0 && length != 0 {
                            let text = bus.read_bytes(text_ptr, length);
                            let insert_start = {
                                let te_ptr = Self::te_record_ptr(bus, te_handle);
                                let text_len = Self::te_text_length(bus, te_handle);
                                if te_ptr != 0 {
                                    let mut sel_start =
                                        bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize;
                                    let mut sel_end =
                                        bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize;
                                    sel_start = sel_start.min(text_len);
                                    sel_end = sel_end.min(text_len);
                                    if sel_end < sel_start {
                                        std::mem::swap(&mut sel_start, &mut sel_end);
                                    }
                                    sel_start
                                } else {
                                    0
                                }
                            };
                            let preview_len = text.len().min(64);
                            if trace_textedit_enabled() {
                                eprintln!(
                                    "[TE] TEStyleInsert hTE=${:08X} hST=${:08X} len={} text_ptr=${:08X} preview={:?}",
                                    te_handle,
                                    style_scrap,
                                    length,
                                    text_ptr,
                                    String::from_utf8_lossy(&text[..preview_len])
                                );
                            }
                            self.te_insert_text(bus, te_handle, &text);
                            if self.te_apply_style_scrap_to_range(
                                bus,
                                te_handle,
                                style_scrap,
                                insert_start,
                                text.len(),
                            ) {
                                self.te_recalculate_layout(bus, te_handle);
                            }
                            self.draw_te_contents(cpu, bus, te_handle);
                        }
                        cpu.write_reg(Register::A7, sp + 18);
                    }
                    0x0008 => {
                        // TEGetPoint ($A83D, selector $0008)
                        // Returns the point for a character offset within the edit record.
                        // FUNCTION TEGetPoint(offset: INTEGER; hTE: TEHandle): Point;
                        // Inside Macintosh Volume VI, 15-31
                        let te_handle = bus.read_long(sp + 2);
                        let offset = bus.read_word(sp + 6) as i16;
                        let te_ptr = Self::te_record_ptr(bus, te_handle);
                        let result_addr = sp + 8;
                        if te_ptr != 0 {
                            let text_len = Self::te_text_length(bus, te_handle);
                            let clamped = i32::from(offset).clamp(0, text_len as i32) as usize;
                            let line_index = Self::te_char_to_line_index(bus, te_handle, clamped);
                            let line_start = Self::te_line_starts(bus, te_handle)
                                .get(line_index)
                                .copied()
                                .unwrap_or(0);
                            let (top, x) = self.te_char_to_point(bus, te_handle, clamped);
                            let y = top.saturating_add(Self::te_ascent_for_line(
                                bus, te_handle, line_index,
                            ));
                            if trace_textedit_enabled() {
                                let starts = Self::te_line_starts(bus, te_handle);
                                let pc = cpu.read_reg(Register::PC);
                                eprintln!(
                                    "[TE] TEGetPoint hTE=${:08X} offset={} line={} start={} point_offset={} point=({}, {}) pc=${:08X} next=[{:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X}]",
                                    te_handle,
                                    clamped,
                                    line_index,
                                    line_start,
                                    clamped,
                                    y,
                                    x,
                                    pc,
                                    bus.read_word(pc),
                                    bus.read_word(pc + 2),
                                    bus.read_word(pc + 4),
                                    bus.read_word(pc + 6),
                                    bus.read_word(pc + 8),
                                    bus.read_word(pc + 10),
                                    bus.read_word(pc + 12),
                                    bus.read_word(pc + 14),
                                    bus.read_word(pc + 16),
                                    bus.read_word(pc + 18),
                                    bus.read_word(pc + 20),
                                    bus.read_word(pc + 22),
                                    bus.read_word(pc + 24),
                                    bus.read_word(pc + 26),
                                    bus.read_word(pc + 28),
                                    bus.read_word(pc + 30),
                                    bus.read_word(pc + 32),
                                    bus.read_word(pc + 34),
                                    bus.read_word(pc + 36),
                                    bus.read_word(pc + 38)
                                );
                                eprintln!(
                                    "[TE] TEGetPoint layout nLines={} teLength={} lineStarts={:?}",
                                    starts.len().saturating_sub(1),
                                    text_len,
                                    starts
                                );
                                eprintln!(
                                    "[TE] TEGetPoint helper@35614=[{:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X}]",
                                    bus.read_word(0x0003_5614),
                                    bus.read_word(0x0003_5616),
                                    bus.read_word(0x0003_5618),
                                    bus.read_word(0x0003_561A),
                                    bus.read_word(0x0003_561C),
                                    bus.read_word(0x0003_561E),
                                    bus.read_word(0x0003_5620),
                                    bus.read_word(0x0003_5622),
                                    bus.read_word(0x0003_5624),
                                    bus.read_word(0x0003_5626),
                                    bus.read_word(0x0003_5628),
                                    bus.read_word(0x0003_562A)
                                );
                                let table_base = cpu.read_reg(Register::A5).wrapping_sub(0x37B4);
                                let rect_table = bus.read_long(table_base);
                                let rect_ptr = rect_table.wrapping_add(21 * 8);
                                let a6 = cpu.read_reg(Register::A6);
                                let ret = bus.read_long(a6 + 4);
                                eprintln!(
                                    "[TE] TEGetPoint rect21 table=${:08X} rect_ptr=${:08X} rect=({},{},{},{}) a4=${:08X} d5={} d6={} d7={} a6=${:08X} ret=${:08X}",
                                    rect_table,
                                    rect_ptr,
                                    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,
                                    cpu.read_reg(Register::A4),
                                    cpu.read_reg(Register::D5) as i32,
                                    cpu.read_reg(Register::D6) as i32,
                                    cpu.read_reg(Register::D7) as i32,
                                    a6,
                                    ret
                                );
                                eprintln!(
                                    "[TE] TEGetPoint caller@{:08X}=[{:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X}]",
                                    ret,
                                    bus.read_word(ret),
                                    bus.read_word(ret + 2),
                                    bus.read_word(ret + 4),
                                    bus.read_word(ret + 6),
                                    bus.read_word(ret + 8),
                                    bus.read_word(ret + 10),
                                    bus.read_word(ret + 12),
                                    bus.read_word(ret + 14),
                                    bus.read_word(ret + 16),
                                    bus.read_word(ret + 18),
                                    bus.read_word(ret + 20),
                                    bus.read_word(ret + 22)
                                );
                                let caller_start = ret.wrapping_sub(0x20);
                                eprintln!(
                                    "[TE] TEGetPoint caller_pre@{:08X}=[{:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X}]",
                                    caller_start,
                                    bus.read_word(caller_start),
                                    bus.read_word(caller_start + 2),
                                    bus.read_word(caller_start + 4),
                                    bus.read_word(caller_start + 6),
                                    bus.read_word(caller_start + 8),
                                    bus.read_word(caller_start + 10),
                                    bus.read_word(caller_start + 12),
                                    bus.read_word(caller_start + 14),
                                    bus.read_word(caller_start + 16),
                                    bus.read_word(caller_start + 18),
                                    bus.read_word(caller_start + 20),
                                    bus.read_word(caller_start + 22),
                                    bus.read_word(caller_start + 24),
                                    bus.read_word(caller_start + 26),
                                    bus.read_word(caller_start + 28),
                                    bus.read_word(caller_start + 30)
                                );
                                eprintln!(
                                    "[TE] TEGetPoint caller_block@00035F7C=[{:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X}]",
                                    bus.read_word(0x0003_5F7C),
                                    bus.read_word(0x0003_5F7E),
                                    bus.read_word(0x0003_5F80),
                                    bus.read_word(0x0003_5F82),
                                    bus.read_word(0x0003_5F84),
                                    bus.read_word(0x0003_5F86),
                                    bus.read_word(0x0003_5F88),
                                    bus.read_word(0x0003_5F8A),
                                    bus.read_word(0x0003_5F8C),
                                    bus.read_word(0x0003_5F8E),
                                    bus.read_word(0x0003_5F90),
                                    bus.read_word(0x0003_5F92),
                                    bus.read_word(0x0003_5F94),
                                    bus.read_word(0x0003_5F96),
                                    bus.read_word(0x0003_5F98),
                                    bus.read_word(0x0003_5F9A),
                                    bus.read_word(0x0003_5F9C),
                                    bus.read_word(0x0003_5F9E),
                                    bus.read_word(0x0003_5FA0),
                                    bus.read_word(0x0003_5FA2),
                                    bus.read_word(0x0003_5FA4),
                                    bus.read_word(0x0003_5FA6),
                                    bus.read_word(0x0003_5FA8),
                                    bus.read_word(0x0003_5FAA),
                                    bus.read_word(0x0003_5FAC),
                                    bus.read_word(0x0003_5FAE),
                                    bus.read_word(0x0003_5FB0),
                                    bus.read_word(0x0003_5FB2),
                                    bus.read_word(0x0003_5FB4),
                                    bus.read_word(0x0003_5FB6),
                                    bus.read_word(0x0003_5FB8),
                                    bus.read_word(0x0003_5FBA)
                                );
                                eprintln!(
                                    "[TE] TEGetPoint branch_c6@00035FC6=[{:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X}] branch_e2@00035FE2=[{:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X}]",
                                    bus.read_word(0x0003_5FC6),
                                    bus.read_word(0x0003_5FC8),
                                    bus.read_word(0x0003_5FCA),
                                    bus.read_word(0x0003_5FCC),
                                    bus.read_word(0x0003_5FCE),
                                    bus.read_word(0x0003_5FD0),
                                    bus.read_word(0x0003_5FD2),
                                    bus.read_word(0x0003_5FD4),
                                    bus.read_word(0x0003_5FE2),
                                    bus.read_word(0x0003_5FE4),
                                    bus.read_word(0x0003_5FE6),
                                    bus.read_word(0x0003_5FE8),
                                    bus.read_word(0x0003_5FEA),
                                    bus.read_word(0x0003_5FEC),
                                    bus.read_word(0x0003_5FEE),
                                    bus.read_word(0x0003_5FF0)
                                );
                                eprintln!(
                                    "[TE] TEGetPoint helper_fn@00036B9A=[{:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X} {:04X}]",
                                    bus.read_word(0x0003_6B9A),
                                    bus.read_word(0x0003_6B9C),
                                    bus.read_word(0x0003_6B9E),
                                    bus.read_word(0x0003_6BA0),
                                    bus.read_word(0x0003_6BA2),
                                    bus.read_word(0x0003_6BA4),
                                    bus.read_word(0x0003_6BA6),
                                    bus.read_word(0x0003_6BA8),
                                    bus.read_word(0x0003_6BAA),
                                    bus.read_word(0x0003_6BAC),
                                    bus.read_word(0x0003_6BAE),
                                    bus.read_word(0x0003_6BB0),
                                    bus.read_word(0x0003_6BB2),
                                    bus.read_word(0x0003_6BB4),
                                    bus.read_word(0x0003_6BB6),
                                    bus.read_word(0x0003_6BB8),
                                    bus.read_word(0x0003_6BBA),
                                    bus.read_word(0x0003_6BBC),
                                    bus.read_word(0x0003_6BBE),
                                    bus.read_word(0x0003_6BC0),
                                    bus.read_word(0x0003_6BC2),
                                    bus.read_word(0x0003_6BC4),
                                    bus.read_word(0x0003_6BC6),
                                    bus.read_word(0x0003_6BC8)
                                );
                            }
                            bus.write_word(result_addr, y as u16);
                            bus.write_word(result_addr + 2, x as u16);
                        } else {
                            bus.write_word(result_addr, 0);
                            bus.write_word(result_addr + 2, 0);
                        }
                        cpu.write_reg(Register::A7, result_addr);
                    }
                    0x0009 => {
                        // TEGetHeight ($A83D, selector $0009)
                        // Returns the total height of the requested line range.
                        // FUNCTION TEGetHeight(endLine: LONGINT; startLine: LONGINT; hTE: TEHandle): LONGINT;
                        // Text 1993, 2-90
                        let te_handle = bus.read_long(sp + 2);
                        let mut start_line = bus.read_long(sp + 6) as i32;
                        let mut end_line = bus.read_long(sp + 10) as i32;
                        let te_ptr = Self::te_record_ptr(bus, te_handle);
                        let n_lines = if te_ptr != 0 {
                            bus.read_word(te_ptr + Self::TE_N_LINES_OFFSET) as i32
                        } else {
                            0
                        };
                        if start_line > 0 {
                            start_line -= 1;
                        } else {
                            start_line = 0;
                        }
                        end_line = end_line.min(n_lines);
                        if end_line < 0 {
                            end_line = 0;
                        } else if end_line > 0 {
                            end_line -= 1;
                        }
                        if start_line > end_line {
                            std::mem::swap(&mut start_line, &mut end_line);
                        }

                        let text_bytes = Self::te_text_bytes(bus, te_handle);
                        let line_starts = Self::te_line_starts(bus, te_handle);
                        if !text_bytes.is_empty() {
                            while end_line >= start_line {
                                let current = end_line as usize;
                                let Some(&line_start) = line_starts.get(current) else {
                                    break;
                                };
                                let line_end = line_starts
                                    .get(current + 1)
                                    .copied()
                                    .unwrap_or(text_bytes.len());
                                let blank_trailing_line = current + 1 == line_starts.len() - 1
                                    && line_start < line_end
                                    && text_bytes[line_start..line_end]
                                        .iter()
                                        .all(|&b| matches!(b, b'\r' | b'\n'));
                                if blank_trailing_line {
                                    end_line -= 1;
                                } else {
                                    break;
                                }
                            }
                        }

                        let mut total_height = 0i32;
                        if end_line >= start_line {
                            for current_line in
                                start_line.max(0) as usize..=end_line.max(0) as usize
                            {
                                total_height += i32::from(Self::te_height_for_line(
                                    bus,
                                    te_handle,
                                    current_line,
                                ));
                            }
                        }
                        if trace_textedit_enabled() {
                            eprintln!(
                                "[TE] TEGetHeight hTE=${:08X} start_line={} end_line={} result={}",
                                te_handle, start_line, end_line, total_height
                            );
                        }
                        bus.write_long(sp + 14, total_height as u32);
                        cpu.write_reg(Register::A7, sp + 14);
                    }
                    0x000A => {
                        // TEContinuousStyle ($A83D, selector $000A)
                        // Returns the common style across the current selection.
                        // FUNCTION TEContinuousStyle(VAR mode: INTEGER; VAR aStyle: TextStyle; hTE: TEHandle): BOOLEAN;
                        // Inside Macintosh Volume VI, 15-34 to 15-35
                        let te_handle = bus.read_long(sp + 2);
                        let style_ptr = bus.read_long(sp + 6);
                        let mode_ptr = bus.read_long(sp + 10);
                        let result_addr = sp + 14;
                        let requested_mode = if mode_ptr != 0 {
                            bus.read_word(mode_ptr)
                        } else {
                            0
                        };
                        if style_ptr != 0 {
                            let (tx_font, tx_face, tx_size, color, _, _) =
                                self.te_primary_style(bus, te_handle);
                            if (requested_mode & 0x0001) != 0 {
                                bus.write_word(style_ptr, tx_font as u16);
                            }
                            if (requested_mode & 0x0002) != 0 {
                                bus.write_word(style_ptr + 2, tx_face as u16);
                            }
                            if (requested_mode & 0x0004) != 0 {
                                bus.write_word(style_ptr + 4, tx_size as u16);
                            }
                            if (requested_mode & 0x0008) != 0 {
                                bus.write_word(style_ptr + 6, color.0);
                                bus.write_word(style_ptr + 8, color.1);
                                bus.write_word(style_ptr + 10, color.2);
                            }
                        }
                        bus.write_word(result_addr, 0xFFFF);
                        cpu.write_reg(Register::A7, result_addr);
                    }
                    0x000B => {
                        // TEUseStyleScrap ($A83D, selector $000B)
                        // Sets style data for the specified text range from a style scrap handle.
                        // PROCEDURE TEUseStyleScrap(rangeStart: LONGINT; rangeEnd: LONGINT;
                        //     newStyles: StScrpHandle; redraw: BOOLEAN; hTE: TEHandle);
                        // Inside Macintosh Volume VI, 15-35 to 15-36
                        let te_handle = bus.read_long(sp + 2);
                        // Pascal BOOLEAN in high byte (MPW C convention).
                        let redraw = bus.read_byte(sp + 6) != 0;
                        let new_styles = bus.read_long(sp + 8);
                        let range_end = bus.read_long(sp + 12) as usize;
                        let range_start = bus.read_long(sp + 16) as usize;
                        let (range_start, range_end) = if range_end < range_start {
                            (range_end, range_start)
                        } else {
                            (range_start, range_end)
                        };
                        if self.te_apply_style_scrap_to_range(
                            bus,
                            te_handle,
                            new_styles,
                            range_start,
                            range_end.saturating_sub(range_start),
                        ) {
                            self.te_recalculate_layout(bus, te_handle);
                            if redraw {
                                self.draw_te_contents(cpu, bus, te_handle);
                            }
                        }
                        cpu.write_reg(Register::A7, sp + 20);
                    }
                    0x000C => {
                        // TECustomHook ($A83D, selector $000C)
                        // Reads or replaces one of TextEdit's internal hook procedures.
                        // PROCEDURE TECustomHook(which: TEIntHook; VAR addr: ProcPtr; hTE: TEHandle);
                        // Inside Macintosh Volume VI, 15-25 to 15-26.
                        // Per IM:VI 15-26, addr is a VAR ProcPtr — on
                        // return it must hold the previous hook. Systemless
                        // does not invoke registered hooks (no guest-fn
                        // dispatch infrastructure), so we report "no
                        // previous hook" by writing 0 into *addr.
                        let addr_ptr = bus.read_long(sp + 6);
                        if addr_ptr != 0 {
                            bus.write_long(addr_ptr, 0);
                        }
                        cpu.write_reg(Register::A7, sp + 12);
                    }
                    0x000D => {
                        // TENumStyles ($A83D, selector $000D)
                        // Returns the number of style changes contained in the
                        // given range, counting one for the start of the range.
                        // FUNCTION TENumStyles(rangeStart: LONGINT; rangeEnd: LONGINT;
                        //                      hTE: TEHandle): LONGINT;
                        // Inside Macintosh Volume VI, 15-36.
                        // Per IM:VI 15-36, an unstyled record always
                        // returns 1. With Systemless's single-run styled
                        // record the in-range transitions count is also
                        // zero, so the +1 for the start-of-range gives 1.
                        let te_handle = bus.read_long(sp + 2);
                        let style_handle = Self::te_style_handle(bus, te_handle);
                        let mut count: u32 = 1;
                        if style_handle != 0 {
                            let style_ptr_record = bus.read_long(style_handle);
                            if style_ptr_record != 0 {
                                let n_runs = bus
                                    .read_word(style_ptr_record + Self::TE_STYLE_N_RUNS_OFFSET)
                                    as u32;
                                count = n_runs.max(1);
                            }
                        }
                        bus.write_long(sp + 14, count);
                        cpu.write_reg(Register::A7, sp + 14);
                    }
                    0x000E => {
                        // TEFeatureFlag ($A83D, selector $000E)
                        // Tests or changes a TextEdit feature bit and returns the previous state.
                        // FUNCTION TEFeatureFlag(feature: INTEGER; action: INTEGER; hTE: TEHandle): INTEGER;
                        // Inside Macintosh Volume VI, 15-22 to 15-24
                        let te_handle = bus.read_long(sp + 2);
                        let action = bus.read_word(sp + 6) as i16;
                        let feature = bus.read_word(sp + 8);
                        let was_set = self.te_feature_bit(te_handle, feature);
                        if matches!(
                            feature,
                            Self::TE_FEATURE_AUTO_SCROLL
                                | Self::TE_FEATURE_TEXT_BUFFERING
                                | Self::TE_FEATURE_OUTLINE_HILITE
                                | Self::TE_FEATURE_INLINE_INPUT
                                | Self::TE_FEATURE_USE_TEXT_SERVICES
                        ) {
                            match action {
                                Self::TE_BIT_CLEAR => {
                                    self.set_te_feature_bit(te_handle, feature, false)
                                }
                                Self::TE_BIT_SET => {
                                    self.set_te_feature_bit(te_handle, feature, true)
                                }
                                Self::TE_BIT_TEST => {}
                                _ => {}
                            }
                        }
                        bus.write_word(sp + 10, if was_set { 1 } else { 0 });
                        cpu.write_reg(Register::A7, sp + 10);
                    }
                    _ => return None,
                }
                Ok(())
            }

            // TEDispose ($A9CD)
            // Disposes of the edit record and releases memory used by
            // the text and record structures.
            // PROCEDURE TEDispose(hTE: TEHandle);
            // Inside Macintosh Volume I, I-383 to I-384; Text 1993, 2-79
            //
            // Regression coverage:
            //   dialog::tests::tedispose_releases_te_record_text_handle_and_pops_arg
            // TEDispose ($A9CD): Frees TERec, hText, and style handles per IM:I I-383..I-384
            (true, 0x1CD) => {
                let sp = cpu.read_reg(Register::A7);
                let te_handle = bus.read_long(sp);
                if te_handle != 0 {
                    let te_ptr = bus.read_long(te_handle);
                    if te_ptr != 0 {
                        // Free the text handle and its data
                        let h_text = bus.read_long(te_ptr + Self::TE_HTEXT_OFFSET);
                        if h_text != 0 {
                            let text_ptr = bus.read_long(h_text);
                            if text_ptr != 0 {
                                bus.free(text_ptr);
                            }
                            bus.free(h_text);
                        }
                        // For styled records, free the style handle
                        if Self::te_is_styled_record(bus, te_ptr) {
                            let style_handle = bus.read_long(te_ptr + Self::TE_TX_FONT_OFFSET);
                            if style_handle != 0 {
                                let style_ptr = bus.read_long(style_handle);
                                if style_ptr != 0 {
                                    bus.free(style_ptr);
                                }
                                bus.free(style_handle);
                            }
                        }
                        bus.free(te_ptr);
                    }
                    bus.free(te_handle);
                    self.textedit_states.remove(&te_handle);
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // TESetText ($A9CF)
            // PROCEDURE TESetText(text: Ptr; length: LONGINT; hTE: TEHandle);
            // Inside Macintosh Volume I (1985), p. I-378.
            //
            // IM:I I-378 verbatim: "TESetText sets the current text
            // contents of the edit record specified by hTE. The text
            // parameter points to the text, and the length parameter
            // contains the number of bytes."
            //
            // MPW Universal Headers `TextEdit.h` declares:
            //   EXTERN_API(void) TESetText(const void *text, long length,
            //                              TEHandle hTE) ONEWORDINLINE(0xA9CF);
            //
            // Pascal calling convention pushes args left-to-right (first
            // arg deepest). At trap entry the stack layout is therefore:
            //   sp+0  TEHandle hTE          (last pushed, shallowest)
            //   sp+4  LONGINT  length       (middle, 4 bytes)
            //   sp+8  Ptr      text         (first pushed, deepest)
            // The trap pops 12 bytes (no result slot — PROCEDURE).
            //
            // Empty-input semantics: per IM:I I-378 passing zero length
            // yields an empty edit record. Systemless additionally defends
            // against NIL text pointer (clears regardless of length);
            // the real Mac ROM (BasiliskII System 7.5.3) does NOT safely
            // handle NIL source pointers, so the catalog row's strict
            // bake witnesses only the zero-length branch of the
            // documented disjunction (see a9cf_tesettext_strict).
            //
            // Witnessed by:
            //   a9cf_tesettext_strict (BII bake +
            //     Systemless runtime)
            //   contract tests in this file:
            //     tesettext_copies_bytes_updates_length_and_pops_arguments
            //     tesettext_nil_or_zero_length_input_clears_text
            //     tesettext_replaces_prior_contents_via_sequential_call
            //     tesettext_balances_stackspace_with_pascal_protocol
            (true, 0x1CF) => {
                let sp = cpu.read_reg(Register::A7);
                let te_handle = bus.read_long(sp);
                let length = bus.read_long(sp + 4) as usize;
                let text_ptr = bus.read_long(sp + 8);
                if text_ptr != 0 && length != 0 {
                    let text = bus.read_bytes(text_ptr, length);
                    self.te_set_text_contents(bus, te_handle, &text);
                } else {
                    self.te_set_text_contents(bus, te_handle, &[]);
                }
                cpu.write_reg(Register::A7, sp + 12);
                Ok(())
            }

            // TEGetText ($A9CB)
            // FUNCTION TEGetText(hTE: TEHandle): CharsHandle;
            // Inside Macintosh Volume I (1985), p. I-384.
            //
            // IM:I I-384 verbatim: "TEGetText returns a handle to the text
            // of the specified edit record. The result is the same as the
            // handle in the hText field of the edit record, but has the
            // CharsHandle data type, which is defined as:
            //
            //   TYPE CharsHandle = ^CharsPtr;
            //        CharsPtr    = ^Chars;
            //        Chars       = PACKED ARRAY[0..32000] OF CHAR;
            //
            // You can get the length of the text from the teLength field
            // of the edit record."
            //
            // Pascal FUNCTION calling convention: the caller pre-allocates
            // a 4-byte CharsHandle result slot at SP+4, pushes a 4-byte
            // TEHandle argument at SP+0. The trap reads the TEHandle,
            // looks up TERec.hText (offset TE_HTEXT_OFFSET = +18 per
            // IM:I I-379), writes that handle to the result slot, and
            // pops the 4-byte argument. The C wrapper then pops the
            // 4-byte result slot — net externally-observed SP delta is
            // zero.
            //
            // MPW Universal Headers `TextEdit.h` declares:
            //   EXTERN_API(CharsHandle) TEGetText(TEHandle hTE)
            //       ONEWORDINLINE(0xA9CB);
            (true, 0x1CB) => {
                let sp = cpu.read_reg(Register::A7);
                let te_handle = bus.read_long(sp);
                bus.write_long(sp + 4, Self::te_text_handle(bus, te_handle));
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // TETextBox ($A9CE)
            //
            // PROCEDURE TETextBox(text: Ptr; length: LONGINT;
            //                     box: Rect; align: INTEGER);
            //
            // Inside Macintosh: Text (1993), p. 2-88; alignment
            // constants on p. 2-87.
            //
            // MPW Universal Headers TextEdit.h:
            //   EXTERN_API(void) TETextBox(const void *text,
            //                              long          length,
            //                              const Rect *  box,
            //                              short         just)
            //                                       ONEWORDINLINE(0xA9CE);
            //
            // IM:Text 1993 p. 2-88: "TETextBox erases the specified
            //   rectangle and then draws the text into it... TETextBox
            //   creates a transient edit record, draws the text wrapped
            //   to fit the rectangle, and disposes of the record."
            // IM:Text 1993 p. 2-87 alignment constants:
            //   teJustLeft  =  0  (flush left — system default)
            //   teJustCenter =  1  (centered)
            //   teJustRight  = -1  (flush right)
            //   teForceLeft  = -2  (force flush left for right-to-left)
            //
            // Calling convention (MPW C canonical, the only one MPW
            // emits because the Universal Headers declaration uses
            // `const Rect *`):
            //   sp+ 0  short        just     (last pushed, shallowest)
            //   sp+ 2  Rect *       box      (4-byte pointer)
            //   sp+ 6  long         length   (4 bytes)
            //   sp+10  const void * text     (4 bytes, deepest)
            //   pop = 14 bytes; no result slot (PROCEDURE).
            //
            // (The Pascal canonical signature inlines the 8-byte Rect
            // by value for an 18-byte frame, but MPW C never produces
            // that form. Systemless's HLE only supports the MPW C
            // canonical layout; the in-Rust contract tests and the
            // BasiliskII strict bake at
            // a9ce_tetextbox_strict/ both exercise
            // this layout exclusively.)
            //
            // Behavior witnessed by a9ce_tetextbox_strict
            // (5-band golden, 4 strict assertions; BII System 7.5.3 ROM
            // and Systemless HLE produce the same boolean predicates):
            //   1. Erases the destination box before drawing (probe a
            //      pre-blackened pixel far from any glyph is WHITE after
            //      the call) — IM:Text 1993 p. 2-88.
            //   2. Word-wraps when text exceeds rect width (40x60 box
            //      with "A B C D E F G" has black pixels at row 14+;
            //      800x16 box with same text has none) — IM:Text 1993
            //      p. 2-88 line layout behavior.
            //   3. Alignment parameter controls horizontal origin
            //      (teJustLeft / teJustCenter / teJustRight produce
            //      strictly increasing leftmost-black-pixel columns)
            //      — IM:Text 1993 p. 2-87.
            //   4. Pascal PROCEDURE pops 14 bytes, no result slot
            //      (StackSpace bookends equal).
            (true, 0x1CE) => {
                let sp = cpu.read_reg(Register::A7);
                let align = bus.read_word(sp) as i16;
                let box_ptr = bus.read_long(sp + 2);
                let box_top = bus.read_word(box_ptr) as i16;
                let box_left = bus.read_word(box_ptr + 2) as i16;
                let box_bottom = bus.read_word(box_ptr + 4) as i16;
                let box_right = bus.read_word(box_ptr + 6) as i16;
                let length = bus.read_long(sp + 6) as usize;
                let text_ptr = bus.read_long(sp + 10);
                cpu.write_reg(Register::A7, sp + 14);

                if trace_dialog_text_inline_enabled() {
                    let mut stack_bytes = [0u8; 24];
                    for (i, byte) in stack_bytes.iter_mut().enumerate() {
                        *byte = bus.read_byte(sp + i as u32);
                    }
                    let preview_len = length.min(64);
                    let preview = if text_ptr != 0 {
                        bus.read_bytes(text_ptr, preview_len)
                    } else {
                        Vec::new()
                    };
                    eprintln!(
                        "[DIALOG-TEXT] TETextBox params current_port=${:08X} sp=${:08X} stack={:02X?} text_ptr=${:08X} len={} box=({},{}..{},{} ) align={} preview={:02X?}",
                        self.current_port,
                        sp,
                        stack_bytes,
                        text_ptr,
                        length,
                        box_top,
                        box_left,
                        box_bottom,
                        box_right,
                        align,
                        preview,
                    );
                }

                if box_right > box_left && box_bottom > box_top {
                    // TETextBox creates a transient edit record and clears the
                    // destination box before drawing the wrapped text. The erase
                    // happens even when length is zero.
                    // Text 1993, 2-88; Executor textedit/teDisplay.cpp C_TETextBox
                    self.draw_rect(
                        cpu,
                        bus,
                        &Rect {
                            top: box_top,
                            left: box_left,
                            bottom: box_bottom,
                            right: box_right,
                        },
                        ShapeOp::Erase,
                    );

                    if text_ptr != 0 && length > 0 {
                        // Read the text bytes from guest memory
                        let text_bytes = bus.read_bytes(text_ptr, length);

                        if trace_dialog_text_inline_enabled() {
                            let preview_len = text_bytes.len().min(160);
                            let first_char =
                                text_bytes.first().copied().map(char::from).unwrap_or('\0');
                            let first_glyph = crate::quickdraw::text::get_glyph(
                                self.tx_font,
                                self.tx_size,
                                first_char,
                            )
                            .is_some();
                            eprintln!(
                            "[DIALOG-TEXT] TETextBox current_port=${:08X} box=({},{}..{},{} ) align={} len={} txFont={} txFace=${:04X} txMode={} txSize={} firstChar={:?} firstGlyph={} text=\"{}\"",
                            self.current_port,
                            box_top,
                            box_left,
                            box_bottom,
                            box_right,
                            align,
                            length,
                            self.tx_font,
                            self.tx_face,
                            self.tx_mode,
                            self.tx_size,
                            first_char,
                            first_glyph,
                            String::from_utf8_lossy(&text_bytes[..preview_len]),
                        );
                        }

                        // Capture font params to avoid borrowing self in closures
                        let font_id = self.tx_font;
                        let font_size = self.tx_size;
                        let advance_extra = self.advance_extra();
                        let missing_advance = self.missing_glyph_advance();

                        let metrics = crate::quickdraw::text::get_font_metrics(font_id, font_size);
                        let line_height = metrics.ascent + metrics.descent + metrics.leading.max(2);
                        let box_width = box_right - box_left;

                        // Measure a run of bytes (no &self borrow needed)
                        let measure = |start: usize, end: usize| -> i16 {
                            let mut w = 0i16;
                            for &b in &text_bytes[start..end] {
                                let ch = b as char;
                                if let Some((g, _)) =
                                    crate::quickdraw::text::get_glyph(font_id, font_size, ch)
                                {
                                    w += g.advance as i16 + advance_extra;
                                } else {
                                    w += missing_advance;
                                }
                            }
                            w
                        };

                        // Word-wrap: split into lines that fit within box_width.
                        // Break on spaces; if a single word exceeds box_width, break mid-word.
                        let mut lines: Vec<(usize, usize)> = Vec::new();
                        let mut line_start = 0usize;
                        let mut last_break = 0usize;
                        let mut line_width = 0i16;

                        for (i, &b) in text_bytes.iter().enumerate() {
                            if b == b'\r' || b == b'\n' {
                                lines.push((line_start, i));
                                line_start = i + 1;
                                last_break = line_start;
                                line_width = 0;
                                continue;
                            }
                            let ch = b as char;
                            let char_w = if let Some((g, _)) =
                                crate::quickdraw::text::get_glyph(font_id, font_size, ch)
                            {
                                g.advance as i16 + advance_extra
                            } else {
                                missing_advance
                            };
                            line_width += char_w;
                            if b == b' ' {
                                last_break = i + 1;
                            }
                            if line_width > box_width && i > line_start {
                                if last_break > line_start {
                                    lines.push((line_start, last_break - 1));
                                    line_start = last_break;
                                } else {
                                    lines.push((line_start, i));
                                    line_start = i;
                                }
                                last_break = line_start;
                                line_width = measure(line_start, i + 1);
                            }
                        }
                        if line_start < text_bytes.len() {
                            lines.push((line_start, text_bytes.len()));
                        }

                        // Draw each line
                        let mut y = box_top + metrics.ascent;
                        for (start, end) in &lines {
                            if y + metrics.descent > box_bottom {
                                break;
                            }
                            let mut trimmed_end = *end;
                            while trimmed_end > *start && text_bytes[trimmed_end - 1] == b' ' {
                                trimmed_end -= 1;
                            }
                            let lw = measure(*start, trimmed_end);
                            // Per Inside Macintosh: Text 1993, lines 7320-7323:
                            //   teJustLeft   =  0 (flush left — system default)
                            //   teJustCenter =  1 (centered)
                            //   teJustRight  = -1 (flush right)
                            //   teForceLeft  = -2 (force flush left)
                            let x = Self::te_line_origin_x(align, box_left, box_right, lw);
                            self.pn_loc = (y, x);
                            for &byte in &text_bytes[*start..trimmed_end] {
                                self.draw_char(cpu, bus, byte as char);
                            }
                            y += line_height;
                        }
                    }
                }
                self.refresh_visible_dialog_snapshot_for_port(bus, self.current_port);
                Ok(())
            }

            // TECalText ($A9D0)
            // PROCEDURE TECalText(hTE: TEHandle);
            // TECalText ($A9D0): Syncs TE_LENGTH and recalculates TE line layout per IM:I I-390
            (true, 0x1D0) => {
                let sp = cpu.read_reg(Register::A7);
                let te_handle = bus.read_long(sp);
                let te_ptr = Self::te_record_ptr(bus, te_handle);
                if te_ptr != 0 {
                    let text_len =
                        Self::te_text_length(bus, te_handle).min(u16::MAX as usize) as u16;
                    bus.write_word(te_ptr + Self::TE_LENGTH_OFFSET, text_len);
                    self.te_recalculate_layout(bus, te_handle);
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // TESetSelect ($A9D1)
            // PROCEDURE TESetSelect(selStart: LONGINT; selEnd: LONGINT; hTE: TEHandle);
            // Inside Macintosh Volume I, I-385
            // TESetSelect ($A9D1): Clamps selEnd to teLength per IM:I I-385; selStart/selEnd range 0..32767 stored as u16
            (true, 0x1D1) => {
                let sp = cpu.read_reg(Register::A7);
                let te_handle = bus.read_long(sp);
                let te_ptr = Self::te_record_ptr(bus, te_handle);
                if te_ptr != 0 {
                    let te_length = u32::from(bus.read_word(te_ptr + Self::TE_LENGTH_OFFSET));
                    // IM:I I-385: "SelEnd and selStart can range from 0 to 32767.
                    // If selEnd is anywhere beyond the last character of the text,
                    // the position just past the last character is used."
                    let sel_end = bus
                        .read_long(sp + 4)
                        .min(u32::from(u16::MAX))
                        .min(te_length);
                    let sel_start = bus.read_long(sp + 8).min(u32::from(u16::MAX));
                    bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, sel_start as u16);
                    bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, sel_end as u16);
                }
                cpu.write_reg(Register::A7, sp + 12);
                Ok(())
            }

            // TEUpdate ($A9D3)
            // PROCEDURE TEUpdate(rUpdate: Rect; hTE: TEHandle);
            // Inside Macintosh Volume I (1985), p. I-387.
            // Inside Macintosh: Text (1993), p. 2-88.
            // MPW Universal Headers TextEdit.h:
            //   EXTERN_API(void) TEUpdate(const Rect *rUpdate, TEHandle hTE)
            //     ONEWORDINLINE(0xA9D3);
            //
            // IM:I I-387 quote: "TEUpdate draws the text specified by hTE
            //   within the rectangle rUpdate of the destination rectangle
            //   for the edit record specified by hTE. You normally call
            //   this procedure in response to an update event for the
            //   window."
            // IM:Text 1993 p. 2-88 quote: "TEUpdate redraws the text
            //   specified by the edit record within the update rectangle."
            //
            // TEUpdate has two stack layouts that real Mac ROMs (System
            // 6.0+ onward) auto-tolerate:
            //
            //   * Pascal canonical (per IM:I I-387 PROCEDURE signature):
            //       sp+0   hTE       (4 bytes — last pushed, Pascal LR
            //                         order: last arg = shallowest)
            //       sp+4   rUpdate   (8 bytes — inlined Rect, deepest)
            //       Net pop = 12 bytes; no result slot (PROCEDURE).
            //
            //   * MPW C canonical (Universal Headers TextEdit.h
            //     declaration with `const Rect *`):
            //       sp+0   hTE          (4 bytes)
            //       sp+4   rUpdatePtr   (4 bytes — pointer to Rect)
            //       Net pop = 8 bytes; no result slot.
            //
            // The two forms agree on the contract: TEUpdate is a redraw
            // operation that does NOT mutate the underlying edit-record
            // state (hText, teLength, selStart, selEnd) — it only paints
            // pixels into the TE's inPort within the rUpdate region.
            //
            // Witnessed by `a9d3_teupdate_strict` (BasiliskII System 7.5.3
            // ROM, three engines-agree assertions on first deterministic
            // bake): C-form stack discipline, Pascal-form stack discipline
            // (via inline `pascal void kx_TEUpdate_Pascal(Rect, TEHandle)
            // = { 0xA9D3 }` thunk), and TE record-state preservation
            // across the two calls. Systemless HLE passes the catalog_test
            // on first try with no source change required — the heuristic
            // below correctly identifies both call shapes.
            //
            // Contract coverage:
            //   dialog::tests::teupdate_pascal_form_redraws_text_and_pops_inline_rect_and_tehandle
            //   dialog::tests::teupdate_c_rect_pointer_form_is_accepted_and_pops_eight_bytes
            //   dialog::tests::teupdate_preserves_te_record_state_across_redraw
            (true, 0x1D3) => {
                let sp = cpu.read_reg(Register::A7);
                // Both Pascal and C forms put hTE at SP+0 (last argument
                // pushed in Pascal left-to-right convention). The forms
                // differ at SP+4:
                //   * C form (8-byte total push): SP+4..7 is `rUpdatePtr`,
                //     a 4-byte pointer to a Rect on the heap or A5 globals.
                //   * Pascal form (12-byte total push): SP+4..11 is the
                //     8-byte Rect inlined directly on the stack.
                //
                // The earlier heuristic (`te_record_ptr(first_long) != 0`)
                // ALWAYS picked Pascal because first_long is the same handle
                // in both forms. POD MARS Master uses the C form; the wrong
                // pop-size leaked 4 bytes per TEUpdate call, which compounded
                // into 8 bytes per update event (two TEUpdates) and within
                // ~50 update cycles drifted A6/A7 enough to corrupt the
                // EventRecord pointer the next WaitNextEvent passed in,
                // starving the click hit-test. Per d4a8444b / 172f262a.
                //
                // The reliable test is: does the longword at SP+4 deref to
                // a plausible Rect (top<=bottom, left<=right, both in
                // -32000..32000)? If yes, C form. If not, fall back to
                // Pascal form. Fallback covers the rare case where SP+4..11
                // genuinely contains an inlined rect.
                //
                // Two extra guards beyond the bounds check:
                //   * `ptr_candidate` must be word-aligned. A Pascal-form
                //     small-coord rect like `(top=1, left=1, …)` produces
                //     `(top<<16)|left = 0x0001_0001`, which is odd and
                //     therefore NOT a valid 68k pointer (heap and globals
                //     are always even-aligned).
                //   * The dereffed Rect must have at least one non-zero
                //     word. A Pascal form with small even coords like
                //     `(top=10, left=10, bottom=40, right=200)` produces
                //     ptr_candidate `0x000A_000A`, which derefs into
                //     typically-zero low-memory or uninitialized heap —
                //     an all-zero "rect" passes the bounds test but isn't
                //     a meaningful update region a real C-form caller
                //     would pass.
                let hte_at_sp = bus.read_long(sp);
                let ptr_candidate = bus.read_long(sp + 4);
                let looks_like_c_form = ptr_candidate != 0
                    && (ptr_candidate & 1) == 0
                    && (0x0000_0010..0x0400_0000).contains(&ptr_candidate)
                    && {
                        let top = bus.read_word(ptr_candidate) as i16;
                        let left = bus.read_word(ptr_candidate + 2) as i16;
                        let bottom = bus.read_word(ptr_candidate + 4) as i16;
                        let right = bus.read_word(ptr_candidate + 6) as i16;
                        top <= bottom
                            && left <= right
                            && (-32000..=32000).contains(&top)
                            && (-32000..=32000).contains(&left)
                            && (-32000..=32000).contains(&bottom)
                            && (-32000..=32000).contains(&right)
                            && !(top == 0 && left == 0 && bottom == 0 && right == 0)
                    };
                let (te_handle, stack_pop) = if looks_like_c_form {
                    (hte_at_sp, 8)
                } else {
                    (hte_at_sp, 12)
                };
                let te_ptr = Self::te_record_ptr(bus, te_handle);
                if te_ptr != 0 {
                    if trace_textedit_enabled() {
                        let dest_rect = Self::te_read_rect(bus, te_ptr + Self::TE_DEST_RECT_OFFSET);
                        let view_rect = Self::te_read_rect(bus, te_ptr + Self::TE_VIEW_RECT_OFFSET);
                        eprintln!(
                            "[TE] TEUpdate hTE=${:08X} dest=({},{},{},{}) view=({},{},{},{})",
                            te_handle,
                            dest_rect.0,
                            dest_rect.1,
                            dest_rect.2,
                            dest_rect.3,
                            view_rect.0,
                            view_rect.1,
                            view_rect.2,
                            view_rect.3
                        );
                    }
                    self.draw_te_contents(cpu, bus, te_handle);
                }
                cpu.write_reg(Register::A7, sp + stack_pop);
                Ok(())
            }

            // TEClick ($A9D4)
            // PROCEDURE TEClick(pt: Point; extend: BOOLEAN; hTE: TEHandle);
            // Inside Macintosh Volume I, I-376; Inside Macintosh: Text 1993, 2-85
            //
            // MPW Universal Headers TextEdit.h:
            //   EXTERN_API(void) TEClick(Point pt, Boolean fExtend, TEHandle hTE)
            //       ONEWORDINLINE(0xA9D4);
            //
            // IM:I I-376 verbatim: "TEClick controls the placement and
            // highlighting of the selection range as determined by mouse
            // events. ... If the mouse button is down outside of the
            // selection range or if the Shift key was not down when the
            // mouse button was pressed (depending on the value of
            // extend), the selection range is set to an insertion point
            // at the offset corresponding to the position of the mouse."
            //
            // Pascal stack frame (args push left-to-right, first
            // source arg deepest):
            //   sp+0  hTE: TEHandle           (4) — last arg, shallowest
            //   sp+4  extend: BOOLEAN         (2) — middle arg (value
            //                                       in high byte per
            //                                       MPW C convention)
            //   sp+6  pt.v: INTEGER           (2) — first arg, second word
            //   sp+8  pt.h: INTEGER           (2) — first arg, first word
            // Total pop = 10 bytes; no function-result slot.
            //
            // HLE compromise: Systemless doesn't model the interactive
            // drag-to-select mouse loop that real-Mac TEClick runs.
            // Per IM:I I-376 a real TEClick would: (a) hit-test the
            // click point against the TE's destRect, (b) compute
            // the byte offset in the text corresponding to the
            // click position, (c) extend or replace the selection
            // range based on the `extend` flag, (d) loop polling
            // the mouse for drag-to-select until WaitMouseUp. Steps
            // (a)..(c) require a working caret-from-pixel mapping
            // that depends on the current font + size + measured
            // line widths; step (d) requires interactive mouse
            // synthesis from the host event source. Neither is
            // wired up in the current scripted event
            // model. So the TE record's selStart/selEnd are NOT
            // updated to reflect the click — apps that depend on
            // click-to-position-cursor will not see the cursor
            // move; apps that programmatically set selStart/selEnd
            // via TESetSelect ($A9D1, already Complete) work as
            // expected.
            //
            // BasiliskII-vs-Systemless divergence: BII System 7.5.3 ROM
            // DOES update selStart/selEnd on a TEClick call (collapsing
            // to the byte offset corresponding to the click position
            // per IM:I I-376) because it has the real pixel-to-character
            // mapping. Systemless leaves them unchanged. The strict bake
            // `a9d4_teclick_strict` therefore witnesses only the
            // engines-agree intersection (active flag + teLength
            // preservation + Pascal PROCEDURE stack discipline), and
            // the Apple-canonical "extend=FALSE collapses selection
            // to click offset" rule is tracked as a contract-only
            // assertion in the catalog row (not part of the golden
            // assertion set).
            //
            // Both engines DO agree that TEClick must not mutate the
            // active flag (owned by TEActivate/TEDeactivate per
            // IM:I I-385) or teLength (owned by
            // TESetText/TEKey/TEDelete/TEInsert).
            //
            // NIL hTE is a defensive no-op (matches the real-Mac
            // safety contract — TEClick on NIL is undefined but
            // shouldn't crash the host).
            //
            // Regression coverage:
            //   dialog::tests::teclick_consumes_point_extend_and_tehandle_arguments
            //   dialog::tests::teclick_empty_text_keeps_insertion_point_at_zero
            //   dialog::tests::teclick_preserves_terec_active_flag_and_telength
            //   dialog::tests::teclick_repeated_calls_balance_stack_no_drift
            (true, 0x1D4) => {
                let sp = cpu.read_reg(Register::A7);
                if trace_textedit_enabled() {
                    let te_handle = bus.read_long(sp);
                    // Pascal BOOLEAN in high byte (MPW C convention).
                    let extend = bus.read_byte(sp + 4) != 0;
                    let pt_v = bus.read_word(sp + 6) as i16;
                    let pt_h = bus.read_word(sp + 8) as i16;
                    eprintln!(
                        "[TE] TEClick hTE=${:08X} extend={} pt=({}, {})",
                        te_handle, extend, pt_v, pt_h
                    );
                }
                cpu.write_reg(Register::A7, sp + 10);
                Ok(())
            }

            // TECopy ($A9D5)
            // PROCEDURE TECopy(hTE: TEHandle);
            // Copies the currently-selected bytes into the TextEdit
            // scrap (TEScrpLength low-mem global at $0AB0 (word) /
            // TEScrpHandle at $0AB4 (long)). Empty selection leaves
            // the scrap untouched per IM:I I-386.
            // Inside Macintosh Volume I, I-386
            // TECopy ($A9D5): Writes selected bytes into TextEdit scrap low-mem globals (TEScrpLength $0AB0, TEScrpHandle $0AB4) per IM:I I-386. Empty selection leaves scrap untouched.
            (true, 0x1D5) => {
                let sp = cpu.read_reg(Register::A7);
                let te_handle = bus.read_long(sp);
                let te_ptr = Self::te_record_ptr(bus, te_handle);
                if te_ptr != 0 {
                    let sel_start = bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize;
                    let sel_end = bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize;
                    let (s, e) = if sel_start <= sel_end {
                        (sel_start, sel_end)
                    } else {
                        (sel_end, sel_start)
                    };
                    if s != e {
                        use crate::memory::globals::addr;
                        let existing = Self::te_text_bytes(bus, te_handle);
                        let s = s.min(existing.len());
                        let e = e.min(existing.len());
                        let selected = &existing[s..e];
                        let mut scrap_handle = bus.read_long(addr::TE_SCRP_HANDLE);
                        if scrap_handle == 0 {
                            scrap_handle = Self::allocate_handle_with_data(bus, 0);
                            bus.write_long(addr::TE_SCRP_HANDLE, scrap_handle);
                        }
                        let text_ptr =
                            Self::ensure_text_handle_size(bus, scrap_handle, selected.len());
                        if text_ptr != 0 && !selected.is_empty() {
                            bus.write_bytes(text_ptr, selected);
                        }
                        bus.write_word(
                            addr::TE_SCRP_LENGTH,
                            selected.len().min(u16::MAX as usize) as u16,
                        );
                    }
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // TECut ($A9D6)
            // PROCEDURE TECut(hTE: TEHandle);
            // Removes the currently-selected text from the edit record
            // and places a copy in the TextEdit scrap (TEScrpLength at
            // $0AB0 / TEScrpHandle at $0AB4). Any previous scrap
            // contents are replaced. Insertion-point selection is a
            // no-op per IM:I I-391.
            // TECut ($A9D6): Removes selected text from TERec and writes a copy into the TextEdit scrap low-mem globals per IM:I I-391. Empty selection is a no-op.
            (true, 0x1D6) => {
                let sp = cpu.read_reg(Register::A7);
                let te_handle = bus.read_long(sp);
                let te_ptr = Self::te_record_ptr(bus, te_handle);
                if te_ptr != 0 {
                    let sel_start = bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize;
                    let sel_end = bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize;
                    let (s, e) = if sel_start <= sel_end {
                        (sel_start, sel_end)
                    } else {
                        (sel_end, sel_start)
                    };
                    if s != e {
                        let existing = Self::te_text_bytes(bus, te_handle);
                        let s = s.min(existing.len());
                        let e = e.min(existing.len());
                        // Copy selection into the scrap (replace, not
                        // append — IM:I I-391).
                        {
                            use crate::memory::globals::addr;
                            let selected = &existing[s..e];
                            let mut scrap_handle = bus.read_long(addr::TE_SCRP_HANDLE);
                            if scrap_handle == 0 {
                                scrap_handle = Self::allocate_handle_with_data(bus, 0);
                                bus.write_long(addr::TE_SCRP_HANDLE, scrap_handle);
                            }
                            let text_ptr =
                                Self::ensure_text_handle_size(bus, scrap_handle, selected.len());
                            if text_ptr != 0 && !selected.is_empty() {
                                bus.write_bytes(text_ptr, selected);
                            }
                            bus.write_word(
                                addr::TE_SCRP_LENGTH,
                                selected.len().min(u16::MAX as usize) as u16,
                            );
                        }
                        // Delete selection from the TE text.
                        let mut merged = Vec::with_capacity(existing.len() - (e - s));
                        merged.extend_from_slice(&existing[..s]);
                        merged.extend_from_slice(&existing[e..]);
                        self.te_set_text_contents(bus, te_handle, &merged);
                        let new_pos = s.min(u16::MAX as usize) as u16;
                        bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, new_pos);
                        bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, new_pos);
                    }
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // TEDelete ($A9D7)
            // Removes the currently selected text from the edit record and
            // redraws. If selStart == selEnd (insertion point), does nothing.
            // Does NOT transfer text to the scrap (unlike TECut).
            // PROCEDURE TEDelete(hTE: TEHandle);
            // Inside Macintosh Volume I, I-387; Text 1993, 2-93
            //
            // Regression coverage:
            //   dialog::tests::tedelete_removes_selection_without_touching_scrap_and_pops_arg
            //   dialog::tests::tedelete_insertion_point_selection_is_noop_and_pops_arg
            // TEDelete ($A9D7): Removes selected text, collapses selection; no-op at insertion point. IM:I I-387
            (true, 0x1D7) => {
                let sp = cpu.read_reg(Register::A7);
                let te_handle = bus.read_long(sp);
                if trace_textedit_enabled() {
                    eprintln!("[TE] TEDelete hTE=${:08X}", te_handle);
                }
                let te_ptr = Self::te_record_ptr(bus, te_handle);
                if te_ptr != 0 {
                    let sel_start = bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize;
                    let sel_end = bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize;
                    if sel_start != sel_end {
                        let existing = Self::te_text_bytes(bus, te_handle);
                        let s = sel_start.min(existing.len());
                        let e = sel_end.min(existing.len());
                        let (s, e) = if s > e { (e, s) } else { (s, e) };
                        let mut merged = Vec::with_capacity(existing.len() - (e - s));
                        merged.extend_from_slice(&existing[..s]);
                        merged.extend_from_slice(&existing[e..]);
                        self.te_set_text_contents(bus, te_handle, &merged);
                        let new_pos = s.min(u16::MAX as usize) as u16;
                        bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, new_pos);
                        bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, new_pos);
                    }
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // TEActivate ($A9D8)
            // Activates the edit record, highlighting the selection range or
            // displaying a blinking caret at the insertion point.
            // PROCEDURE TEActivate(hTE: TEHandle);
            // Inside Macintosh Volume I, I-385; Text 1993, 2-80
            //
            // Regression coverage:
            //   teactivate_sets_active_flag
            //   teactivate_pops_four_bytes
            //   teactivate_preserves_selection
            //   teactivate_and_tedeactivate_repaint_empty_insertion_caret
            // TEActivate ($A9D8): Sets TERec.active flag per IM:I I-385
            (true, 0x1D8) => {
                let sp = cpu.read_reg(Register::A7);
                let te_handle = bus.read_long(sp);
                if trace_textedit_enabled() {
                    eprintln!("[TE] TEActivate hTE=${:08X}", te_handle);
                }
                let te_ptr = Self::te_record_ptr(bus, te_handle);
                if te_ptr != 0 {
                    bus.write_word(te_ptr + Self::TE_ACTIVE_OFFSET, 1);
                    bus.write_long(te_ptr + Self::TE_CARET_TIME_OFFSET, self.tick_count);
                    bus.write_word(te_ptr + Self::TE_CARET_STATE_OFFSET, 0);
                    self.draw_te_contents(cpu, bus, te_handle);
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // TEDeactivate ($A9D9)
            // Deactivates the edit record, unhighlighting the selection range
            // or removing the caret. Does not affect selStart/selEnd.
            // PROCEDURE TEDeactivate(hTE: TEHandle);
            // Inside Macintosh Volume I, I-385; Text 1993, 2-80
            //
            // Regression coverage:
            //   tedeactivate_clears_active_flag
            //   tedeactivate_pops_four_bytes
            //   tedeactivate_preserves_selection
            //   teactivate_and_tedeactivate_repaint_empty_insertion_caret
            // TEDeactivate ($A9D9): Clears TERec.active flag per IM:I I-385
            (true, 0x1D9) => {
                let sp = cpu.read_reg(Register::A7);
                let te_handle = bus.read_long(sp);
                if trace_textedit_enabled() {
                    eprintln!("[TE] TEDeactivate hTE=${:08X}", te_handle);
                }
                let te_ptr = Self::te_record_ptr(bus, te_handle);
                if te_ptr != 0 {
                    bus.write_word(te_ptr + Self::TE_ACTIVE_OFFSET, 0);
                    self.draw_te_contents(cpu, bus, te_handle);
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // TEIdle ($A9DA)
            // PROCEDURE TEIdle(hTE: TEHandle);
            // Inside Macintosh Volume I (1985), p. I-374; Inside
            //   Macintosh: Text (1993), p. 2-84.
            // MPW Universal Headers TextEdit.h:
            //   EXTERN_API(void) TEIdle(TEHandle hTE) ONEWORDINLINE(0xA9DA);
            //
            // Pascal stack frame:
            //   sp+0  hTE: TEHandle  (4)
            // Total pop = 4 bytes; no function-result slot. Net
            // externally-observed SP delta is zero (StackSpace
            // bookends are equal across one call).
            //
            // IM:I I-374 quote: "Call TEIdle from your event loop...
            // TEIdle causes the caret of the edit record to blink at
            // the rate specified in caretTime."
            //
            // IM:Text 1993 p. 2-84 quote: "TEIdle blinks the caret if
            // the destination rectangle contains the caret position;
            // if the specified edit record is inactive (such as when
            // the window is inactive), TEIdle has no effect."
            //
            // HLE model: TextEdit's TERec layout includes the private
            // caretTime and caretState fields before just/teLength. Systemless
            // treats caretTime as the last toggle tick and caretState == 0 as
            // visible. On an active insertion point, TEIdle waits the
            // documented initial 32-tick interval, toggles caretState, and
            // redraws the edit record. Non-insertion selections do not blink.
            //
            // Crucial no-mutation contract (witnessed by both
            // engines): TEIdle MUST NOT mutate TERec.active,
            // TERec.selStart, TERec.selEnd, TERec.teLength, or any
            // other selection / text fields. Selection updates are
            // the exclusive responsibility of TESetSelect /
            // TEClick / TEKey / TEDelete; activation toggles are
            // handled by TEActivate / TEDeactivate.
            //
            // NIL hTE is a defensive no-op.
            //
            // Witnessed by `a9da_teidle_strict` strict bake (3/3 BII
            // golden assertions): single-call StackSpace balance,
            // four-field TERec preservation across one call, and
            // 8-call composition StackSpace balance.
            //
            // Regression coverage:
            //   dialog::tests::teidle_consumes_tehandle_argument
            //   dialog::tests::teidle_toggles_visible_insertion_caret_after_blink_interval
            //   dialog::tests::teidle_preserves_active_flag_and_selection_offsets
            //   dialog::tests::teidle_repeated_calls_balance_stack_and_preserve_terec_state
            (true, 0x1DA) => {
                let sp = cpu.read_reg(Register::A7);
                let te_handle = bus.read_long(sp);
                self.textedit_idle(cpu, bus, te_handle);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // TEPaste ($A9DB)
            // PROCEDURE TEPaste(hTE: TEHandle);
            // Replaces the current selection with the contents of the
            // TextEdit scrap (TEScrpLength at $0AB0 word, TEScrpHandle
            // at $0AB4 long). Empty scrap leaves the TERec unchanged
            // per IM:I I-387.
            // Inside Macintosh Volume I, I-387
            // TEPaste ($A9DB): Replaces current selection with TextEdit scrap contents (TEScrpLength / TEScrpHandle low-mem globals) per IM:I I-387. Empty scrap is a no-op.
            (true, 0x1DB) => {
                let sp = cpu.read_reg(Register::A7);
                let te_handle = bus.read_long(sp);
                if trace_textedit_enabled() {
                    eprintln!("[TE] TEPaste hTE=${:08X}", te_handle);
                }
                use crate::memory::globals::addr;
                let scrap_len = bus.read_word(addr::TE_SCRP_LENGTH) as usize;
                let scrap_handle = bus.read_long(addr::TE_SCRP_HANDLE);
                if scrap_handle != 0 && scrap_len > 0 {
                    let scrap_ptr = bus.read_long(scrap_handle);
                    if scrap_ptr != 0 {
                        let scrap = bus.read_bytes(scrap_ptr, scrap_len);
                        self.te_insert_text(bus, te_handle, &scrap);
                    }
                }
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // TEKey ($A9DC)
            // Replaces the selection range with the input character and positions
            // the insertion point just past it. Backspace ($08) deletes the
            // selection or the character before the insertion point.
            // PROCEDURE TEKey(key: CHAR; hTE: TEHandle);
            // Inside Macintosh Volume I, I-385; Text 1993, 2-81
            //
            // Regression coverage:
            //   tekey_inserts_printable_character
            //   tekey_replaces_selection_with_character
            //   tekey_backspace_deletes_before_insertion
            //   tekey_backspace_deletes_selection
            //   tekey_backspace_at_start_is_noop
            //   tekey_pops_six_bytes
            //   tekey_with_nil_hte_is_safe_noop
            //   tekey_nil_master_ptr_is_safe_noop
            //   tekey_does_not_mutate_other_te_record
            //   tekey_insertion_at_start_mid_end_parametric
            //   tekey_does_not_mutate_other_registers_or_caller_stack
            // TEKey ($A9DC): Inserts character at insertion point, replaces selection; backspace deletes. IM:I I-385
            (true, 0x1DC) => {
                let sp = cpu.read_reg(Register::A7);
                let te_handle = bus.read_long(sp);
                let key = bus.read_word(sp + 4) as u8;
                if trace_textedit_enabled() {
                    eprintln!(
                        "[TE] TEKey hTE=${:08X} key=${:02X} {:?}",
                        te_handle,
                        key,
                        char::from(key).escape_default().to_string()
                    );
                }
                let te_ptr = Self::te_record_ptr(bus, te_handle);
                if te_ptr != 0 {
                    let sel_start = bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET) as usize;
                    let sel_end = bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize;
                    let existing = Self::te_text_bytes(bus, te_handle);
                    let text_len = existing.len();
                    let s = sel_start.min(text_len);
                    let e = sel_end.min(text_len);
                    let (s, e) = if s > e { (e, s) } else { (s, e) };

                    if key == 0x08 {
                        // Backspace
                        if s != e {
                            // Delete selected text
                            let mut merged = Vec::with_capacity(text_len - (e - s));
                            merged.extend_from_slice(&existing[..s]);
                            merged.extend_from_slice(&existing[e..]);
                            self.te_set_text_contents(bus, te_handle, &merged);
                            let new_pos = s.min(u16::MAX as usize) as u16;
                            bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, new_pos);
                            bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, new_pos);
                        } else if s > 0 {
                            // Delete character before insertion point
                            let mut merged = Vec::with_capacity(text_len - 1);
                            merged.extend_from_slice(&existing[..s - 1]);
                            merged.extend_from_slice(&existing[s..]);
                            self.te_set_text_contents(bus, te_handle, &merged);
                            let new_pos = (s - 1).min(u16::MAX as usize) as u16;
                            bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, new_pos);
                            bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, new_pos);
                        }
                        // At start with no selection: do nothing
                    } else {
                        // Insert printable character (replacing selection if any)
                        self.te_insert_text(bus, te_handle, &[key]);
                    }
                }
                cpu.write_reg(Register::A7, sp + 6);
                Ok(())
            }

            // TEScroll ($A9DD)
            // PROCEDURE TEScroll(dh: INTEGER; dv: INTEGER; hTE: TEHandle);
            // TEScroll ($A9DD): Scrolls destRect by (dh, dv) and redraws per IM:I I-389 / Text 1993 2-89
            (true, 0x1DD) => {
                let sp = cpu.read_reg(Register::A7);
                let te_handle = bus.read_long(sp);
                let dv = bus.read_word(sp + 4) as i16;
                let dh = bus.read_word(sp + 6) as i16;
                if trace_textedit_enabled() {
                    eprintln!("[TE] TEScroll hTE=${:08X} dh={} dv={}", te_handle, dh, dv);
                }
                self.te_scroll_contents(cpu, bus, te_handle, dh, dv);
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // TEInsert ($A9DE)
            // PROCEDURE TEInsert(text: Ptr; length: LONGINT; hTE: TEHandle);
            // Inside Macintosh Volume I, I-387; Inside Macintosh: Text 1993,
            // 2-94. TEInsert splices the supplied bytes into hText at the
            // selStart offset (inserting JUST BEFORE the selection range, not
            // replacing it). The selection range is preserved logically —
            // selStart and selEnd both shift forward by `length` so the
            // selection continues to span the same original characters.
            // TEInsert does not touch the scrap.
            (true, 0x1DE) => {
                let sp = cpu.read_reg(Register::A7);
                let te_handle = bus.read_long(sp);
                let length = bus.read_long(sp + 4) as usize;
                let text_ptr = bus.read_long(sp + 8);
                if text_ptr != 0 && length != 0 {
                    let te_ptr = Self::te_record_ptr(bus, te_handle);
                    if te_ptr != 0 {
                        let existing = Self::te_text_bytes(bus, te_handle);
                        let sel_start = (bus.read_word(te_ptr + Self::TE_SEL_START_OFFSET)
                            as usize)
                            .min(existing.len());
                        let sel_end = (bus.read_word(te_ptr + Self::TE_SEL_END_OFFSET) as usize)
                            .min(existing.len());
                        let text = bus.read_bytes(text_ptr, length);
                        let mut merged = Vec::with_capacity(existing.len() + text.len());
                        merged.extend_from_slice(&existing[..sel_start]);
                        merged.extend_from_slice(&text);
                        merged.extend_from_slice(&existing[sel_start..]);
                        self.te_set_text_contents(bus, te_handle, &merged);
                        let shifted_start = (sel_start + text.len()).min(u16::MAX as usize) as u16;
                        let shifted_end = (sel_end + text.len()).min(u16::MAX as usize) as u16;
                        bus.write_word(te_ptr + Self::TE_SEL_START_OFFSET, shifted_start);
                        bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, shifted_end);
                    }
                }
                cpu.write_reg(Register::A7, sp + 12);
                Ok(())
            }

            // TESetAlignment ($A9DF)
            // Sets the alignment (justification) of the text in the edit record.
            // PROCEDURE TESetAlignment(just: INTEGER; hTE: TEHandle);
            // Inside Macintosh Volume I, I-387; Text 1993, 2-87
            //
            // Regression coverage:
            //   tesetalignment_writes_just_field
            //   tesetalignment_center_value
            //   tesetalignment_right_value
            //   tesetalignment_pops_six_bytes
            // TESetAlignment ($A9DF): Writes just field to TERec per IM:I I-387
            (true, 0x1DF) => {
                let sp = cpu.read_reg(Register::A7);
                let te_handle = bus.read_long(sp);
                let just = bus.read_word(sp + 4) as i16;
                let te_ptr = Self::te_record_ptr(bus, te_handle);
                if te_ptr != 0 {
                    bus.write_word(te_ptr + Self::TE_JUST_OFFSET, just as u16);
                }
                cpu.write_reg(Register::A7, sp + 6);
                Ok(())
            }

            // SelectDialogItemText ($A97E)
            // PROCEDURE SelectDialogItemText(theDialog: DialogPtr;
            //     itemNo: INTEGER; strtSel: INTEGER; endSel: INTEGER);
            // Selects and highlights text in an editable text dialog item.
            // Inside Macintosh Volume I, I-414; Macintosh Toolbox Essentials
            // 1992, 6-131..6-132
            //
            // Pascal stack frame:
            //   SP+0  endSel: INTEGER     (2) — last arg, shallowest
            //   SP+2  strtSel: INTEGER    (2)
            //   SP+4  itemNo: INTEGER     (2)
            //   SP+6  theDialog: DialogPtr (4) — first arg, deepest
            // Total pop = 10 bytes.
            //
            // Behaviour per IM:I I-414:
            //   - "Selects the text from character strtSel through
            //     character endSel-1 in the editable text item with
            //     item number itemNo of the specified dialog box."
            //   - Special case: "If you set strtSel = 0 and
            //     endSel = -1, the entire text is selected" — we
            //     normalize this to (0, text.len()) at dispatch.
            //   - If itemNo is not an editText item (type 16), the
            //     procedure is a no-op (real Mac just ignores it).
            //   - The named item becomes the dialog's active edit
            //     field (DialogRecord.editField at offset 164).
            //
            // Systemless stores the normalized range in DialogItem and mirrors
            // it into DialogRecord.textH's TERecord when present, then redraws
            // the visible editText item so the selection highlight is
            // immediate like SelIText / SelectDialogItemText.
            (true, 0x17E) => {
                let sp = cpu.read_reg(Register::A7);
                let end_sel = bus.read_word(sp) as i16;
                let start_sel = bus.read_word(sp + 2) as i16;
                let item_no = bus.read_word(sp + 4) as i16;
                let dialog_ptr = bus.read_long(sp + 6);
                let mut redraw_item = None;
                if dialog_ptr != 0 && item_no > 0 {
                    if let Some(items) = self.dialog_items.get_mut(&dialog_ptr) {
                        if let Some(item) = items.get_mut((item_no - 1) as usize) {
                            if item.item_type & 0x7F == 16 {
                                // IM:I I-414 special case: (0, -1)
                                // means "select all" — normalize
                                // to (0, text.len()).
                                let text_len = item.text.len() as i16;
                                let (s, e) = if start_sel == 0 && end_sel == -1 {
                                    (0, text_len)
                                } else {
                                    // Clamp to text bounds.
                                    let s = start_sel.max(0).min(text_len);
                                    let e = end_sel.max(0).min(text_len);
                                    // Swap if reversed (defensive
                                    // — real Mac also normalizes).
                                    if s <= e {
                                        (s, e)
                                    } else {
                                        (e, s)
                                    }
                                };
                                item.sel_start = s;
                                item.sel_end = e;
                                bus.write_word(dialog_ptr + 164, (item_no - 1) as u16);
                                // Mirror selStart/selEnd into the TERecord so
                                // callers that read (**textH).selStart via the
                                // canonical DialogRecord layout can verify the
                                // selection (IM:I I-382, I-414).
                                // textH is a TEHandle at dialog_ptr+160
                                // (IM:I I-411 DialogRecord layout).
                                let text_h_handle = bus.read_long(dialog_ptr + 160);
                                if text_h_handle != 0 {
                                    let te_ptr = bus.read_long(text_h_handle);
                                    if te_ptr != 0 {
                                        bus.write_word(
                                            te_ptr + Self::TE_SEL_START_OFFSET,
                                            s as u16,
                                        );
                                        bus.write_word(te_ptr + Self::TE_SEL_END_OFFSET, e as u16);
                                    }
                                }
                                redraw_item = Some((dialog_ptr, item_no));
                            }
                        }
                    }
                }
                if let Some((dialog_ptr, item_no)) = redraw_item {
                    self.redraw_dialog_text_item(bus, dialog_ptr, item_no);
                }
                cpu.write_reg(Register::A7, sp + 10);
                Ok(())
            }

            // NewCDialog ($AA4B)
            // Color-aware variant of NewDialog. Identical parameters and
            // semantics; internally uses a CGrafPort instead of GrafPort
            // so the dialog supports color drawing. For our HLE this is
            // the same code path as NewDialog.
            // FUNCTION NewCDialog(dStorage: Ptr; boundsRect: Rect;
            //     title: Str255; visible: BOOLEAN; procID: INTEGER;
            //     behind: WindowPtr; goAwayFlag: BOOLEAN;
            //     refCon: LongInt; items: Handle): CDialogPtr;
            // Inside Macintosh Volume V, V-243
            //
            // NewDialog ($A97D)
            // Creates a dialog from a caller-supplied item list handle.
            // FUNCTION NewDialog(dStorage: Ptr; boundsRect: Rect; title: Str255;
            //     visible: BOOLEAN; procID: INTEGER; behind: WindowPtr;
            //     goAwayFlag: BOOLEAN; refCon: LONGINT; items: Handle): DialogPtr;
            // Inside Macintosh Volume I, I-412
            // NewDialog ($A97D): Allocates DialogRecord, processes DITL items, pops 32 bytes
            // NewCDialog ($AA4B): Color-aware NewDialog; same code path, CGrafPort variant per IM:V V-243
            (true, 0x17D) | (true, 0x24B) => {
                let sp = cpu.read_reg(Register::A7);
                // 68K Pascal stack:
                //   SP+0:  items(4)
                //   SP+4:  refCon(4)
                //   SP+8:  goAwayFlag(2)
                //   SP+10: behind(4)
                //   SP+14: procID(2)
                //   SP+16: visible(2)
                //   SP+18: title(4)
                //   SP+22: boundsRect(4)
                //   SP+26: dStorage(4)
                //   SP+30: result(4)
                let items_handle = bus.read_long(sp);
                let ref_con = bus.read_long(sp + 4);
                // Pascal BOOLEAN as the HIGH byte of its 2-byte stack slot
                // (MPW C convention).
                let go_away_flag = bus.read_byte(sp + 8) != 0;
                let behind = bus.read_long(sp + 10);
                let proc_id = bus.read_word(sp + 14) as i16;
                let visible = bus.read_byte(sp + 16) != 0;
                let title_ptr = bus.read_long(sp + 18);
                let bounds_rect_ptr = bus.read_long(sp + 22);
                let storage_ptr = bus.read_long(sp + 26);

                let bounds = if bounds_rect_ptr != 0 {
                    (
                        bus.read_word(bounds_rect_ptr) as i16,
                        bus.read_word(bounds_rect_ptr + 2) as i16,
                        bus.read_word(bounds_rect_ptr + 4) as i16,
                        bus.read_word(bounds_rect_ptr + 6) as i16,
                    )
                } else {
                    (0, 0, 0, 0)
                };
                let title = if title_ptr != 0 {
                    decode_mac_roman_for_render(&bus.read_pstring(title_ptr))
                } else {
                    String::new()
                };

                let items_ptr = if items_handle != 0 {
                    bus.read_long(items_handle)
                } else {
                    0
                };
                let items_len = if items_ptr != 0 {
                    bus.get_alloc_size(items_ptr).unwrap_or(0)
                } else {
                    0
                };
                let items = if items_ptr != 0 && items_len != 0 {
                    Self::parse_ditl(bus, items_ptr, items_len)
                } else {
                    Vec::new()
                };

                let dlg_ptr = self.finish_dialog_creation(
                    bus,
                    cpu,
                    storage_ptr,
                    bounds,
                    &title,
                    visible,
                    proc_id,
                    go_away_flag,
                    ref_con,
                    items_handle,
                    items,
                );
                // Honor Pascal `behind` param at SP+10 per IM:I I-412.
                self.apply_behind_parameter(bus, dlg_ptr, behind);
                bus.write_long(sp + 30, dlg_ptr);
                cpu.write_reg(Register::A7, sp + 30);
                Ok(())
            }

            // CloseDialog ($A982)
            // PROCEDURE CloseDialog(theDialog: DialogPtr);
            // Inside Macintosh Volume I, I-413
            // CloseDialog ($A982): Removes the dialog from the window list
            // and restores previous window state (bounds, procID, title)
            // when the closed dialog was frontmost. IM:I I-413 explicitly
            // states "deletes the dialog window from the window list" —
            // routed through untrack_window so FrontWindow sees the
            // updated list, matching DisposDialog's pattern.
            (true, 0x182) => {
                let sp = cpu.read_reg(Register::A7);
                let dialog_ptr = bus.read_long(sp);
                self.close_dialog_window(bus, cpu, dialog_ptr, false);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // UpdtDialog ($A978)
            // Redraws the dialog items in the specified update region.
            // PROCEDURE UpdtDialog(theDialog: DialogPtr; updateRgn: RgnHandle);
            // Inside Macintosh Volume I, I-415; Macintosh Toolbox Essentials 1992, 6-142..6-143
            (true, 0x178) => {
                let sp = cpu.read_reg(Register::A7);
                let update_rgn = bus.read_long(sp);
                let dialog_ptr = bus.read_long(sp + 4);
                cpu.write_reg(Register::A7, sp + 8);
                if update_rgn == 0 {
                    return Some(Ok(()));
                }
                let update_rect = Self::region_handle_rect(bus, update_rgn);
                self.update_dialog_window_contents(bus, cpu, dialog_ptr, update_rect);
                Ok(())
            }

            // CouldDialog ($A979) / FreeDialog ($A97A)
            // PROCEDURE CouldDialog(dialogID: INTEGER);
            // PROCEDURE FreeDialog (dialogID: INTEGER);
            // Inside Macintosh Volume I, I-415
            //
            // Per IM:I I-415, CouldDialog makes the DLOG, its DITL, and
            // resource-backed DITL items unpurgeable, loading them first if
            // needed; FreeDialog makes the same already-loaded resources
            // purgeable again. Systemless models that visible handle-state
            // axis for DLOG/DITL plus CNTL/ICON/PICT DITL resources. WDEF
            // cascade is intentionally omitted: HLE dialog window procs are
            // not loaded or executed as guest WDEF resources.
            // CouldDialog ($A979): Loads and HNoPurge-equivalent marks DLOG/DITL/resource-backed items; writes ResErr noErr on both hit and miss in BasiliskII.
            // FreeDialog ($A97A): HPurge-equivalent marks the already-loaded DLOG/DITL/resource-backed items; writes ResErr noErr on both hit and miss in BasiliskII.
            (true, 0x179) | (true, 0x17A) => {
                let sp = cpu.read_reg(Register::A7);
                let dialog_id = bus.read_word(sp) as i16;
                let load_if_missing = trap_num == 0x179;
                let purgeable = trap_num == 0x17A;
                self.cascade_dialog_resource_purgeability(
                    bus,
                    *b"DLOG",
                    dialog_id,
                    purgeable,
                    load_if_missing,
                );
                let res_err = self.dialog_template_res_err(dialog_id);
                bus.write_word(0x0A60, res_err as u16);
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // HideDialogItem ($A827)
            // Erases the item's enclosing rectangle, then moves the
            // item's display rect offscreen so it isn't drawn or hit-tested.
            // The original rect is saved so ShowDialogItem can restore it.
            // PROCEDURE HideDialogItem(theDialog: DialogPtr; itemNo: INTEGER);
            // Inside Macintosh Volume IV, IV-59
            // Stack: SP+0=itemNo(2), SP+2=theDialog(4). Pop 6.
            // HideDialogItem ($A827): If itemRect.left < 8192, calls EraseRect on
            // the item's enclosing rectangle, adds that rectangle to the update
            // region, and offsets itemRect.left/right by +16384 so the item
            // becomes offscreen; already-hidden items are unchanged
            // (Inside Macintosh Volume IV, IV-59; MTE 1992, 6-123).
            // Pops 6 bytes per IM:IV IV-59.
            (true, 0x027) => {
                let sp = cpu.read_reg(Register::A7);
                let item_no = bus.read_word(sp) as i16;
                let dialog_ptr = bus.read_long(sp + 2);
                cpu.write_reg(Register::A7, sp + 6);
                let mut updated_control_rect = None;
                let mut redraw_local_rect = None;
                if dialog_ptr != 0 && item_no > 0 {
                    let key = (dialog_ptr, item_no);
                    if let Some(items) = self.dialog_items.get_mut(&dialog_ptr) {
                        let idx = (item_no as usize).wrapping_sub(1);
                        if idx < items.len() {
                            let rect = items[idx].rect;
                            // MTE 1992, 6-123: already-hidden items (left > 8192) are a no-op.
                            if rect.1 < 8192 {
                                let enclosing = Self::dialog_item_enclosing_local_rect(
                                    items[idx].item_type,
                                    rect,
                                );
                                self.hidden_dialog_item_rects.entry(key).or_insert(rect);
                                items[idx].rect.1 = rect.1.wrapping_add(16384);
                                items[idx].rect.3 = rect.3.wrapping_add(16384);
                                updated_control_rect = Some(items[idx].rect);
                                redraw_local_rect = Some(enclosing);
                            }
                        }
                    }
                    if let Some(rect) = redraw_local_rect {
                        self.erase_dialog_item_enclosing_rect(bus, dialog_ptr, rect);
                        self.invalidate_window_rect(bus, dialog_ptr, rect);
                    }
                    if let Some(rect) = updated_control_rect {
                        if let Some(ctrl_handle) =
                            self.dialog_control_handle_for_item(dialog_ptr, item_no)
                        {
                            let ctrl_ptr = bus.read_long(ctrl_handle);
                            if ctrl_ptr != 0 {
                                bus.write_word(ctrl_ptr + 8, rect.0 as u16);
                                bus.write_word(ctrl_ptr + 10, rect.1 as u16);
                                bus.write_word(ctrl_ptr + 12, rect.2 as u16);
                                bus.write_word(ctrl_ptr + 14, rect.3 as u16);
                            }
                        }
                        if let Some(tracking) = self.dialog_tracking.as_mut() {
                            if tracking.dialog_ptr == dialog_ptr {
                                let idx = (item_no as usize).wrapping_sub(1);
                                if idx < tracking.items.len() {
                                    tracking.items[idx].rect = rect;
                                }
                            }
                        }
                    }
                }
                Ok(())
            }

            // ShowDialogItem ($A828)
            // Restores the rect saved by HideDialogItem and invalidates
            // the new rect so the dialog redraws the item.
            // PROCEDURE ShowDialogItem(theDialog: DialogPtr; itemNo: INTEGER);
            // Inside Macintosh Volume IV, IV-59
            // ShowDialogItem ($A828): If itemRect.left > 8192, restores visibility by
            // subtracting 16384 from itemRect.left/right; already-visible items are unchanged
            // (MTE 1992, 6-124). Pops 6 bytes per IM:IV IV-59.
            (true, 0x028) => {
                let sp = cpu.read_reg(Register::A7);
                let item_no = bus.read_word(sp) as i16;
                let dialog_ptr = bus.read_long(sp + 2);
                cpu.write_reg(Register::A7, sp + 6);
                let mut updated_control_rect = None;
                let mut redraw_local_rect = None;
                if dialog_ptr != 0 && item_no > 0 {
                    let key = (dialog_ptr, item_no);
                    if let Some(items) = self.dialog_items.get_mut(&dialog_ptr) {
                        let idx = (item_no as usize).wrapping_sub(1);
                        if idx < items.len() {
                            let rect = items[idx].rect;
                            // MTE 1992, 6-124: already-visible items (left < 8192) are a no-op.
                            if rect.1 > 8192 {
                                let restored_rect = if let Some(orig_rect) =
                                    self.hidden_dialog_item_rects.remove(&key)
                                {
                                    orig_rect
                                } else {
                                    (
                                        rect.0,
                                        rect.1.wrapping_sub(16384),
                                        rect.2,
                                        rect.3.wrapping_sub(16384),
                                    )
                                };
                                let enclosing = Self::dialog_item_enclosing_local_rect(
                                    items[idx].item_type,
                                    restored_rect,
                                );
                                items[idx].rect = restored_rect;
                                updated_control_rect = Some(restored_rect);
                                redraw_local_rect = Some(enclosing);
                            }
                        }
                    }
                    if let Some(rect) = redraw_local_rect {
                        self.invalidate_window_rect(bus, dialog_ptr, rect);
                    }
                    if let Some(rect) = updated_control_rect {
                        if let Some(ctrl_handle) =
                            self.dialog_control_handle_for_item(dialog_ptr, item_no)
                        {
                            let ctrl_ptr = bus.read_long(ctrl_handle);
                            if ctrl_ptr != 0 {
                                bus.write_word(ctrl_ptr + 8, rect.0 as u16);
                                bus.write_word(ctrl_ptr + 10, rect.1 as u16);
                                bus.write_word(ctrl_ptr + 12, rect.2 as u16);
                                bus.write_word(ctrl_ptr + 14, rect.3 as u16);
                            }
                        }
                        if let Some(tracking) = self.dialog_tracking.as_mut() {
                            if tracking.dialog_ptr == dialog_ptr {
                                let idx = (item_no as usize).wrapping_sub(1);
                                if idx < tracking.items.len() {
                                    tracking.items[idx].rect = rect;
                                }
                            }
                        }
                    }
                }
                Ok(())
            }

            // FindDItem ($A984)
            // Returns the 0-indexed item number of the item containing thePt
            // (in coordinates local to the dialog box), or –1 if no item
            // contains the point. Disabled items are returned per IM:IV-60;
            // hidden items naturally fail the rect-contains check because
            // HideDialogItem moves their rect to (16384, 16384, 16385, 16385).
            // Overlapping items resolve to the first matching item in DITL
            // order (Macintosh Toolbox Essentials 1992, 6-125).
            // FUNCTION FindDItem(theDialog: DialogPtr; thePt: Point): INTEGER;
            // Inside Macintosh Volume IV, IV-60; Macintosh Toolbox Essentials 1992, 6-125
            //
            // Stack layout (Pascal — args pushed left-to-right, result slot
            // pre-pushed by caller):
            //   SP+0..3:  thePt (Point — v at +0..1, h at +2..3)
            //   SP+4..7:  theDialog (DialogPtr)
            //   SP+8..9:  result slot (INTEGER)
            //
            // FindDItem ($A984): Walks dialog_items in order; first item whose
            // local rect contains thePt wins; returns 0-indexed item number or
            // -1 per Macintosh Toolbox Essentials 1992, p. 6-125.
            (true, 0x184) => {
                let sp = cpu.read_reg(Register::A7);
                let pt_v = bus.read_word(sp) as i16;
                let pt_h = bus.read_word(sp + 2) as i16;
                let dialog_ptr = bus.read_long(sp + 4);
                let result: i16 = if dialog_ptr == 0 {
                    -1
                } else if let Some(items) = self.dialog_items.get(&dialog_ptr) {
                    let mut hit: i16 = -1;
                    for (idx, item) in items.iter().enumerate() {
                        let (top, left, bottom, right) = item.rect;
                        if pt_v >= top && pt_v < bottom && pt_h >= left && pt_h < right {
                            hit = idx as i16;
                            break;
                        }
                    }
                    hit
                } else {
                    -1
                };
                bus.write_word(sp + 8, result as u16);
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // CouldAlert ($A989)
            // PROCEDURE CouldAlert(alertID: INTEGER);
            // Inside Macintosh Volume I, I-420
            //
            // FreeAlert ($A98A)
            // PROCEDURE FreeAlert(alertID: INTEGER);
            // Inside Macintosh Volume I, I-420
            //
            // Companion of CouldDialog/FreeDialog ($A979/$A97A) for ALRT
            // templates. Per IM:I I-420, CouldAlert loads and makes the
            // ALRT, DITL, and resource-backed DITL items unpurgeable;
            // FreeAlert marks the already-loaded equivalents purgeable.
            // BasiliskII treats missing alert IDs as harmless no-ops and
            // leaves ResErr at noErr.
            (true, 0x189) | (true, 0x18A) => {
                let sp = cpu.read_reg(Register::A7);
                let alert_id = bus.read_word(sp) as i16;
                let load_if_missing = trap_num == 0x189;
                let purgeable = trap_num == 0x18A;
                self.cascade_dialog_resource_purgeability(
                    bus,
                    *b"ALRT",
                    alert_id,
                    purgeable,
                    load_if_missing,
                );
                bus.write_word(0x0A60, 0);
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // ErrorSound ($A98C)
            // Sets the error-sound procedure for alerts.
            // PROCEDURE ErrorSound(soundProc: ProcPtr);
            // Inside Macintosh Volume I, I-411
            //
            // Per IM:I I-411: "The address of the sound procedure
            // being used is stored in the global variable DABeeper."
            // ErrorSound replaces the Dialog Manager's standard
            // sound procedure (installed by InitDialogs $A97B) with
            // the caller's `soundProc`. NIL is documented as "no
            // sound (or menu bar blinking) at all" — same encoding
            // as a NIL InitDialogs default.
            //
            // HLE compromise: Systemless doesn't invoke DABeeper from
            // any Alert family path (no menu-bar-blink emulation),
            // so this trap's only observable side effect is the
            // DABeeper global itself. Apps that probe DABeeper
            // before/after ErrorSound to detect "did the previous
            // app's sound proc get installed?" still see the
            // correct value. Future iterations that wire up
            // sound-proc invocation from Alert dispatch will pick
            // up the stored ProcPtr automatically.
            //
            // Pop = 4 bytes (soundProc ProcPtr).
            // ErrorSound ($A98C): Per IM:I I-411 stores soundProc at $0A9C (DABeeper global); NIL is the documented "no sound + no menu-bar-blink" default. Mirrors InitDialogs's "install standard sound procedure" step. HLE does not invoke DABeeper from Alert dispatch (no menu-bar-blink emulation), so this is state-only.
            (true, 0x18C) => {
                let sp = cpu.read_reg(Register::A7);
                let sound_proc = bus.read_long(sp);
                bus.write_long(crate::memory::globals::addr::DA_BEEPER, sound_proc);
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // SetDAFont ($A98D variant — actually uses DlgFont low-mem global)
            // Sets the font for subsequently created dialogs and alerts.
            // PROCEDURE SetDAFont(fontNum: INTEGER); [Not in ROM]
            // Inside Macintosh Volume I, I-412
            // Assembly-language: sets DlgFont ($0AFA) directly.
            //
            // Note: $A98D is GetDItem; SetDAFont is not a ROM trap but a
            // glue routine that writes DlgFont. We handle it via the
            // dedicated trap word $A97F (Pack13 slot repurposed).
            // If apps call it directly, they just write DlgFont.

            // SetDialogItemText ($A98F)
            // Sets the text of a dialog item (statText or editText).
            // PROCEDURE SetDialogItemText(item: Handle; text: Str255);
            // Inside Macintosh Volume I, I-422; Macintosh Toolbox Essentials 1992, 6-131
            (true, 0x18F) => {
                let sp = cpu.read_reg(Register::A7);
                let pc = cpu.read_reg(Register::PC);
                let text_str_ptr = bus.read_long(sp);
                let item_handle = bus.read_long(sp + 4);
                let mut redraw_text_item = None;
                if text_str_ptr != 0 {
                    let bytes = bus.read_pstring(text_str_ptr);
                    let len = bytes.len();
                    let text = decode_mac_roman_for_render(&bytes);

                    if item_handle != 0 {
                        let data_ptr = Self::ensure_text_handle_size(bus, item_handle, len);
                        if data_ptr != 0 {
                            bus.write_bytes(data_ptr, &bytes);
                        }
                    }

                    // Also update the item in dialog_items so ModalDialog picks it up
                    if let Some((dlg_ptr, idx)) =
                        self.dialog_item_handles.get(&item_handle).copied()
                    {
                        let item_no = (idx + 1) as i16;
                        if trace_dialog_items_enabled() {
                            eprintln!(
                                "[DIALOG-ITEM] SetDialogItemText pc=${:08X} dialog=${:08X} item={} handle=${:08X} text={:?}",
                                pc,
                                dlg_ptr,
                                item_no,
                                item_handle,
                                text
                            );
                        }
                        if let Some(items) = self.dialog_items.get_mut(&dlg_ptr) {
                            if idx < items.len() {
                                items[idx].text = text.clone();
                            }
                        }
                        let mut refresh_tracking = false;
                        if let Some(tracking) = self.dialog_tracking.as_mut() {
                            if tracking.dialog_ptr == dlg_ptr && idx < tracking.items.len() {
                                tracking.items[idx].text = text.clone();
                                if tracking.edit_item == item_no {
                                    tracking.edit_text = text;
                                    tracking.edit_text_modified = false;
                                }
                                refresh_tracking = !tracking.game_managed;
                            }
                        }
                        if refresh_tracking {
                            self.refresh_dialog_tracking_snapshot(bus);
                        } else {
                            redraw_text_item = Some((dlg_ptr, item_no));
                        }
                    } else if trace_dialog_items_enabled() {
                        eprintln!(
                            "[DIALOG-ITEM] SetDialogItemText pc=${:08X} dialog=<unknown> handle=${:08X} text={:?}",
                            pc,
                            item_handle,
                            text
                        );
                    }
                }

                if let Some((dlg_ptr, item_no)) = redraw_text_item {
                    self.redraw_dialog_text_item(bus, dlg_ptr, item_no);
                }

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

            // GetDialogItemText ($A990)
            // Returns the text of a dialog item (statText or editText).
            // PROCEDURE GetDialogItemText(item: Handle; VAR text: Str255);
            // Inside Macintosh Volume I, I-422
            (true, 0x190) => {
                let sp = cpu.read_reg(Register::A7);
                let text_ptr = bus.read_long(sp);
                let item_handle = bus.read_long(sp + 4);

                if text_ptr != 0 {
                    // If dialog tracking is active, return the current edit text
                    let mut wrote = false;
                    if let Some(ref tracking) = self.dialog_tracking {
                        let current_edit_handle =
                            self.dialog_item_handles.get(&item_handle).copied().filter(
                                |(dlg_ptr, idx)| {
                                    *dlg_ptr == tracking.dialog_ptr
                                        && (*idx as i16 + 1) == tracking.edit_item
                                },
                            );
                        if current_edit_handle.is_some() {
                            let bytes = tracking.edit_text.as_bytes();
                            let len = bytes.len().min(255);
                            bus.write_byte(text_ptr, len as u8);
                            for (i, byte) in bytes.iter().take(len).enumerate() {
                                bus.write_byte(text_ptr + 1 + i as u32, *byte);
                            }
                            wrote = true;
                        }
                    }
                    if !wrote {
                        // Text item handles store raw bytes, not a Pascal-length byte.
                        // Inside Macintosh Volume I, I-422; Executor dialManip.cpp
                        if item_handle != 0 {
                            let master = bus.read_long(item_handle);
                            if master != 0 {
                                let len = bus.get_alloc_size(master).unwrap_or(0).min(255) as usize;
                                bus.write_byte(text_ptr, len as u8);
                                for i in 0..len {
                                    bus.write_byte(
                                        text_ptr + 1 + i as u32,
                                        bus.read_byte(master + i as u32),
                                    );
                                }
                                wrote = true;
                            }
                        }
                        if !wrote {
                            bus.write_byte(text_ptr, 0);
                        }
                    }
                }
                cpu.write_reg(Register::A7, sp + 8);
                Ok(())
            }

            // DialogDispatch ($AA68)
            // Selector-based dispatch for extended Dialog Manager routines.
            // Selector is in D0 (not on stack) per the MPW THREEWORDINLINE
            // sequence: MOVE.W #selector, D0 / _DialogDispatch. The low
            // byte is the routine number; the high byte encodes param-
            // bytes / 2 which the dispatcher uses to pop args.
            // Macintosh Toolbox Essentials 1992, 6-162
            // DialogDispatch ($AA68): Selector-based per MTb 1992 6-162..6-167: $03 GetStdFilterProc returns a guest-callable standard-filter shim ProcPtr so apps using the common `GetStdFilterProc(&p); ModalDialog(p, &itemHit);` pattern get a safe non-NIL proc instead of Systemless's old NIL compromise; the shim itself declines events (FALSE) so Systemless's ModalDialog default Return/Escape handling still runs. $04 SetDialogDefaultItem writes newItem to DialogRecord.aDefItem at offset 168 + mirrors into DialogTrackingState.default_item if dialog is being tracked; $05 SetDialogCancelItem stores newItem in DialogTrackingState.cancel_item (no canonical DialogRecord field — System 7 addition); $06 SetDialogTracksCursor is a no-op noErr (HLE has no interactive cursor tracking); $0C NewFeaturesDialog allocates an Appearance-flavored DialogRecord via NewCDialog delegation. All return noErr; pop = 2 (selector encoded in D0) + arg_words*2 from the (arg_words << 8) | routine D0 encoding.
            (true, 0x268) => {
                let sp = cpu.read_reg(Register::A7);
                let d0 = cpu.read_reg(Register::D0) as u16;
                let param_bytes = (((d0 >> 8) & 0xFF) as u32) * 2;
                let routine = d0 & 0xFF;
                match routine {
                    // NewFeaturesDialog (selector $0C, param_bytes=34)
                    // FUNCTION NewFeaturesDialog(inStorage: Ptr;
                    //     inBoundsRect: Rect; inTitle: ConstStr255Param;
                    //     inIsVisible: Boolean; inProcID: SInt16;
                    //     inBehind: WindowPtr; inGoAwayFlag: Boolean;
                    //     inRefCon: SInt32; inItems: Handle;
                    //     inFeatures: UInt32): DialogPtr;
                    // Mac Toolbox: Appearance Manager (Apple, 1997).
                    //
                    // Same shape as NewCDialog with one extra LongInt
                    // for Appearance feature flags. Mid-90s titles
                    // (e.g. Meteor Storm) call this unconditionally —
                    // even when Gestalt('appr') reports no features —
                    // to allocate their main dialog. Delegate to the
                    // existing NewCDialog path so the dialog is created
                    // without the Appearance theming, then drop the
                    // unused inFeatures word.
                    0x0C => {
                        // Stack (low to high), 34 bytes of params + 4 result:
                        //   SP+0:  inFeatures(4)
                        //   SP+4:  inItems(4)
                        //   SP+8:  inRefCon(4)
                        //   SP+12: inGoAwayFlag(2)
                        //   SP+14: inBehind(4)
                        //   SP+18: inProcID(2)
                        //   SP+20: inIsVisible(2)
                        //   SP+22: inTitle(4)
                        //   SP+26: inBoundsRect(4)
                        //   SP+30: inStorage(4)
                        //   SP+34: result DialogPtr(4)
                        let _features = bus.read_long(sp);
                        let items_handle = bus.read_long(sp + 4);
                        let ref_con = bus.read_long(sp + 8);
                        let go_away_flag = bus.read_byte(sp + 12) != 0;
                        let behind = bus.read_long(sp + 14);
                        let proc_id = bus.read_word(sp + 18) as i16;
                        let visible = bus.read_byte(sp + 20) != 0;
                        let title_ptr = bus.read_long(sp + 22);
                        let bounds_rect_ptr = bus.read_long(sp + 26);
                        let storage_ptr = bus.read_long(sp + 30);

                        let bounds = if bounds_rect_ptr != 0 {
                            (
                                bus.read_word(bounds_rect_ptr) as i16,
                                bus.read_word(bounds_rect_ptr + 2) as i16,
                                bus.read_word(bounds_rect_ptr + 4) as i16,
                                bus.read_word(bounds_rect_ptr + 6) as i16,
                            )
                        } else {
                            (0, 0, 0, 0)
                        };
                        let title = if title_ptr != 0 {
                            decode_mac_roman_for_render(&bus.read_pstring(title_ptr))
                        } else {
                            String::new()
                        };
                        let items_ptr = if items_handle != 0 {
                            bus.read_long(items_handle)
                        } else {
                            0
                        };
                        let items_len = if items_ptr != 0 {
                            bus.get_alloc_size(items_ptr).unwrap_or(0)
                        } else {
                            0
                        };
                        let items = if items_ptr != 0 && items_len != 0 {
                            Self::parse_ditl(bus, items_ptr, items_len)
                        } else {
                            Vec::new()
                        };

                        let dlg_ptr = self.finish_dialog_creation(
                            bus,
                            cpu,
                            storage_ptr,
                            bounds,
                            &title,
                            visible,
                            proc_id,
                            go_away_flag,
                            ref_con,
                            items_handle,
                            items,
                        );
                        self.apply_behind_parameter(bus, dlg_ptr, behind);
                        bus.write_long(sp + param_bytes, dlg_ptr);
                        cpu.write_reg(Register::A7, sp + param_bytes);
                    }
                    // GetStdFilterProc (selector 3, param_bytes=4)
                    // FUNCTION GetStdFilterProc(VAR theProc: ProcPtr): OSErr;
                    // Macintosh Toolbox Essentials 1992, 6-163
                    //
                    // Returns a ProcPtr to the system's standard
                    // event filter for modal dialogs. The public
                    // Dialogs.h / Dialogs.p declarations expose
                    // GetStdFilterProc as returning a ModalFilterUPP
                    // through a VAR/out pointer. MTb 1992 documents
                    // that NIL filterProc passed to ModalDialog uses
                    // the standard filter, and BasiliskII returns a
                    // non-NIL callable proc here.
                    //
                    // HLE compromise: return a tiny guest-resident
                    // Pascal FUNCTION shim that always returns FALSE:
                    //
                    //   JMP    shimBody ; recognizable callback entry for
                    //                    runner-side filter-proc firing
                    //   MOVEQ  #0,D0    ; Boolean result = FALSE in D0 too
                    //   CLR.W  16(SP)   ; Boolean result = FALSE
                    //   RTD    #12      ; pop dialog/event/itemHit args
                    //
                    // This makes the proc SAFE and callable for guest
                    // code (better than the older NIL placeholder) while
                    // still preserving Systemless's own ModalDialog default
                    // handling for Return/Escape when apps pass the proc
                    // straight back to ModalDialog.
                    //
                    // NIL VAR ptr (the caller passed NULL for theProc)
                    // is a defensive no-op — the impl skips the write
                    // rather than dereffing NIL.
                    0x03 => {
                        let proc_ptr = bus.read_long(sp);
                        let shim = if self.dialog_std_filter_proc != 0 {
                            self.dialog_std_filter_proc
                        } else {
                            let shim = bus.alloc(16);
                            bus.write_word(shim, 0x4EF9); // JMP abs.L shimBody
                            bus.write_long(shim + 2, shim + 6);
                            // Pascal FUNCTION ModalFilterProc(dialog, event, itemHit): Boolean
                            // entry stack layout:
                            //   +0  return address
                            //   +4  itemHit ptr
                            //   +8  EventRecord ptr
                            //   +12 DialogPtr
                            //   +16 Boolean result slot
                            bus.write_word(shim + 6, 0x7000); // MOVEQ #0, D0
                            bus.write_word(shim + 8, 0x426F); // CLR.W 16(SP)
                            bus.write_word(shim + 10, 0x0010);
                            bus.write_word(shim + 12, 0x4E74); // RTD #12
                            bus.write_word(shim + 14, 0x000C);
                            self.dialog_std_filter_proc = shim;
                            shim
                        };
                        if proc_ptr != 0 {
                            bus.write_long(proc_ptr, shim);
                        }
                        // pop params; leave result in place
                        bus.write_word(sp + param_bytes, 0); // noErr
                        cpu.write_reg(Register::A7, sp + param_bytes);
                    }
                    // SetDialogDefaultItem (selector 4, param_bytes=6)
                    // FUNCTION SetDialogDefaultItem(theDialog: DialogPtr;
                    //     newItem: INTEGER): OSErr;
                    // Macintosh Toolbox Essentials 1992, 6-164
                    //
                    // Stack: SP+0=newItem(2), SP+2=theDialog(4),
                    //        SP+6=result OSErr slot (caller pre-pushed).
                    //
                    // Marks `newItem` as the dialog's default
                    // button — pressed when user hits Return per
                    // IM:MTb 6-164. Stored in DialogRecord.aDefItem
                    // at offset 168 so subsequent ModalDialog
                    // redraws can outline the default button. If
                    // dialog tracking is active for this dialog,
                    // mirror the value into DialogTrackingState
                    // .default_item so the active redraw path
                    // sees it without re-reading guest memory.
                    0x04 => {
                        let new_item = bus.read_word(sp) as i16;
                        let dialog_ptr = bus.read_long(sp + 2);
                        if dialog_ptr != 0 {
                            // DialogRecord.aDefItem is at offset 168
                            // per the existing GetNewDialog impl
                            // (dialog.rs:1961 writes "aDefItem" here).
                            bus.write_word(dialog_ptr + 168, new_item as u16);
                            // Mirror into active tracking state if
                            // this dialog is currently being tracked.
                            if let Some(tracking) = self.dialog_tracking.as_mut() {
                                if tracking.dialog_ptr == dialog_ptr {
                                    tracking.default_item = new_item;
                                }
                            }
                        }
                        bus.write_word(sp + param_bytes, 0); // noErr
                        cpu.write_reg(Register::A7, sp + param_bytes);
                    }
                    // SetDialogCancelItem (selector 5, param_bytes=6)
                    // FUNCTION SetDialogCancelItem(theDialog: DialogPtr;
                    //     newItem: INTEGER): OSErr;
                    // Macintosh Toolbox Essentials 1992, 6-165
                    //
                    // System 7 addition (no canonical DialogRecord
                    // field — Apple added cancel-item tracking to
                    // the Dialog Manager AFTER the original
                    // DialogRecord layout was finalized). Stored in
                    // a host-side per-dialog map and mirrored into
                    // active tracking state when ModalDialog is
                    // already running. NIL theDialog is a defensive
                    // no-op.
                    0x05 => {
                        let new_item = bus.read_word(sp) as i16;
                        let dialog_ptr = bus.read_long(sp + 2);
                        if dialog_ptr != 0 {
                            self.dialog_cancel_items.insert(dialog_ptr, new_item);
                            if let Some(tracking) = self.dialog_tracking.as_mut() {
                                if tracking.dialog_ptr == dialog_ptr {
                                    tracking.cancel_item = new_item;
                                }
                            }
                        }
                        bus.write_word(sp + param_bytes, 0); // noErr
                        cpu.write_reg(Register::A7, sp + param_bytes);
                    }
                    // SetDialogTracksCursor (selector 6, param_bytes=6)
                    // FUNCTION SetDialogTracksCursor(theDialog: DialogPtr;
                    //     tracks: BOOLEAN): OSErr;
                    // Macintosh Toolbox Essentials 1992, 6-166
                    //
                    // System 7 addition that controls whether the
                    // Dialog Manager auto-changes the cursor (to
                    // an I-beam) when the mouse is over an
                    // editText item. HLE compromise: Systemless
                    // doesn't model interactive cursor tracking
                    // (no continuous mouse-poll stream from the
                    // scripted event source), so the trap is a
                    // no-op noErr. Apps that defensively call this
                    // at dialog setup time get noErr and proceed.
                    0x06 => {
                        bus.write_word(sp + param_bytes, 0); // noErr
                        cpu.write_reg(Register::A7, sp + param_bytes);
                    }
                    _ => {
                        eprintln!(
                            "[TRAP] DialogDispatch unknown routine=${:02X} d0=${:04X}",
                            routine, d0
                        );
                        cpu.write_reg(Register::A7, sp + param_bytes);
                    }
                }
                Ok(())
            }

            _ => return None,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::super::test_helpers::{setup, setup_with_port, MockCpu, TEST_SP};
    use crate::cpu::{CpuOps, Register};
    use crate::memory::{MacMemoryBus, MemoryBus};
    use crate::trap::dispatch::{
        DialogButtonTrackingState, DialogItem, DialogPopupDraw, DialogTrackingState,
        PendingDialogPopupMenu, PersistentDialogSnapshot, RetainedModalDialogClickState,
    };
    use crate::trap::menu::{Menu, MenuItem};
    use crate::trap::TrapDispatcher;
    use crate::ui_theme::UiThemeId;
    use std::collections::VecDeque;

    fn screen_pixel_is_set(bus: &MacMemoryBus, base: u32, row_bytes: u32, x: i16, y: i16) -> bool {
        let byte = bus.read_byte(base + (y as u32 * row_bytes) + ((x as u32) / 8));
        byte & (0x80u8 >> ((x as u8) & 7)) != 0
    }

    fn count_set_pixels(
        bus: &MacMemoryBus,
        base: u32,
        row_bytes: u32,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
    ) -> u32 {
        let mut count = 0;
        for y in top..bottom {
            for x in left..right {
                if screen_pixel_is_set(bus, base, row_bytes, x, y) {
                    count += 1;
                }
            }
        }
        count
    }

    fn count_pixel_index(
        bus: &MacMemoryBus,
        base: u32,
        row_bytes: u32,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
        pixel_index: u8,
    ) -> u32 {
        let mut count = 0;
        for y in top..bottom {
            for x in left..right {
                if bus.read_byte(base + y as u32 * row_bytes + x as u32) == pixel_index {
                    count += 1;
                }
            }
        }
        count
    }

    fn sum_screen_bytes(
        bus: &MacMemoryBus,
        base: u32,
        row_bytes: u32,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
    ) -> u64 {
        let mut sum = 0;
        for y in top..bottom {
            let row = base + (y as u32 * row_bytes);
            for x in left..right {
                sum += u64::from(bus.read_byte(row + x as u32));
            }
        }
        sum
    }

    fn make_style_scrap(
        bus: &mut MacMemoryBus,
        styles: &[(u32, i16, i16, i16, i16, i16, (u16, u16, u16))],
    ) -> u32 {
        let handle = TrapDispatcher::allocate_handle_with_data(
            bus,
            TrapDispatcher::SCRAP_STYLE_TAB_OFFSET
                + (styles.len() as u32 * TrapDispatcher::SCRAP_STYLE_ELEMENT_SIZE),
        );
        let ptr = bus.read_long(handle);
        bus.write_word(
            ptr + TrapDispatcher::SCRAP_N_STYLES_OFFSET,
            styles.len() as u16,
        );
        for (index, (start, height, ascent, font, face, size, color)) in styles.iter().enumerate() {
            let base = ptr
                + TrapDispatcher::SCRAP_STYLE_TAB_OFFSET
                + (index as u32 * TrapDispatcher::SCRAP_STYLE_ELEMENT_SIZE);
            bus.write_long(base + TrapDispatcher::SCRAP_STYLE_START_CHAR_OFFSET, *start);
            bus.write_word(
                base + TrapDispatcher::SCRAP_STYLE_HEIGHT_OFFSET,
                *height as u16,
            );
            bus.write_word(
                base + TrapDispatcher::SCRAP_STYLE_ASCENT_OFFSET,
                *ascent as u16,
            );
            bus.write_word(base + TrapDispatcher::SCRAP_STYLE_FONT_OFFSET, *font as u16);
            bus.write_word(base + TrapDispatcher::SCRAP_STYLE_FACE_OFFSET, *face as u16);
            bus.write_word(base + TrapDispatcher::SCRAP_STYLE_SIZE_OFFSET, *size as u16);
            bus.write_word(base + TrapDispatcher::SCRAP_STYLE_COLOR_OFFSET, color.0);
            bus.write_word(base + TrapDispatcher::SCRAP_STYLE_COLOR_OFFSET + 2, color.1);
            bus.write_word(base + TrapDispatcher::SCRAP_STYLE_COLOR_OFFSET + 4, color.2);
        }
        handle
    }

    // ---- InitDialogs ($A97B) ----

    #[test]
    fn init_dialogs_pops_4_bytes() {
        let (mut disp, mut cpu, mut bus) = setup();
        // SP+0: ptr (4 bytes)
        bus.write_long(TEST_SP, 0x00000000);

        let result = disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn dialog_edit_state_falls_back_to_first_valid_edit_text_item() {
        let (_disp, _cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        bus.write_word(dialog_ptr + 164, 0); // stale/default editField points at item 1
        bus.write_word(dialog_ptr + 168, 1); // default OK button
        let items = vec![
            DialogItem {
                item_type: 4,
                text: "OK".to_string(),
                ..Default::default()
            },
            DialogItem {
                item_type: 4,
                text: "Cancel".to_string(),
                ..Default::default()
            },
            DialogItem {
                item_type: 16,
                text: "answer".to_string(),
                ..Default::default()
            },
        ];

        let (text, edit_item, default_item) =
            TrapDispatcher::dialog_edit_state(&bus, dialog_ptr, &items);

        assert_eq!(text, "answer");
        assert_eq!(edit_item, 3);
        assert_eq!(default_item, 1);
    }

    #[test]
    fn dialog_edit_state_empty_live_handle_does_not_fallback_to_template_text() {
        let (_disp, _cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(16);
        let text_handle = bus.alloc(4);

        bus.write_long(text_handle, 0); // valid empty text handle
        bus.write_long(items_handle, ditl_ptr);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_word(dialog_ptr + 164, 0); // item 1 is the active edit field
        bus.write_word(dialog_ptr + 168, 1);
        bus.write_word(ditl_ptr, 0);
        bus.write_long(ditl_ptr + 2, text_handle);
        bus.write_word(ditl_ptr + 6, 10);
        bus.write_word(ditl_ptr + 8, 10);
        bus.write_word(ditl_ptr + 10, 26);
        bus.write_word(ditl_ptr + 12, 120);
        bus.write_byte(ditl_ptr + 14, 16);
        bus.write_byte(ditl_ptr + 15, 0);

        let items = vec![DialogItem {
            item_type: 16,
            rect: (10, 10, 26, 120),
            text: "Edit Text".to_string(),
            resource_id: 0,
            proc_ptr: 0,
            sel_start: 0,
            sel_end: 0,
        }];

        let (text, edit_item, default_item) =
            TrapDispatcher::dialog_edit_state(&bus, dialog_ptr, &items);

        assert_eq!(text, "");
        assert_eq!(edit_item, 1);
        assert_eq!(default_item, 1);
    }

    #[test]
    fn parse_dlog_ignores_position_word_when_resource_is_classic_length() {
        let (_disp, _cpu, mut bus) = setup();
        let ptr = bus.alloc(24);
        bus.write_word(ptr, 228); // top
        bus.write_word(ptr + 2, 198); // left
        bus.write_word(ptr + 4, 372); // bottom
        bus.write_word(ptr + 6, 455); // right
        bus.write_word(ptr + 8, 2); // procID
        bus.write_byte(ptr + 10, 1); // visible
        bus.write_byte(ptr + 12, 0); // goAway
        bus.write_long(ptr + 14, 0); // refCon
        bus.write_word(ptr + 18, 1013); // itemsID
        bus.write_byte(ptr + 20, 0); // empty title
        bus.write_byte(ptr + 21, 0); // padding only, not a position field
        bus.write_word(ptr + 22, 0x280A); // poison: centerMainScreen if over-read

        let (bounds, proc_id, visible, items_id, title, position) =
            TrapDispatcher::parse_dlog(&bus, ptr, 22);

        assert_eq!(bounds, (228, 198, 372, 455));
        assert_eq!(proc_id, 2);
        assert!(visible);
        assert_eq!(items_id, 1013);
        assert_eq!(title, "");
        assert_eq!(position, 0);
    }

    #[test]
    fn parse_dlog_reads_position_word_when_resource_contains_it() {
        let (_disp, _cpu, mut bus) = setup();
        let ptr = bus.alloc(24);
        bus.write_word(ptr, 10);
        bus.write_word(ptr + 2, 20);
        bus.write_word(ptr + 4, 110);
        bus.write_word(ptr + 6, 220);
        bus.write_word(ptr + 8, 2);
        bus.write_byte(ptr + 10, 1);
        bus.write_long(ptr + 14, 0);
        bus.write_word(ptr + 18, 42);
        bus.write_byte(ptr + 20, 0);
        bus.write_byte(ptr + 21, 0);
        bus.write_word(ptr + 22, 0x280A);

        let (_, _, _, _, _, position) = TrapDispatcher::parse_dlog(&bus, ptr, 24);

        assert_eq!(position, 0x280A);
    }

    #[test]
    fn dlog_parse_title_alignment_even_and_odd() {
        let (_disp, _cpu, mut bus) = setup();
        let odd = build_test_dlog_with_title((10, 20, 110, 220), 701, "Odd", 0x300A);
        let even = build_test_dlog_with_title((11, 21, 111, 221), 702, "Even", 0x700A);

        let odd_ptr = bus.alloc(odd.len() as u32);
        bus.write_bytes(odd_ptr, &odd);
        let even_ptr = bus.alloc(even.len() as u32);
        bus.write_bytes(even_ptr, &even);

        // MTE 1992 p. 6-148: the optional position word follows the Pascal
        // title string after a 0-or-1-byte alignment pad.
        let (odd_bounds, _, _, odd_items, odd_title, odd_position) =
            TrapDispatcher::parse_dlog(&bus, odd_ptr, odd.len() as u32);
        let (even_bounds, _, _, even_items, even_title, even_position) =
            TrapDispatcher::parse_dlog(&bus, even_ptr, even.len() as u32);

        assert_eq!(odd_bounds, (10, 20, 110, 220));
        assert_eq!(odd_items, 701);
        assert_eq!(odd_title, "Odd");
        assert_eq!(odd_position, 0x300A);
        assert_eq!(odd.len(), 26);

        assert_eq!(even_bounds, (11, 21, 111, 221));
        assert_eq!(even_items, 702);
        assert_eq!(even_title, "Even");
        assert_eq!(even_position, 0x700A);
        assert_eq!(even.len(), 28);
    }

    #[test]
    fn dlog_parse_position_constants() {
        let (_disp, _cpu, mut bus) = setup();
        // MTE 1992 p. 6-148 documents the compiled DLOG position constants
        // stored after the aligned title string.
        for position in [0x0000u16, 0xB00A, 0x300A, 0x700A] {
            let dlog = build_test_dlog_with_title((10, 20, 110, 220), 711, "P", position);
            let ptr = bus.alloc(dlog.len() as u32);
            bus.write_bytes(ptr, &dlog);

            let (_, _, _, _, _, parsed_position) =
                TrapDispatcher::parse_dlog(&bus, ptr, dlog.len() as u32);

            assert_eq!(parsed_position, position);
        }
    }

    #[test]
    fn alrt_parse_stage_nibbles_and_position() {
        let (_disp, _cpu, mut bus) = setup();
        let mut alrt = build_alrt_template(-321, 0xF721);
        alrt.extend_from_slice(&0xB00Au16.to_be_bytes());
        let ptr = bus.alloc(alrt.len() as u32);
        bus.write_bytes(ptr, &alrt);

        let (bounds, items_id, stages, position) =
            TrapDispatcher::parse_alrt(&bus, ptr, alrt.len() as u32);

        assert_eq!(bounds, (0, 0, 80, 200));
        assert_eq!(items_id, -321);
        assert_eq!(stages, 0xF721);
        // IM:I I-425 to I-426: low nibble is stage 1, high nibble is stage 4.
        assert_eq!(stages & 0x000F, 0x0001);
        assert_eq!((stages >> 4) & 0x000F, 0x0002);
        assert_eq!((stages >> 8) & 0x000F, 0x0007);
        assert_eq!((stages >> 12) & 0x000F, 0x000F);
        assert_eq!(position, 0xB00A);

        let classic_alrt = build_alrt_template(123, 0x0008);
        let classic_ptr = bus.alloc(classic_alrt.len() as u32);
        bus.write_bytes(classic_ptr, &classic_alrt);
        let (_, classic_items_id, classic_stages, classic_position) =
            TrapDispatcher::parse_alrt(&bus, classic_ptr, classic_alrt.len() as u32);
        assert_eq!(classic_items_id, 123);
        assert_eq!(classic_stages, 0x0008);
        assert_eq!(classic_position, 0);
    }

    fn push_ditl_count(data: &mut Vec<u8>, count_minus_one: i16) {
        data.extend_from_slice(&count_minus_one.to_be_bytes());
    }

    fn push_ditl_item(
        data: &mut Vec<u8>,
        item_handle: u32,
        rect: (i16, i16, i16, i16),
        item_type: u8,
        length_or_reserved: u8,
        payload: &[u8],
    ) {
        data.extend_from_slice(&item_handle.to_be_bytes());
        data.extend_from_slice(&rect.0.to_be_bytes());
        data.extend_from_slice(&rect.1.to_be_bytes());
        data.extend_from_slice(&rect.2.to_be_bytes());
        data.extend_from_slice(&rect.3.to_be_bytes());
        data.push(item_type);
        data.push(length_or_reserved);
        data.extend_from_slice(payload);
        if payload.len() % 2 != 0 {
            data.push(0);
        }
    }

    fn parse_ditl_bytes(bus: &mut MacMemoryBus, data: &[u8]) -> Vec<DialogItem> {
        let ptr = bus.alloc(data.len() as u32);
        bus.write_bytes(ptr, data);
        TrapDispatcher::parse_ditl(bus, ptr, data.len() as u32)
    }

    #[test]
    fn ditl_iter_empty_count_minus_one() {
        let (_disp, _cpu, mut bus) = setup();
        let mut ditl = Vec::new();
        // IM:I I-427: the first word stores the number of items minus 1.
        push_ditl_count(&mut ditl, -1);

        let items = parse_ditl_bytes(&mut bus, &ditl);

        assert!(items.is_empty());
    }

    #[test]
    fn ditl_iter_button_checkbox_radio_static_edit_text() {
        let (_disp, _cpu, mut bus) = setup();
        let mut ditl = Vec::new();
        push_ditl_count(&mut ditl, 4);
        for (item_type, rect, text) in [
            (4, (1, 2, 11, 42), "OK"),
            (5, (12, 2, 22, 72), "Check"),
            (6, (23, 2, 33, 72), "Radio"),
            (8, (34, 2, 44, 92), "Static"),
            (16, (45, 2, 57, 122), "Edit"),
        ] {
            push_ditl_item(
                &mut ditl,
                0,
                rect,
                item_type,
                text.len() as u8,
                text.as_bytes(),
            );
        }

        let items = parse_ditl_bytes(&mut bus, &ditl);

        assert_eq!(items.len(), 5);
        assert_eq!(items[0].item_type, 4);
        assert_eq!(items[0].rect, (1, 2, 11, 42));
        assert_eq!(items[0].text, "OK");
        assert_eq!(items[1].item_type, 5);
        assert_eq!(items[1].text, "Check");
        assert_eq!(items[2].item_type, 6);
        assert_eq!(items[2].text, "Radio");
        assert_eq!(items[3].item_type, 8);
        assert_eq!(items[3].text, "Static");
        assert_eq!(items[4].item_type, 16);
        assert_eq!(items[4].rect, (45, 2, 57, 122));
        assert_eq!(items[4].text, "Edit");
    }

    #[test]
    fn ditl_iter_control_icon_picture_resource_id() {
        let (_disp, _cpu, mut bus) = setup();
        let mut ditl = Vec::new();
        push_ditl_count(&mut ditl, 2);
        // MTE 1992 p. 6-153 documents resource-backed DITL items as a
        // reserved byte followed by a signed 16-bit resource ID.
        push_ditl_item(&mut ditl, 0, (1, 2, 11, 42), 7, 0, &128i16.to_be_bytes());
        // IM:I I-427 describes the same slot as a length byte of 2.
        push_ditl_item(
            &mut ditl,
            0,
            (12, 2, 28, 34),
            32,
            2,
            &(-42i16).to_be_bytes(),
        );
        push_ditl_item(&mut ditl, 0, (29, 2, 70, 90), 64, 0, &300i16.to_be_bytes());

        let items = parse_ditl_bytes(&mut bus, &ditl);

        assert_eq!(items.len(), 3);
        assert_eq!(items[0].item_type, 7);
        assert_eq!(items[0].resource_id, 128);
        assert_eq!(items[1].item_type, 32);
        assert_eq!(items[1].resource_id, -42);
        assert_eq!(items[2].item_type, 64);
        assert_eq!(items[2].resource_id, 300);
    }

    #[test]
    fn ditl_iter_user_item_without_data_bytes() {
        let (_disp, _cpu, mut bus) = setup();
        let mut ditl = Vec::new();
        push_ditl_count(&mut ditl, 0);
        push_ditl_item(&mut ditl, 0x12345678, (10, 20, 30, 40), 0, 0, &[]);

        let items = parse_ditl_bytes(&mut bus, &ditl);

        assert_eq!(items.len(), 1);
        assert_eq!(items[0].item_type, 0);
        assert_eq!(items[0].rect, (10, 20, 30, 40));
        assert_eq!(items[0].proc_ptr, 0x12345678);
    }

    #[test]
    fn ditl_iter_help_item_sized_payload() {
        let (_disp, _cpu, mut bus) = setup();
        let mut ditl = Vec::new();
        push_ditl_count(&mut ditl, 1);
        // MTE 1992 p. 6-154: help items use a 4- or 6-byte payload.
        push_ditl_item(
            &mut ditl,
            0,
            (0, 0, 0, 0),
            1,
            6,
            &[0x00, 0x08, 0x01, 0x2C, 0x00, 0x02],
        );
        push_ditl_item(&mut ditl, 0, (5, 6, 17, 50), 4, 2, b"OK");

        let items = parse_ditl_bytes(&mut bus, &ditl);

        assert_eq!(items.len(), 2);
        assert_eq!(items[0].item_type, 1);
        assert_eq!(items[1].item_type, 4);
        assert_eq!(items[1].text, "OK");
    }

    #[test]
    fn ditl_iter_odd_pascal_text_alignment() {
        let (_disp, _cpu, mut bus) = setup();
        let mut ditl = Vec::new();
        push_ditl_count(&mut ditl, 1);
        push_ditl_item(&mut ditl, 0, (1, 2, 11, 42), 8, 3, b"Odd");
        push_ditl_item(&mut ditl, 0, (12, 2, 22, 42), 4, 2, b"OK");

        let items = parse_ditl_bytes(&mut bus, &ditl);

        assert_eq!(items.len(), 2);
        assert_eq!(items[0].text, "Odd");
        assert_eq!(items[1].rect, (12, 2, 22, 42));
        assert_eq!(items[1].text, "OK");
    }

    #[test]
    fn ditl_iter_even_pascal_text_alignment() {
        let (_disp, _cpu, mut bus) = setup();
        let mut ditl = Vec::new();
        push_ditl_count(&mut ditl, 1);
        push_ditl_item(&mut ditl, 0, (1, 2, 11, 42), 8, 4, b"Even");
        push_ditl_item(&mut ditl, 0, (12, 2, 22, 42), 4, 2, b"OK");

        let items = parse_ditl_bytes(&mut bus, &ditl);

        assert_eq!(items.len(), 2);
        assert_eq!(items[0].text, "Even");
        assert_eq!(items[1].rect, (12, 2, 22, 42));
        assert_eq!(items[1].text, "OK");
    }

    #[test]
    fn ditl_iter_rejects_or_stops_at_truncated_item() {
        let (_disp, _cpu, mut bus) = setup();
        let mut ditl = Vec::new();
        push_ditl_count(&mut ditl, 1);
        push_ditl_item(&mut ditl, 0, (1, 2, 11, 42), 4, 2, b"OK");
        ditl.extend_from_slice(&0u32.to_be_bytes());
        for coord in [12i16, 2, 22, 72] {
            ditl.extend_from_slice(&coord.to_be_bytes());
        }
        ditl.push(8);
        ditl.push(6);
        ditl.extend_from_slice(b"Bad");

        let items = parse_ditl_bytes(&mut bus, &ditl);

        assert_eq!(items.len(), 1);
        assert_eq!(items[0].text, "OK");
    }

    fn build_test_dlog(bounds: (i16, i16, i16, i16), items_id: i16, position: u16) -> Vec<u8> {
        build_test_dlog_with_title(bounds, items_id, "", position)
    }

    fn build_test_dlog_with_title(
        bounds: (i16, i16, i16, i16),
        items_id: i16,
        title: &str,
        position: u16,
    ) -> Vec<u8> {
        let mut data = Vec::with_capacity(22 + title.len() + (title.len() & 1));
        data.extend_from_slice(&bounds.0.to_be_bytes());
        data.extend_from_slice(&bounds.1.to_be_bytes());
        data.extend_from_slice(&bounds.2.to_be_bytes());
        data.extend_from_slice(&bounds.3.to_be_bytes());
        data.extend_from_slice(&2i16.to_be_bytes()); // procID: plainDBox
        data.push(0); // invisible; geometry-only tests do not need drawing
        data.push(0); // filler
        data.push(0); // goAwayFlag
        data.push(0); // filler
        data.extend_from_slice(&0u32.to_be_bytes()); // refCon
        data.extend_from_slice(&items_id.to_be_bytes());
        data.push(title.len() as u8);
        data.extend_from_slice(title.as_bytes());
        if (1 + title.len()) % 2 != 0 {
            data.push(0); // title alignment padding
        }
        data.extend_from_slice(&position.to_be_bytes());
        data
    }

    fn build_test_ditl_item(
        item_type: u8,
        rect: (i16, i16, i16, i16),
        item_data: &[u8],
    ) -> Vec<u8> {
        build_test_ditl_items(&[(item_type, rect, item_data)])
    }

    fn build_test_ditl_items(items: &[(u8, (i16, i16, i16, i16), &[u8])]) -> Vec<u8> {
        let total_len = items.iter().fold(2usize, |len, (_, _, item_data)| {
            len + 14 + item_data.len() + (item_data.len() & 1)
        });
        let mut data = Vec::with_capacity(total_len);
        data.extend_from_slice(&((items.len() as u16).saturating_sub(1)).to_be_bytes());
        for (item_type, rect, item_data) in items {
            append_test_ditl_item(&mut data, *item_type, *rect, item_data);
        }
        data
    }

    fn append_test_ditl_item(
        data: &mut Vec<u8>,
        item_type: u8,
        rect: (i16, i16, i16, i16),
        item_data: &[u8],
    ) {
        data.extend_from_slice(&0u32.to_be_bytes()); // itmHandle / userItem proc ptr
        data.extend_from_slice(&rect.0.to_be_bytes());
        data.extend_from_slice(&rect.1.to_be_bytes());
        data.extend_from_slice(&rect.2.to_be_bytes());
        data.extend_from_slice(&rect.3.to_be_bytes());
        data.push(item_type);
        data.push(item_data.len() as u8);
        data.extend_from_slice(item_data);
        if item_data.len() % 2 != 0 {
            data.push(0);
        }
    }

    fn build_test_alrt(bounds: (i16, i16, i16, i16), items_id: i16, stages: u16) -> Vec<u8> {
        let mut data = Vec::with_capacity(12);
        data.extend_from_slice(&bounds.0.to_be_bytes());
        data.extend_from_slice(&bounds.1.to_be_bytes());
        data.extend_from_slice(&bounds.2.to_be_bytes());
        data.extend_from_slice(&bounds.3.to_be_bytes());
        data.extend_from_slice(&items_id.to_be_bytes());
        data.extend_from_slice(&stages.to_be_bytes());
        data
    }

    #[test]
    fn multi_button_alert_tracks_click_and_returns_selected_item() {
        // Multi-choice Alert resources are real modal dialogs, not merely
        // OK-only notices. Keep the Alert function frame pending until a
        // button is selected, then write the selected item number into the
        // function result slot at SP+6.
        let (mut disp, mut cpu, mut bus) = setup();
        let alert_id = 3300;
        let ditl_id = 3301;
        let alrt = build_test_alrt((100, 100, 210, 400), ditl_id, 0x5555);
        let ditl = build_test_ditl_items(&[
            (4, (70, 230, 90, 290), b"Choice 1"),
            (4, (70, 130, 90, 210), b"Choice 2"),
            (4, (70, 20, 90, 100), b"Cancel"),
            (
                8,
                (16, 20, 56, 280),
                b"Select one of these options before continuing.",
            ),
        ]);
        disp.install_test_resource(&mut bus, *b"ALRT", alert_id, &alrt);
        disp.install_test_resource(&mut bus, *b"DITL", ditl_id, &ditl);

        bus.write_long(TEST_SP, 0); // filterProc
        bus.write_word(TEST_SP + 4, alert_id as u16);
        bus.write_word(TEST_SP + 6, 0xCAFE);
        let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
        assert_eq!(bus.read_word(TEST_SP + 6), 0);
        assert!(disp.dialog_tracking.is_some());
        assert!(disp.is_tracking_refire(0xA985));

        disp.push_mouse_down(180, 250);
        for _ in 0..4 {
            let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            if disp
                .dialog_tracking
                .as_ref()
                .and_then(|tracking| tracking.active_button.as_ref())
                .is_some()
            {
                break;
            }
        }
        assert!(disp
            .dialog_tracking
            .as_ref()
            .and_then(|tracking| tracking.active_button.as_ref())
            .is_some_and(|button| button.item_no == 2));

        disp.push_mouse_up(180, 250);
        let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        for _ in 0..64 {
            if disp.dialog_tracking.is_none() {
                break;
            }
            let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
        }

        assert!(disp.dialog_tracking.is_none());
        assert_eq!(bus.read_word(TEST_SP + 6), 2);
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
    }

    fn loaded_resource_handle_for_test(
        disp: &TrapDispatcher,
        res_type: [u8; 4],
        res_id: i16,
    ) -> u32 {
        disp.loaded_handles
            .iter()
            .find_map(|(&handle, &(_, loaded_type, loaded_id))| {
                if loaded_type == res_type && loaded_id == res_id {
                    Some(handle)
                } else {
                    None
                }
            })
            .unwrap_or_else(|| {
                panic!(
                    "expected loaded {} resource {}",
                    String::from_utf8_lossy(&res_type),
                    res_id
                )
            })
    }

    fn assert_resource_nonpurgeable(
        disp: &TrapDispatcher,
        bus: &MacMemoryBus,
        res_type: [u8; 4],
        res_id: i16,
    ) {
        let handle = loaded_resource_handle_for_test(disp, res_type, res_id);
        assert_ne!(
            bus.read_long(handle),
            0,
            "{} {} should have a non-NIL master pointer",
            String::from_utf8_lossy(&res_type),
            res_id
        );
        assert_eq!(
            disp.handle_state_bits.get(&handle).copied().unwrap_or(0) & 0x40,
            0,
            "{} {} should be nonpurgeable",
            String::from_utf8_lossy(&res_type),
            res_id
        );
    }

    fn assert_resource_purgeable(disp: &TrapDispatcher, res_type: [u8; 4], res_id: i16) {
        let handle = loaded_resource_handle_for_test(disp, res_type, res_id);
        assert_ne!(
            disp.handle_state_bits.get(&handle).copied().unwrap_or(0) & 0x40,
            0,
            "{} {} should be purgeable",
            String::from_utf8_lossy(&res_type),
            res_id
        );
    }

    fn ditl_item_handle_field(bus: &MacMemoryBus, ditl_ptr: u32, item_no: i16) -> u32 {
        if item_no <= 0 || ditl_ptr == 0 {
            return 0;
        }

        let max_index = bus.read_word(ditl_ptr) as i16;
        if item_no > max_index + 1 {
            return 0;
        }

        let mut offset = 2u32;
        for current_item in 1..=max_index + 1 {
            let handle_addr = ditl_ptr + offset;
            offset += 4; // itmHandle / userItem ProcPtr
            offset += 8; // item display rectangle
            let data_len = bus.read_byte(ditl_ptr + offset + 1) as u32;
            offset += 2; // item type + data length
            let padded = (data_len + 1) & !1;
            if current_item == item_no {
                return bus.read_long(handle_addr);
            }
            offset += padded;
        }

        0
    }

    // ---- DialogDispatch ($AA68) ----
    // Macintosh Toolbox Essentials (1992), pp. 6-162 to 6-167.

    #[test]
    fn dialogdispatch_getstdfilterproc_selector_03_writes_non_nil_shim_and_returns_noerr() {
        let (mut disp, mut cpu, mut bus) = setup();
        let proc_storage_ptr = 0x300000u32;

        bus.write_long(proc_storage_ptr, 0x00AB_CDEF);
        bus.write_long(TEST_SP, proc_storage_ptr); // VAR theProc
        bus.write_word(TEST_SP + 4, 0xBEEF); // OSErr result slot
        cpu.write_reg(Register::D0, 0x0203); // selector 3, 4 param bytes

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

        let shim = bus.read_long(proc_storage_ptr);
        assert_ne!(shim, 0);
        assert_eq!(shim, disp.dialog_std_filter_proc);
        assert_eq!(bus.read_word(shim), 0x4EF9); // JMP abs.L shimBody
        assert_eq!(bus.read_long(shim + 2), shim + 6);
        assert_eq!(bus.read_word(shim + 6), 0x7000); // MOVEQ #0, D0
        assert_eq!(bus.read_word(shim + 8), 0x426F); // CLR.W 16(SP)
        assert_eq!(bus.read_word(shim + 10), 0x0010);
        assert_eq!(bus.read_word(shim + 12), 0x4E74); // RTD #12
        assert_eq!(bus.read_word(shim + 14), 0x000C);
        assert_eq!(bus.read_word(TEST_SP + 4), 0);
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn dialogdispatch_getstdfilterproc_selector_03_reuses_cached_shim() {
        let (mut disp, mut cpu, mut bus) = setup();
        let slot_a = 0x300000u32;
        let slot_b = 0x300004u32;

        bus.write_long(TEST_SP, slot_a);
        bus.write_word(TEST_SP + 4, 0xBEEF);
        cpu.write_reg(Register::D0, 0x0203);
        let first = disp.dispatch_dialog(true, 0x268, &mut cpu, &mut bus);
        assert!(first.unwrap().is_ok());
        let shim_a = bus.read_long(slot_a);
        assert_ne!(shim_a, 0);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, slot_b);
        bus.write_word(TEST_SP + 4, 0xCAFE);
        cpu.write_reg(Register::D0, 0x0203);
        let second = disp.dispatch_dialog(true, 0x268, &mut cpu, &mut bus);
        assert!(second.unwrap().is_ok());
        let shim_b = bus.read_long(slot_b);

        assert_eq!(shim_a, shim_b);
        assert_eq!(shim_b, disp.dialog_std_filter_proc);
        assert_eq!(bus.read_word(shim_b), 0x4EF9);
        assert_eq!(bus.read_word(shim_b + 6), 0x7000);
        assert_eq!(bus.read_word(TEST_SP + 4), 0);
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn dialogdispatch_setdialogdefaultitem_selector_04_writes_adefitem_and_updates_tracking() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(256);

        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
            dialog_ptr,
            bounds: (0, 0, 0, 0),
            title: String::new(),
            proc_id: 0,
            items: Vec::new(),
            default_item: 1,
            cancel_item: 2,
            edit_text: String::new(),
            edit_item: 0,
            saved_pixels: Vec::new(),
            stack_ptr: TEST_SP,
            item_hit_ptr: 0,
            rendered_pixels: Vec::new(),
            flash_remaining: 0,
            flash_delay: 0,
            flash_item: 0,
            edit_text_modified: false,
            draw_proc_queue: VecDeque::new(),
            draw_procs_done: true,
            rendered_pixels_final: true,
            filter_proc: 0,
            game_managed: false,
            last_filter_event: None,
            popup_draws: Vec::new(),
            active_popup: None,
            active_button: None,
            active_user_item: None,
        });

        bus.write_word(TEST_SP, 9); // newItem
        bus.write_long(TEST_SP + 2, dialog_ptr); // theDialog
        bus.write_word(TEST_SP + 6, 0xBEEF); // OSErr result slot
        cpu.write_reg(Register::D0, 0x0304); // selector 4, 6 param bytes

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

        assert_eq!(bus.read_word(dialog_ptr + 168), 9);
        assert_eq!(
            disp.dialog_tracking.as_ref().map(|t| t.default_item),
            Some(9)
        );
        assert_eq!(bus.read_word(TEST_SP + 6), 0);
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
    }

    #[test]
    fn dialogdispatch_setdialogcancelitem_selector_05_updates_tracking_cancel_item_and_returns_noerr(
    ) {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(256);

        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
            dialog_ptr,
            bounds: (0, 0, 0, 0),
            title: String::new(),
            proc_id: 0,
            items: Vec::new(),
            default_item: 1,
            cancel_item: 2,
            edit_text: String::new(),
            edit_item: 0,
            saved_pixels: Vec::new(),
            stack_ptr: TEST_SP,
            item_hit_ptr: 0,
            rendered_pixels: Vec::new(),
            flash_remaining: 0,
            flash_delay: 0,
            flash_item: 0,
            edit_text_modified: false,
            draw_proc_queue: VecDeque::new(),
            draw_procs_done: true,
            rendered_pixels_final: true,
            filter_proc: 0,
            game_managed: false,
            last_filter_event: None,
            popup_draws: Vec::new(),
            active_popup: None,
            active_button: None,
            active_user_item: None,
        });

        bus.write_word(TEST_SP, 7); // newItem
        bus.write_long(TEST_SP + 2, dialog_ptr); // theDialog
        bus.write_word(TEST_SP + 6, 0xBEEF); // OSErr result slot
        cpu.write_reg(Register::D0, 0x0305); // selector 5, 6 param bytes

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

        assert_eq!(disp.dialog_cancel_items.get(&dialog_ptr), Some(&7));
        assert_eq!(
            disp.dialog_tracking.as_ref().map(|t| t.cancel_item),
            Some(7)
        );
        assert_eq!(bus.read_word(TEST_SP + 6), 0);
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
    }

    #[test]
    fn dialogdispatch_modal_dialog_first_entry_honors_preserved_default_and_cancel_items() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x200000u32;
        let item_hit_ptr = 0x300000u32;

        disp.front_window = dialog_ptr;
        disp.window_bounds = (92, 95, 240, 320);
        disp.window_proc_id = 1;
        disp.window_title = "AA68".to_string();
        disp.dialog_items.insert(
            dialog_ptr,
            vec![
                DialogItem {
                    item_type: 4,
                    rect: (20, 20, 40, 90),
                    text: "OK".to_string(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
                DialogItem {
                    item_type: 4,
                    rect: (20, 110, 40, 200),
                    text: "Cancel".to_string(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
            ],
        );
        bus.write_word(dialog_ptr + 168, 2);
        disp.dialog_cancel_items.insert(dialog_ptr, 1);
        bus.write_long(TEST_SP, item_hit_ptr);
        bus.write_long(TEST_SP + 4, 0);

        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);

        let tracking = disp.dialog_tracking.as_ref().unwrap();
        assert_eq!(tracking.default_item, 2);
        assert_eq!(tracking.cancel_item, 1);
    }

    #[test]
    fn dialogdispatch_setdialogtrackscursor_selector_06_returns_noerr_and_pops_arguments() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(256);

        bus.write_word(TEST_SP, 1); // tracks = TRUE
        bus.write_long(TEST_SP + 2, dialog_ptr); // theDialog
        bus.write_word(TEST_SP + 6, 0xBEEF); // OSErr result slot
        cpu.write_reg(Register::D0, 0x0306); // selector 6, 6 param bytes

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

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

    // ---- GetNewDialog ($A97C) ----

    #[test]
    fn get_new_dialog_missing_dlog_sets_reserr_and_returns_nil() {
        // MTE 1992 p. 6-114: GetNewDialog returns NIL when the DLOG
        // resource can't be read; the failed resource read surfaces through
        // Resource Manager ResErr as resNotFound (-192).
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_word(0x0A60, 0);

        let result = disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        let dlg_ptr = bus.read_long(TEST_SP + 10);
        assert_eq!(
            dlg_ptr, 0,
            "GetNewDialog must return NIL when DLOG is missing"
        );
        assert_eq!(bus.read_word(0x0A60) as i16, -192);
        assert!(disp.window_list.is_empty());
    }

    #[test]
    fn get_new_dialog_missing_ditl_sets_reserr_and_returns_nil() {
        // MTE 1992 p. 6-114: GetNewDialog also returns NIL when the item-list
        // resource named by the DLOG cannot be read.
        let (mut disp, mut cpu, mut bus) = setup();
        let dlog = build_test_dlog((10, 20, 110, 220), 1909, 0);
        disp.install_test_resource(&mut bus, *b"DLOG", 1908, &dlog);
        bus.write_word(0x0A60, 0);
        bus.write_word(TEST_SP + 8, 1908);

        let result = disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus);

        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        assert_eq!(bus.read_long(TEST_SP + 10), 0);
        assert_eq!(bus.read_word(0x0A60) as i16, -192);
        assert!(disp.window_list.is_empty());
        assert!(disp.dialog_items.is_empty());
    }

    #[test]
    fn get_new_dialog_nil_storage_allocates_low_byte_clean_dialog_record() {
        let (mut disp, mut cpu, mut bus) = setup();
        let screen_base = bus.alloc((640 * 480) as u32);
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 640, 640, 480, 8);

        let dlog = build_test_dlog((40, 50, 120, 240), 1911, 0);
        let ditl = build_test_ditl_item(4, (50, 80, 70, 140), b"OK");
        disp.install_test_resource(&mut bus, *b"DLOG", 1910, &dlog);
        disp.install_test_resource(&mut bus, *b"DITL", 1911, &ditl);
        let skew = bus.alloc(5);
        assert_ne!(
            (skew + MacMemoryBus::allocation_bucket_size(5)) & 0xFF,
            0,
            "test precondition should leave the heap skewed before GetNewDialog"
        );

        bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind
        bus.write_long(TEST_SP + 4, 0); // dStorage = NIL
        bus.write_word(TEST_SP + 8, 1910);
        bus.write_long(TEST_SP + 10, 0xDEAD_BEEF);

        disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        let dialog_ptr = bus.read_long(TEST_SP + 10);
        assert_ne!(dialog_ptr, 0);
        assert_eq!(
            dialog_ptr & 0xFF,
            0,
            "manager-owned DialogRecord pointers should keep the low byte clear"
        );
        assert_eq!(
            bus.get_alloc_size(dialog_ptr),
            Some(170),
            "DialogRecord allocation should keep its logical size"
        );
    }

    #[test]
    fn get_new_dialog_copies_ditl_not_aliases_resource() {
        // IM:I I-403 and MTE 1992 p. 6-114: GetNewDialog reads the DITL
        // resource, makes a copy, and uses that copy so several dialogs can
        // share identical resource-backed items without aliasing the resource.
        let (mut disp, mut cpu, mut bus) = setup();
        let screen_base = bus.alloc((640 * 480) as u32);
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 640, 640, 480, 8);

        let dlog = build_test_dlog((40, 50, 110, 230), 2201, 0);
        let ditl = build_test_ditl_item(8, (10, 12, 28, 120), b"Shared");
        disp.install_test_resource(&mut bus, *b"DLOG", 2200, &dlog);
        let original_ditl_ptr = disp.install_test_resource(&mut bus, *b"DITL", 2201, &ditl);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind
        bus.write_long(TEST_SP + 4, 0); // dStorage
        bus.write_word(TEST_SP + 8, 2200); // dialogID
        bus.write_long(TEST_SP + 10, 0xDEAD_BEEF);
        disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let first_dialog = bus.read_long(TEST_SP + 10);
        let first_ditl_ptr = bus.read_long(bus.read_long(first_dialog + 156));

        assert_ne!(first_dialog, 0);
        assert_ne!(first_ditl_ptr, 0);
        assert_ne!(first_ditl_ptr, original_ditl_ptr);
        assert_eq!(ditl_item_handle_field(&bus, original_ditl_ptr, 1), 0);
        assert_ne!(ditl_item_handle_field(&bus, first_ditl_ptr, 1), 0);

        bus.write_long(first_ditl_ptr + 2, 0xAABB_CCDD);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind
        bus.write_long(TEST_SP + 4, 0); // dStorage
        bus.write_word(TEST_SP + 8, 2200); // dialogID
        bus.write_long(TEST_SP + 10, 0xDEAD_BEEF);
        disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let second_dialog = bus.read_long(TEST_SP + 10);
        let second_ditl_ptr = bus.read_long(bus.read_long(second_dialog + 156));
        let second_text_handle = ditl_item_handle_field(&bus, second_ditl_ptr, 1);

        assert_ne!(second_dialog, 0);
        assert_ne!(second_ditl_ptr, original_ditl_ptr);
        assert_ne!(second_ditl_ptr, first_ditl_ptr);
        assert_ne!(second_text_handle, 0);
        assert_ne!(second_text_handle, 0xAABB_CCDD);
        assert_eq!(ditl_item_handle_field(&bus, original_ditl_ptr, 1), 0);
    }

    #[test]
    fn get_new_dialog_rewrites_reserved_item_handles() {
        // IM:I I-405: an item list in memory contains item handles for text,
        // controls, icons, and pictures. GetNewDialog must rewrite the copied
        // DITL's reserved handle fields, not the resource's fields.
        let (mut disp, mut cpu, mut bus) = setup();
        let screen_base = bus.alloc((640 * 480) as u32);
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 640, 640, 480, 8);

        let cntl_id = 2301i16.to_be_bytes();
        let icon_id = 2302i16.to_be_bytes();
        let pict_id = 2303i16.to_be_bytes();
        let dlog = build_test_dlog((40, 50, 130, 260), 2300, 0);
        let ditl = build_test_ditl_items(&[
            (8, (10, 12, 24, 140), b"Static".as_slice()),
            (16, (30, 12, 44, 140), b"Edit".as_slice()),
            (7, (50, 12, 70, 120), cntl_id.as_slice()),
            (32, (10, 150, 42, 182), icon_id.as_slice()),
            (64, (48, 150, 80, 220), pict_id.as_slice()),
        ]);

        let mut cntl = Vec::new();
        for word in [0i16, 0, 20, 110, 1, -1, 3, 0, 0] {
            cntl.extend_from_slice(&(word as u16).to_be_bytes());
        }
        cntl.extend_from_slice(&0x1234_5678u32.to_be_bytes());
        cntl.push(4);
        cntl.extend_from_slice(b"Pick");
        disp.install_test_resource(&mut bus, *b"DLOG", 2299, &dlog);
        disp.install_test_resource(&mut bus, *b"CNTL", 2301, &cntl);
        let icon_ptr = disp.install_test_resource(&mut bus, *b"ICON", 2302, &[0xAA; 128]);
        let pict_ptr = disp.install_test_resource(&mut bus, *b"PICT", 2303, &[0x11; 32]);
        let original_ditl_ptr = disp.install_test_resource(&mut bus, *b"DITL", 2300, &ditl);

        bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind
        bus.write_long(TEST_SP + 4, 0); // dStorage
        bus.write_word(TEST_SP + 8, 2299); // dialogID
        disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        let dialog_ptr = bus.read_long(TEST_SP + 10);
        let copied_ditl_ptr = bus.read_long(bus.read_long(dialog_ptr + 156));
        assert_ne!(dialog_ptr, 0);
        assert_ne!(copied_ditl_ptr, original_ditl_ptr);

        for item_no in 1..=5 {
            assert_eq!(
                ditl_item_handle_field(&bus, original_ditl_ptr, item_no),
                0,
                "resource DITL item {} handle field must stay reserved",
                item_no
            );
        }

        let static_handle = ditl_item_handle_field(&bus, copied_ditl_ptr, 1);
        let edit_handle = ditl_item_handle_field(&bus, copied_ditl_ptr, 2);
        let control_handle = ditl_item_handle_field(&bus, copied_ditl_ptr, 3);
        let icon_handle = ditl_item_handle_field(&bus, copied_ditl_ptr, 4);
        let pict_handle = ditl_item_handle_field(&bus, copied_ditl_ptr, 5);

        assert_eq!(
            bus.read_bytes(bus.read_long(static_handle), 6),
            b"Static".to_vec()
        );
        assert_eq!(
            bus.read_bytes(bus.read_long(edit_handle), 4),
            b"Edit".to_vec()
        );
        assert_eq!(bus.read_long(bus.read_long(control_handle) + 4), dialog_ptr);
        assert_eq!(
            disp.dialog_control_handles.get(&control_handle),
            Some(&(dialog_ptr, 3))
        );
        assert_eq!(bus.read_long(icon_handle), icon_ptr);
        assert_eq!(bus.read_long(pict_handle), pict_ptr);
    }

    #[test]
    fn get_new_dialog_centers_standard_dlog_with_position_constant() {
        let (mut disp, mut cpu, mut bus) = setup();
        let screen_base = bus.alloc((800 * 600) as u32);
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 800, 800, 600, 8);

        let dlog = build_test_dlog((10, 20, 110, 220), 1503, 0x280A);
        let ditl = build_test_ditl_item(4, (20, 20, 40, 80), b"OK");
        disp.install_test_resource(&mut bus, *b"DLOG", 1502, &dlog);
        disp.install_test_resource(&mut bus, *b"DITL", 1503, &ditl);
        bus.write_long(TEST_SP, 0); // behind
        bus.write_long(TEST_SP + 4, 0); // dStorage
        bus.write_word(TEST_SP + 8, 1502); // dialogID

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

        let dlg_ptr = bus.read_long(TEST_SP + 10);
        assert_ne!(dlg_ptr, 0);
        assert_eq!(disp.window_bounds, (250, 300, 350, 500));
    }

    #[test]
    fn get_new_dialog_uses_system7_alert_position_near_top() {
        // MTE 1992 p. 4-126: alert position leaves about one-fifth of
        // the unused vertical screen space above the new window. EVO's
        // startup registration DLOG has these dimensions; a one-third
        // placement draws it visibly too low compared with BasiliskII.
        let (mut disp, mut cpu, mut bus) = setup();
        let screen_base = bus.alloc((800 * 600) as u32);
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 800, 800, 600, 8);

        let dlog = build_test_dlog((40, 40, 310, 530), 1513, 0x300A);
        let ditl = build_test_ditl_item(4, (241, 291, 261, 381), b"Not Yet");
        disp.install_test_resource(&mut bus, *b"DLOG", 1512, &dlog);
        disp.install_test_resource(&mut bus, *b"DITL", 1513, &ditl);
        bus.write_long(TEST_SP, 0); // behind
        bus.write_long(TEST_SP + 4, 0); // dStorage
        bus.write_word(TEST_SP + 8, 1512); // dialogID

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

        let dlg_ptr = bus.read_long(TEST_SP + 10);
        assert_ne!(dlg_ptr, 0);
        assert_eq!(disp.window_bounds, (66, 155, 336, 645));
    }

    #[test]
    fn get_new_dialog_draws_visible_shell_with_unresolved_user_item_proc() {
        // Visible dialogs appear immediately on a real Mac. userItem contents
        // are application-owned and may be installed later with SetDItem, but
        // the dialog shell and standard items must not be suppressed.
        // Inside Macintosh Volume I, I-405, I-412, I-421.
        let (mut disp, mut cpu, mut bus) = setup();
        let screen_base = bus.alloc((800 * 600) as u32);
        bus.write_bytes(screen_base, &vec![0x77; 800 * 600]);
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 800, 800, 600, 8);

        let mut dlog = build_test_dlog((100, 100, 180, 260), 1701, 0);
        dlog[10] = 1; // visible
        let ditl = build_test_ditl_items(&[
            (0x80, (8, 8, 30, 80), b"".as_slice()),
            (4, (44, 20, 64, 90), b"OK".as_slice()),
        ]);
        disp.install_test_resource(&mut bus, *b"DLOG", 1700, &dlog);
        disp.install_test_resource(&mut bus, *b"DITL", 1701, &ditl);
        bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind
        bus.write_long(TEST_SP + 4, 0); // dStorage
        bus.write_word(TEST_SP + 8, 1700); // dialogID

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

        let dlg_ptr = bus.read_long(TEST_SP + 10);
        assert_ne!(dlg_ptr, 0);
        assert_eq!(bus.read_byte(dlg_ptr + 110), 0xFF);
        assert!(disp.dialog_items.contains_key(&dlg_ptr));
        assert!(
            disp.dialog_saved_pixels.contains_key(&dlg_ptr),
            "visible dialogs save the covered pixels before drawing"
        );
        assert!(
            disp.dialog_visible_snapshots.contains_key(&dlg_ptr),
            "visible dialogs retain their clean initial shell for first ModalDialog entry"
        );
        assert!(!disp.dialog_initial_draw_deferred.contains(&dlg_ptr));
        assert!(
            disp.event_queue
                .iter()
                .any(|event| event.what == 6 && event.message == dlg_ptr),
            "visible dialog should still get an update event"
        );

        let probe_addr = screen_base + 120 * 800 + 120;
        assert_ne!(
            bus.read_byte(probe_addr),
            0x77,
            "visible GetNewDialog should draw the standard dialog background immediately"
        );
    }

    #[test]
    fn hidden_dialog_quickdraw_shape_is_clipped_by_empty_vis_region() {
        // A real invisible window has an empty visRgn. Drawing through its
        // port must be clipped away instead of touching the screen-backed
        // PixMap. This covers hidden dialogs whose userItem setup draws into
        // the current dialog port before the dialog is ever shown.
        let (mut disp, mut cpu, mut bus) = setup();
        let screen_base = bus.alloc((800 * 600) as u32);
        bus.write_bytes(screen_base, &vec![0x77; 800 * 600]);
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 800, 800, 600, 8);

        let dlog = build_test_dlog((100, 100, 180, 260), 1705, 0);
        let ditl = build_test_ditl_item(8, (8, 8, 24, 90), b"Hidden");
        disp.install_test_resource(&mut bus, *b"DLOG", 1704, &dlog);
        disp.install_test_resource(&mut bus, *b"DITL", 1705, &ditl);
        bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind
        bus.write_long(TEST_SP + 4, 0); // dStorage
        bus.write_word(TEST_SP + 8, 1704); // dialogID

        let result = disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        let dlg_ptr = bus.read_long(TEST_SP + 10);
        assert_ne!(dlg_ptr, 0);
        assert_eq!(bus.read_byte(dlg_ptr + 110), 0);
        assert_eq!(
            TrapDispatcher::region_handle_rect(&bus, bus.read_long(dlg_ptr + 24)),
            None
        );

        let rect_ptr = bus.alloc(8);
        bus.write_word(rect_ptr, 20);
        bus.write_word(rect_ptr + 2, 20);
        bus.write_word(rect_ptr + 4, 30);
        bus.write_word(rect_ptr + 6, 30);
        bus.write_long(TEST_SP, rect_ptr);

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

        let probe_addr = screen_base + 125 * 800 + 125;
        assert_eq!(
            bus.read_byte(probe_addr),
            0x77,
            "QuickDraw drawing through a hidden dialog port must be clipped"
        );
        assert!(
            !disp.dialog_saved_pixels.contains_key(&dlg_ptr),
            "no visible draw means no saved-under snapshot"
        );
    }

    #[test]
    fn dispos_dialog_skips_saved_pixels_for_never_drawn_deferred_dialog() {
        // Defensive coverage for the deferred-state guard: if a dialog is
        // marked never drawn, DisposDialog must not restore stale saved-under
        // pixels. Real visible dialogs normally draw at creation.
        // Inside Macintosh Volume I, I-425.
        let (mut disp, mut cpu, mut bus) = setup();
        let screen_base = bus.alloc((800 * 600) as u32);
        bus.write_bytes(screen_base, &vec![0x77; 800 * 600]);
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 800, 800, 600, 8);

        let mut dlog = build_test_dlog((100, 100, 180, 260), 1703, 0);
        dlog[10] = 1; // visible
        let ditl = build_test_ditl_items(&[
            (0x80, (8, 8, 30, 80), b"".as_slice()),
            (4, (44, 20, 64, 90), b"OK".as_slice()),
        ]);
        disp.install_test_resource(&mut bus, *b"DLOG", 1702, &dlog);
        disp.install_test_resource(&mut bus, *b"DITL", 1703, &ditl);
        bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind
        bus.write_long(TEST_SP + 4, 0); // dStorage
        bus.write_word(TEST_SP + 8, 1702); // dialogID

        let result = disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        let dlg_ptr = bus.read_long(TEST_SP + 10);
        disp.dialog_initial_draw_deferred.insert(dlg_ptr);

        // Replace the saved snapshot with a distinct dirty pattern. If
        // DisposDialog restores it, the probe byte will change to 0x33.
        disp.dialog_saved_pixels
            .insert(dlg_ptr, vec![0x33; 90 * 170]);
        let probe_addr = screen_base + 120 * 800 + 120;
        bus.write_byte(probe_addr, 0x77);

        bus.write_long(TEST_SP, dlg_ptr);
        let result = disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        assert_eq!(
            bus.read_byte(probe_addr),
            0x77,
            "never-drawn deferred dialog must not restore saved background"
        );
        assert!(!disp.dialog_initial_draw_deferred.contains(&dlg_ptr));
        assert!(!disp.dialog_saved_pixels.contains_key(&dlg_ptr));
    }

    // save_dialog_pixels / restore_dialog_pixels must guard against off-screen y
    // (negative after the dBoxProc structure margin, or beyond screen_h).
    // Without the guard, (y as u32) sign-extends a negative i16 and
    // multiply-with-overflow panics in debug.
    #[test]
    fn save_dialog_pixels_handles_top_below_dbox_margin_without_overflow() {
        let (disp, _cpu, mut bus) = setup();
        let screen_base = bus.alloc((800 * 600) as u32);
        for i in 0..800u32 * 600 {
            bus.write_byte(screen_base + i, 0x42);
        }
        let mut d = disp;
        bus.write_long(0x0824, screen_base);
        d.screen_mode = (screen_base, 800, 800, 600, 8);

        // Bounds with top=2 → save_top = -6 (negative).
        let saved = d.save_dialog_pixels(&bus, (2, 2, 50, 50));
        // Row width = (58 - (-6)) = 64; row count = 64.
        let row_width = 64usize;
        let row_count = 64usize;
        assert_eq!(saved.len(), row_width * row_count);

        // First 6 rows are off-screen (y = -6..-1) → zero-padded.
        for row in 0..6 {
            for col in 0..row_width {
                assert_eq!(
                    saved[row * row_width + col],
                    0x00,
                    "off-screen row {} col {} must be zero-padded",
                    row,
                    col
                );
            }
        }
        // y=0 row, save_left=-6 so cols 0..6 are off-screen → 0,
        // cols 6..64 read from the 0x42-filled framebuffer.
        let row0 = &saved[6 * row_width..7 * row_width];
        for (col, &px) in row0.iter().enumerate().take(6) {
            assert_eq!(
                px, 0x00,
                "off-screen column {} within on-screen row must be zero",
                col
            );
        }
        for (col, &px) in row0.iter().enumerate().skip(6) {
            assert_eq!(
                px, 0x42,
                "on-screen pixel at col {} must round-trip via framebuffer",
                col
            );
        }
    }

    // save_rect_pixels / restore_rect_pixels guard the same off-screen y
    // overflow hazard as save_dialog_pixels.
    #[test]
    fn save_rect_pixels_handles_negative_top_without_overflow() {
        let (disp, _cpu, mut bus) = setup();
        let screen_base = bus.alloc((800 * 600) as u32);
        for i in 0..800u32 * 600 {
            bus.write_byte(screen_base + i, 0x55);
        }
        let mut d = disp;
        bus.write_long(0x0824, screen_base);
        d.screen_mode = (screen_base, 800, 800, 600, 8);

        // Rect spanning above the screen top.
        let saved = d.save_rect_pixels(&bus, (-3, 0, 5, 10));
        let row_width = 10usize;
        let row_count = (5 - (-3)) as usize;
        assert_eq!(saved.len(), row_width * row_count);
        // First 3 rows off-screen (y = -3..0) → zero-padded.
        for row in 0..3 {
            for col in 0..row_width {
                assert_eq!(saved[row * row_width + col], 0x00);
            }
        }
        // Next 5 rows on-screen → 0x55.
        for row in 3..8 {
            for col in 0..row_width {
                assert_eq!(saved[row * row_width + col], 0x55);
            }
        }
    }

    #[test]
    fn save_dialog_pixels_handles_top_above_screen_height_without_overflow() {
        let (disp, _cpu, mut bus) = setup();
        let screen_base = bus.alloc((800 * 600) as u32);
        let mut d = disp;
        bus.write_long(0x0824, screen_base);
        d.screen_mode = (screen_base, 800, 800, 600, 8);

        // Bounds way below screen → save_top = 595+ and save_bottom
        // = 700+, many rows off-screen on the bottom side.
        let saved = d.save_dialog_pixels(&bus, (600, 0, 700, 50));
        let row_count = (700 + TrapDispatcher::DBOX_FRAME_MARGIN
            - (600 - TrapDispatcher::DBOX_FRAME_MARGIN)) as usize;
        let row_width = (50 + TrapDispatcher::DBOX_FRAME_MARGIN
            - (0 - TrapDispatcher::DBOX_FRAME_MARGIN)) as usize;
        assert_eq!(saved.len(), row_count * row_width);
    }

    fn install_new_dialog_test_screen(
        disp: &mut TrapDispatcher,
        bus: &mut MacMemoryBus,
        fill: u8,
    ) -> u32 {
        let screen_base = bus.alloc((640 * 480) as u32);
        bus.write_bytes(screen_base, &vec![fill; 640 * 480]);
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 640, 640, 480, 8);
        screen_base
    }

    fn call_new_dialog_for_test(
        disp: &mut TrapDispatcher,
        cpu: &mut MockCpu,
        bus: &mut MacMemoryBus,
        storage_ptr: u32,
        bounds: (i16, i16, i16, i16),
        visible: bool,
        proc_id: i16,
        behind: u32,
        items_handle: u32,
    ) -> (u32, u32) {
        let title_ptr = bus.alloc(32);
        bus.write_pstring(title_ptr, b"Lifecycle");
        let bounds_ptr = bus.alloc(8);
        bus.write_word(bounds_ptr, bounds.0 as u16);
        bus.write_word(bounds_ptr + 2, bounds.1 as u16);
        bus.write_word(bounds_ptr + 4, bounds.2 as u16);
        bus.write_word(bounds_ptr + 6, bounds.3 as u16);

        let sp = TEST_SP - 30;
        cpu.write_reg(Register::A7, sp);
        for offset in 0..34u32 {
            bus.write_byte(sp + offset, 0);
        }
        bus.write_long(sp, items_handle);
        bus.write_long(sp + 4, 0x1234_5678); // refCon
        bus.write_byte(sp + 8, 0xFF); // goAwayFlag
        bus.write_long(sp + 10, behind);
        bus.write_word(sp + 14, proc_id as u16);
        bus.write_byte(sp + 16, if visible { 0xFF } else { 0 });
        bus.write_long(sp + 18, title_ptr);
        bus.write_long(sp + 22, bounds_ptr);
        bus.write_long(sp + 26, storage_ptr);
        bus.write_long(sp + 30, 0xDEAD_BEEF);

        disp.dispatch_dialog(true, 0x17D, cpu, bus)
            .unwrap()
            .unwrap();
        (sp, bus.read_long(sp + 30))
    }

    fn install_ditl_handle_for_test(bus: &mut MacMemoryBus, ditl: &[u8]) -> (u32, u32) {
        let items_ptr = bus.alloc(ditl.len() as u32);
        bus.write_bytes(items_ptr, ditl);
        let items_handle = bus.alloc(4);
        bus.write_long(items_handle, items_ptr);
        (items_handle, items_ptr)
    }

    #[test]
    fn new_dialog_with_storage_uses_caller_record() {
        // IM:I I-412: dStorage supplies the DialogRecord storage; NIL asks
        // the Dialog Manager to allocate the record instead.
        let (mut disp, mut cpu, mut bus) = setup();
        install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
        let storage_ptr = bus.alloc(170);

        let (sp, dialog_ptr) = call_new_dialog_for_test(
            &mut disp,
            &mut cpu,
            &mut bus,
            storage_ptr,
            (80, 90, 150, 260),
            false,
            4,
            0xFFFF_FFFF,
            0,
        );

        assert_eq!(dialog_ptr, storage_ptr);
        assert_eq!(bus.read_long(sp + 30), storage_ptr);
        assert_eq!(cpu.read_reg(Register::A7), sp + 30);
        assert_eq!(disp.window_list, vec![storage_ptr]);
    }

    #[test]
    fn new_dialog_without_storage_allocates_record() {
        // IM:I I-412 and MTE 1992 p. 6-118: passing NIL for dStorage
        // allocates the DialogRecord in the heap.
        let (mut disp, mut cpu, mut bus) = setup();
        install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);

        let (_sp, dialog_ptr) = call_new_dialog_for_test(
            &mut disp,
            &mut cpu,
            &mut bus,
            0,
            (80, 90, 150, 260),
            false,
            2,
            0xFFFF_FFFF,
            0,
        );

        assert_ne!(dialog_ptr, 0);
        assert_eq!(bus.get_alloc_size(dialog_ptr), Some(170));
        assert_eq!(disp.window_list, vec![dialog_ptr]);
    }

    #[test]
    fn new_dialog_sets_window_kind_dialog_kind() {
        // IM:I I-407 and I-412: a DialogPtr is a WindowPtr whose
        // WindowRecord.windowKind is dialogKind, independent of the WDEF
        // procID used to draw modeless or modal chrome.
        let (mut disp, mut cpu, mut bus) = setup();
        install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);

        let (_sp, dialog_ptr) = call_new_dialog_for_test(
            &mut disp,
            &mut cpu,
            &mut bus,
            0,
            (80, 90, 150, 260),
            false,
            4,
            0xFFFF_FFFF,
            0,
        );

        assert_eq!(bus.read_word(dialog_ptr + 108) as i16, 2);
        assert_eq!(disp.window_proc_ids.get(&dialog_ptr), Some(&4));
    }

    #[test]
    fn new_dialog_sets_dialog_port_font_to_dialog_font() {
        // IM:I I-412 and MTE 1992 p. 6-104: SetDAFont/SetDialogFont set
        // DlgFont for subsequently created dialog and alert grafPorts.
        let (mut disp, mut cpu, mut bus) = setup();
        install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
        bus.write_word(crate::memory::globals::addr::DLG_FONT, 3); // Geneva

        let (_sp, dialog_ptr) = call_new_dialog_for_test(
            &mut disp,
            &mut cpu,
            &mut bus,
            0,
            (80, 90, 150, 260),
            false,
            2,
            0xFFFF_FFFF,
            0,
        );

        assert_eq!(bus.read_word(dialog_ptr + 68) as i16, 3);
        assert_eq!(disp.tx_font, 3);
        assert_eq!(
            disp.port_draw_states
                .get(&dialog_ptr)
                .map(|state| state.tx_font),
            Some(3)
        );
    }

    #[test]
    fn new_dialog_visible_queues_or_draws_expected_update_path() {
        // IM:I I-412 and I-287: visible NewDialog draws the dialog window and
        // generates an update event for the contents.
        let (mut disp, mut cpu, mut bus) = setup();
        let screen_base = install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);

        let (_sp, dialog_ptr) = call_new_dialog_for_test(
            &mut disp,
            &mut cpu,
            &mut bus,
            0,
            (80, 90, 150, 260),
            true,
            2,
            0xFFFF_FFFF,
            0,
        );

        assert_eq!(bus.read_byte(dialog_ptr + 110), 0xFF);
        assert!(disp
            .event_queue
            .iter()
            .any(|event| event.what == 6 && event.message == dialog_ptr));
        assert!(
            TrapDispatcher::region_handle_rect(&bus, bus.read_long(dialog_ptr + 122)).is_some()
        );
        assert_ne!(bus.read_byte(screen_base + 100 * 640 + 110), 0x77);
    }

    #[test]
    fn new_dialog_invisible_defers_screen_pixels() {
        // IM:I I-412: if visible is FALSE, the window is initially invisible
        // and may later be shown with ShowWindow.
        let (mut disp, mut cpu, mut bus) = setup();
        let screen_base = install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);

        let (_sp, dialog_ptr) = call_new_dialog_for_test(
            &mut disp,
            &mut cpu,
            &mut bus,
            0,
            (80, 90, 150, 260),
            false,
            2,
            0xFFFF_FFFF,
            0,
        );

        assert_eq!(bus.read_byte(dialog_ptr + 110), 0);
        assert!(!disp
            .event_queue
            .iter()
            .any(|event| event.what == 6 && event.message == dialog_ptr));
        assert_eq!(
            TrapDispatcher::region_handle_rect(&bus, bus.read_long(dialog_ptr + 122)),
            None
        );
        assert_eq!(bus.read_byte(screen_base + 100 * 640 + 110), 0x77);
    }

    #[test]
    fn new_dialog_honors_behind_nil_and_inserts_at_back() {
        // Inside Macintosh Volume I, I-412: NewDialog returns a DialogPtr
        // and honors the `behind` parameter for plane order.
        let (mut disp, mut cpu, mut bus) = setup();
        // Seed an existing window that will stay in front.
        let existing = 0x200040u32;
        disp.window_list = vec![existing];
        disp.front_window = existing;
        bus.write_byte(existing + 110u32, 0xFF); // visible

        // init_cgraf_window reads screen_mode for bounds math; a
        // zero-initialized mode multiplies-with-overflow later.
        let screen_base = bus.alloc((800 * 600) as u32);
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 800, 800, 600, 8);

        // Bounds at the origin are now safe via the off-screen y overflow guard
        // in save_dialog_pixels. Keep a non-origin placement for realism.
        let bounds_rect_ptr = 0x301200u32;
        bus.write_word(bounds_rect_ptr, 100);
        bus.write_word(bounds_rect_ptr + 2, 100);
        bus.write_word(bounds_rect_ptr + 4, 300);
        bus.write_word(bounds_rect_ptr + 6, 400);

        let sp = TEST_SP - 30;
        cpu.write_reg(Register::A7, sp);
        for i in 0..34u32 {
            bus.write_byte(sp + i, 0);
        }
        bus.write_long(sp + 22, bounds_rect_ptr);
        bus.write_word(sp + 16, 1); // visible
        bus.write_word(sp + 14, 1); // dBoxProc WDEF
        bus.write_long(sp + 10, 0); // behind = NIL (backmost)

        let pre_a7 = cpu.read_reg(Register::A7);
        let result = disp.dispatch_dialog(true, 0x17D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            cpu.read_reg(Register::A7),
            pre_a7 + 30,
            "NewDialog must pop 30 bytes of parameters and leave result at new SP+0"
        );
        let dlg_ptr = bus.read_long(sp + 30);
        assert_ne!(dlg_ptr, 0);
        assert_eq!(
            bus.read_word(dlg_ptr + 108),
            2,
            "DialogRecord.window.windowKind must be dialogKind"
        );
        assert_eq!(
            disp.window_proc_ids.get(&dlg_ptr),
            Some(&1),
            "dialog WDEF procID must be tracked separately from windowKind"
        );
        assert_eq!(
            disp.window_list,
            vec![existing, dlg_ptr],
            "NewDialog(behind=NIL) must insert dialog at the back"
        );
        assert_eq!(
            disp.front_window, existing,
            "front must stay on the pre-existing visible window"
        );
    }

    #[test]
    fn new_cdialog_honors_behind_nil_and_inserts_at_back() {
        // Inside Macintosh Volume V, V-243: NewCDialog follows the same
        // creation path as NewDialog but returns a color dialog pointer.
        let (mut disp, mut cpu, mut bus) = setup();
        let existing = 0x200040u32;
        disp.window_list = vec![existing];
        disp.front_window = existing;
        bus.write_byte(existing + 110u32, 0xFF); // visible

        let screen_base = bus.alloc((800 * 600) as u32);
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 800, 800, 600, 8);

        let bounds_rect_ptr = 0x301280u32;
        bus.write_word(bounds_rect_ptr, 120);
        bus.write_word(bounds_rect_ptr + 2, 120);
        bus.write_word(bounds_rect_ptr + 4, 320);
        bus.write_word(bounds_rect_ptr + 6, 420);

        let sp = TEST_SP - 30;
        cpu.write_reg(Register::A7, sp);
        for i in 0..34u32 {
            bus.write_byte(sp + i, 0);
        }
        bus.write_long(sp + 22, bounds_rect_ptr);
        bus.write_word(sp + 16, 1); // visible
        bus.write_long(sp + 10, 0); // behind = NIL (backmost)

        let pre_a7 = cpu.read_reg(Register::A7);
        let result = disp.dispatch_dialog(true, 0x24B, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            cpu.read_reg(Register::A7),
            pre_a7 + 30,
            "NewCDialog must pop 30 bytes of parameters and leave result at new SP+0"
        );
        let dlg_ptr = bus.read_long(sp + 30);
        assert_ne!(dlg_ptr, 0);
        assert_eq!(
            disp.window_list,
            vec![existing, dlg_ptr],
            "NewCDialog(behind=NIL) must insert dialog at the back"
        );
        assert_eq!(
            disp.front_window, existing,
            "front must stay on the pre-existing visible window"
        );
    }

    #[test]
    fn get_new_dialog_honors_behind_specific_window() {
        // GetNewDialog with no DLOG resource hits the fallback branch
        // that does NOT call finish_dialog_creation — so behind can't
        // reshuffle a list entry that was never added. This test
        // verifies the stack slot is at least READ without panicking.
        // The main contract test is new_dialog_honors_behind_nil_
        // and_inserts_at_back above, since that exercises the
        // primary post-finish_dialog_creation path.
        let (mut disp, mut cpu, mut bus) = setup();

        let sp = TEST_SP;
        // Write a specific non-trivial behind pointer at SP+0.
        bus.write_long(sp, 0xDEAD0000);

        let result = disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
    }

    // ---- SelectDialogItemText ($A97E) ----

    #[test]
    fn select_dialog_item_text_zero_to_32767_selects_entire_text_and_sets_editfield() {
        // Macintosh Toolbox Essentials 1992, 6-131: selecting the whole
        // editable text item uses strtSel=0 and endSel=32767.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        bus.write_word(dialog_ptr + 164, 0xFFFF); // editField = -1 (none)

        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 16,
                rect: (10, 20, 30, 40),
                text: "ABCDE".to_string(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        bus.write_word(TEST_SP, 32767u16); // endSel
        bus.write_word(TEST_SP + 2, 0); // strtSel
        bus.write_word(TEST_SP + 4, 1); // itemNo (1-based)
        bus.write_long(TEST_SP + 6, dialog_ptr); // theDialog

        let result = disp.dispatch_dialog(true, 0x17E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);

        let item = &disp.dialog_items[&dialog_ptr][0];
        assert_eq!(item.sel_start, 0);
        assert_eq!(item.sel_end, 5);
        assert_eq!(bus.read_word(dialog_ptr + 164), 0);
    }

    #[test]
    fn select_dialog_item_text_clamps_and_normalizes_selection_bounds() {
        // IM:I I-414 defines a [strtSel,endSel) range; callers can pass
        // out-of-range values, so HLE clamps to text bounds and normalizes
        // reversed inputs.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);

        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 16,
                rect: (10, 20, 30, 40),
                text: "ABCDE".to_string(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        bus.write_word(TEST_SP, 2); // endSel
        bus.write_word(TEST_SP + 2, 9); // strtSel (> text length)
        bus.write_word(TEST_SP + 4, 1); // itemNo
        bus.write_long(TEST_SP + 6, dialog_ptr); // theDialog

        let result = disp.dispatch_dialog(true, 0x17E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);

        let item = &disp.dialog_items[&dialog_ptr][0];
        assert_eq!(item.sel_start, 2);
        assert_eq!(item.sel_end, 5);
    }

    #[test]
    fn select_dialog_item_text_non_edit_item_is_noop() {
        // Macintosh Toolbox Essentials 1992, 6-131: selection applies to
        // editable text items only.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        bus.write_word(dialog_ptr + 164, 0x7FFF); // sentinel

        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 8, // statText
                rect: (10, 20, 30, 40),
                text: "STATIC".to_string(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 1,
                sel_end: 3,
            }],
        );

        bus.write_word(TEST_SP, 4); // endSel
        bus.write_word(TEST_SP + 2, 0); // strtSel
        bus.write_word(TEST_SP + 4, 1); // itemNo
        bus.write_long(TEST_SP + 6, dialog_ptr); // theDialog

        let result = disp.dispatch_dialog(true, 0x17E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);

        let item = &disp.dialog_items[&dialog_ptr][0];
        assert_eq!(item.sel_start, 1);
        assert_eq!(item.sel_end, 3);
        assert_eq!(bus.read_word(dialog_ptr + 164), 0x7FFF);
    }

    // ---- Alert ($A985) ----

    // Alert with no matching ALRT resource must return -1 per IM:I-412
    // ("Alert returns -1 and does nothing").
    #[test]
    fn alert_returns_minus_one_when_alrt_resource_missing() {
        let (mut disp, mut cpu, mut bus) = setup();

        // SP+4: alertID = 128 (no ALRT resource loaded in the test
        // dispatcher).
        bus.write_word(TEST_SP + 4, 128);
        let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(bus.read_word(TEST_SP + 6) as i16, -1);
    }

    // ---- te_line_origin_x (TextEdit alignment) ----
    //
    // Per IM:Text 1993 lines 7320-7323 and the MPW Universal Headers:
    // teJustLeft = 0, teJustCenter = 1, teJustRight = -1, teForceLeft = -2.

    #[test]
    fn te_line_origin_te_just_left_flush_left() {
        // teJustLeft (0): origin = box_left + TextEdit's left inset.
        assert_eq!(
            TrapDispatcher::te_line_origin_x(0, 20, 200, 80),
            21,
            "teJustLeft (0) must anchor one pixel inside box_left"
        );
    }

    #[test]
    fn te_line_origin_te_just_center_midpoint() {
        // teJustCenter (1): origin = box_left + (box_w - line_w) / 2.
        // (200-20 - 80) / 2 = 50 → origin = 20 + 50 = 70.
        assert_eq!(
            TrapDispatcher::te_line_origin_x(1, 20, 200, 80),
            70,
            "teJustCenter must midpoint the slack"
        );
    }

    #[test]
    fn te_line_origin_te_just_right_flush_right() {
        // teJustRight (-1): origin = box_right - line_width.
        // 200 - 80 = 120.
        assert_eq!(
            TrapDispatcher::te_line_origin_x(-1, 20, 200, 80),
            120,
            "teJustRight (-1) must anchor at box_right - line_width"
        );
    }

    #[test]
    fn te_line_origin_te_force_left_flush_left() {
        // teForceLeft (-2): origin = box_left + TextEdit's left inset
        // (overrides any
        // localised right-to-left default).
        assert_eq!(
            TrapDispatcher::te_line_origin_x(-2, 20, 200, 80),
            21,
            "teForceLeft (-2) must anchor one pixel inside box_left"
        );
    }

    #[test]
    fn te_line_origin_te_just_system_defaults_left() {
        // teJustSystem (0): localised default = left for LTR, with the
        // same TextEdit left inset.
        assert_eq!(
            TrapDispatcher::te_line_origin_x(0, 20, 200, 80),
            21,
            "teJustSystem must default to flush left inside box_left"
        );
    }

    // ---- StopAlert ($A986) ----

    #[test]
    fn stop_alert_returns_minus_one_when_alrt_resource_missing() {
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 2);
        bus.write_word(crate::memory::globals::addr::ANUMBER, 0xC0DE);
        bus.write_word(TEST_SP + 4, 128);
        let result = disp.dispatch_dialog(true, 0x186, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(bus.read_word(TEST_SP + 6) as i16, -1);
        assert_eq!(bus.read_word(crate::memory::globals::addr::ALERT_STAGE), 2);
        assert_eq!(bus.read_word(crate::memory::globals::addr::ANUMBER), 0xC0DE);
    }

    // ---- NoteAlert ($A987) ----

    #[test]
    fn note_alert_returns_minus_one_when_alrt_resource_missing() {
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 1);
        bus.write_word(crate::memory::globals::addr::ANUMBER, 0xBEEF);
        bus.write_word(TEST_SP + 4, 128);
        let result = disp.dispatch_dialog(true, 0x187, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(bus.read_word(TEST_SP + 6) as i16, -1);
        assert_eq!(bus.read_word(crate::memory::globals::addr::ALERT_STAGE), 1);
        assert_eq!(bus.read_word(crate::memory::globals::addr::ANUMBER), 0xBEEF);
    }

    // ---- CautionAlert ($A988) ----

    #[test]
    fn caution_alert_returns_minus_one_when_alrt_resource_missing() {
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 3);
        bus.write_word(crate::memory::globals::addr::ANUMBER, 0xFACE);
        bus.write_word(TEST_SP + 4, 128);
        let result = disp.dispatch_dialog(true, 0x188, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(bus.read_word(TEST_SP + 6) as i16, -1);
        assert_eq!(bus.read_word(crate::memory::globals::addr::ALERT_STAGE), 3);
        assert_eq!(bus.read_word(crate::memory::globals::addr::ANUMBER), 0xFACE);
    }

    // ---- Alert family stages-driven default item (IM:I I-417 / I-422) ----
    //
    // Each test installs an ALRT resource with a controlled
    // 16-byte template (8-byte boundsRect + 2-byte itemsID + 2-byte
    // stages + padding) and verifies the trap returns the bold
    // (default) item for the current AlertStage low-mem byte.
    //
    // ALRT stages encoding (IM:I I-422): 16-bit word, 4 nibbles
    // (low to high = stage 1..4). Within each nibble, bit 3 is
    // boldItmNum (`okDismissal = 8`), bit 2 is boxDrwn (`alBit = 4`),
    // and bits 0..1 are the sound number.

    /// Build a 12-byte ALRT template: bounds=(0,0,80,200) +
    /// itemsID + stages + 0 padding. Real ALRTs are typically
    /// 12 bytes; we install at least 12.
    fn build_alrt_template(items_id: i16, stages: u16) -> Vec<u8> {
        let mut v = Vec::with_capacity(12);
        v.extend_from_slice(&0u16.to_be_bytes()); // top
        v.extend_from_slice(&0u16.to_be_bytes()); // left
        v.extend_from_slice(&80u16.to_be_bytes()); // bottom
        v.extend_from_slice(&200u16.to_be_bytes()); // right
        v.extend_from_slice(&items_id.to_be_bytes()); // itemsID
        v.extend_from_slice(&stages.to_be_bytes()); // stages
        v
    }

    // ---- CouldDialog ($A979) / FreeDialog ($A97A) ----

    #[test]
    fn could_dialog_present_resource_sets_reserr_noerr() {
        // Inside Macintosh Volume I, I-415: CouldDialog targets a DLOG by
        // resource ID and prepares it for later use. BasiliskII leaves
        // ResErr at noErr on the missing-resource path.
        let (mut disp, mut cpu, mut bus) = setup();
        let dlog = vec![0u8; 20];
        disp.install_test_resource(&mut bus, *b"DLOG", 300, &dlog);
        bus.write_word(0x0A60, 0x7FFF);
        bus.write_word(TEST_SP, 300);

        let pre_a7 = cpu.read_reg(Register::A7);
        let result = disp.dispatch_dialog(true, 0x179, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
        assert_eq!(bus.read_word(0x0A60) as i16, 0);
    }

    #[test]
    fn could_dialog_missing_resource_sets_reserr_resnotfound() {
        // BasiliskII leaves ResErr at noErr even when the DLOG is missing.
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_word(0x0A60, 0);
        bus.write_word(TEST_SP, 301);

        let pre_a7 = cpu.read_reg(Register::A7);
        let result = disp.dispatch_dialog(true, 0x179, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
        assert_eq!(bus.read_word(0x0A60) as i16, 0);
    }

    #[test]
    fn free_dialog_present_resource_sets_reserr_noerr() {
        // Inside Macintosh Volume I, I-415: FreeDialog reverses CouldDialog
        // for previously targeted DLOG templates. BasiliskII leaves
        // ResErr at noErr on the missing-resource path.
        let (mut disp, mut cpu, mut bus) = setup();
        let dlog = vec![0u8; 20];
        disp.install_test_resource(&mut bus, *b"DLOG", 302, &dlog);
        bus.write_word(0x0A60, 0x7FFF);
        bus.write_word(TEST_SP, 302);

        let pre_a7 = cpu.read_reg(Register::A7);
        let result = disp.dispatch_dialog(true, 0x17A, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
        assert_eq!(bus.read_word(0x0A60) as i16, 0);
    }

    #[test]
    fn free_dialog_missing_resource_sets_reserr_resnotfound() {
        // BasiliskII leaves ResErr at noErr even when the DLOG is missing.
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_word(0x0A60, 0);
        bus.write_word(TEST_SP, 303);

        let pre_a7 = cpu.read_reg(Register::A7);
        let result = disp.dispatch_dialog(true, 0x17A, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
        assert_eq!(bus.read_word(0x0A60) as i16, 0);
    }

    #[test]
    fn could_dialog_free_dialog_purgeability() {
        // IM:I I-415: CouldDialog reads the dialog resource family into
        // memory if needed and makes it unpurgeable; FreeDialog reverses
        // the purgeability state for those already-loaded resources.
        let (mut disp, mut cpu, mut bus) = setup();
        let dlog_id: i16 = 3100;
        let ditl_id: i16 = 3101;
        let cntl_id: i16 = 3102;
        let icon_id: i16 = 3103;
        let pict_id: i16 = 3104;
        let dlog = build_test_dlog((10, 20, 110, 220), ditl_id, 0);
        let ditl = build_test_ditl_items(&[
            (7, (1, 2, 12, 82), &cntl_id.to_be_bytes()),
            (32, (14, 2, 46, 34), &icon_id.to_be_bytes()),
            (64, (48, 2, 88, 82), &pict_id.to_be_bytes()),
        ]);
        disp.install_test_resource(&mut bus, *b"DLOG", dlog_id, &dlog);
        disp.install_test_resource(&mut bus, *b"DITL", ditl_id, &ditl);
        disp.install_test_resource(&mut bus, *b"CNTL", cntl_id, &[0u8; 32]);
        disp.install_test_resource(&mut bus, *b"ICON", icon_id, &[0xAA; 128]);
        disp.install_test_resource(&mut bus, *b"PICT", pict_id, &[0x11; 32]);
        disp.res_load = false;

        bus.write_word(0x0A60, 0x7FFF);
        bus.write_word(TEST_SP, dlog_id as u16);
        let pre_a7 = cpu.read_reg(Register::A7);
        assert!(disp
            .dispatch_dialog(true, 0x179, &mut cpu, &mut bus)
            .unwrap()
            .is_ok());
        assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
        assert_eq!(bus.read_word(0x0A60) as i16, 0);
        for (res_type, res_id) in [
            (*b"DLOG", dlog_id),
            (*b"DITL", ditl_id),
            (*b"CNTL", cntl_id),
            (*b"ICON", icon_id),
            (*b"PICT", pict_id),
        ] {
            assert_resource_nonpurgeable(&disp, &bus, res_type, res_id);
        }

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, dlog_id as u16);
        assert!(disp
            .dispatch_dialog(true, 0x17A, &mut cpu, &mut bus)
            .unwrap()
            .is_ok());
        assert_eq!(bus.read_word(0x0A60) as i16, 0);
        for (res_type, res_id) in [
            (*b"DLOG", dlog_id),
            (*b"DITL", ditl_id),
            (*b"CNTL", cntl_id),
            (*b"ICON", icon_id),
            (*b"PICT", pict_id),
        ] {
            assert_resource_purgeable(&disp, res_type, res_id);
        }
    }

    // ---- CouldAlert ($A989) / FreeAlert ($A98A) ----

    #[test]
    fn could_alert_present_resource_sets_reserr_noerr() {
        // IM:I I-420: CouldAlert targets an ALRT template by ID.
        // Systemless's HLE compromise writes Resource Manager error
        // state at ResErr ($0A60): noErr for present resources.
        let (mut disp, mut cpu, mut bus) = setup();
        let alrt = build_alrt_template(200, 0x0000);
        disp.install_test_resource(&mut bus, *b"ALRT", 400, &alrt);
        bus.write_word(0x0A60, 0x7FFF);
        bus.write_word(TEST_SP, 400);

        let pre_a7 = cpu.read_reg(Register::A7);
        let result = disp.dispatch_dialog(true, 0x189, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
        assert_eq!(bus.read_word(0x0A60) as i16, 0);
    }

    #[test]
    fn could_alert_missing_resource_sets_reserr_resnotfound() {
        // IM:I I-420: missing ALRT IDs are ignored.
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_word(0x0A60, 0);
        bus.write_word(TEST_SP, 401);

        let pre_a7 = cpu.read_reg(Register::A7);
        let result = disp.dispatch_dialog(true, 0x189, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
        assert_eq!(bus.read_word(0x0A60) as i16, 0);
    }

    #[test]
    fn free_alert_present_resource_sets_reserr_noerr() {
        // IM:I I-420: FreeAlert undoes a prior CouldAlert target.
        // HLE compromise reports success via ResErr for present ALRT.
        let (mut disp, mut cpu, mut bus) = setup();
        let alrt = build_alrt_template(200, 0x0000);
        disp.install_test_resource(&mut bus, *b"ALRT", 402, &alrt);
        bus.write_word(0x0A60, 0x7FFF);
        bus.write_word(TEST_SP, 402);

        let pre_a7 = cpu.read_reg(Register::A7);
        let result = disp.dispatch_dialog(true, 0x18A, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
        assert_eq!(bus.read_word(0x0A60) as i16, 0);
    }

    #[test]
    fn free_alert_missing_resource_sets_reserr_resnotfound() {
        // Missing ALRT IDs in FreeAlert are ignored.
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_word(0x0A60, 0);
        bus.write_word(TEST_SP, 403);

        let pre_a7 = cpu.read_reg(Register::A7);
        let result = disp.dispatch_dialog(true, 0x18A, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
        assert_eq!(bus.read_word(0x0A60) as i16, 0);
    }

    #[test]
    fn could_alert_and_free_alert_leave_loaded_alert_attrs_unchanged() {
        let (mut disp, mut cpu, mut bus) = setup();
        let alrt = build_alrt_template(200, 0x0000);
        let data_ptr = disp.install_test_resource(&mut bus, *b"ALRT", 404, &alrt);
        let handle = disp.get_or_create_resource_handle(&mut bus, *b"ALRT", 404, data_ptr);
        let attrs_before = disp.resource_attributes_for_handle(handle).unwrap();

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 404);
        assert!(disp
            .dispatch_dialog(true, 0x189, &mut cpu, &mut bus)
            .unwrap()
            .is_ok());
        assert_eq!(bus.read_word(0x0A60) as i16, 0);
        assert_eq!(
            disp.resource_attributes_for_handle(handle).unwrap(),
            attrs_before
        );

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 404);
        assert!(disp
            .dispatch_dialog(true, 0x18A, &mut cpu, &mut bus)
            .unwrap()
            .is_ok());
        assert_eq!(bus.read_word(0x0A60) as i16, 0);
        assert_eq!(
            disp.resource_attributes_for_handle(handle).unwrap(),
            attrs_before
        );
    }

    #[test]
    fn could_alert_free_alert_purgeability() {
        // IM:I I-420 gives CouldAlert/FreeAlert the same resource-family
        // purgeability contract as CouldDialog/FreeDialog, rooted at ALRT.
        let (mut disp, mut cpu, mut bus) = setup();
        let alrt_id: i16 = 3200;
        let ditl_id: i16 = 3201;
        let cntl_id: i16 = 3202;
        let icon_id: i16 = 3203;
        let pict_id: i16 = 3204;
        let alrt = build_alrt_template(ditl_id, 0x0000);
        let ditl = build_test_ditl_items(&[
            (7, (1, 2, 12, 82), &cntl_id.to_be_bytes()),
            (32, (14, 2, 46, 34), &icon_id.to_be_bytes()),
            (64, (48, 2, 88, 82), &pict_id.to_be_bytes()),
        ]);
        disp.install_test_resource(&mut bus, *b"ALRT", alrt_id, &alrt);
        disp.install_test_resource(&mut bus, *b"DITL", ditl_id, &ditl);
        disp.install_test_resource(&mut bus, *b"CNTL", cntl_id, &[0u8; 32]);
        disp.install_test_resource(&mut bus, *b"ICON", icon_id, &[0xAA; 128]);
        disp.install_test_resource(&mut bus, *b"PICT", pict_id, &[0x11; 32]);
        disp.res_load = false;

        bus.write_word(0x0A60, 0x7FFF);
        bus.write_word(TEST_SP, alrt_id as u16);
        let pre_a7 = cpu.read_reg(Register::A7);
        assert!(disp
            .dispatch_dialog(true, 0x189, &mut cpu, &mut bus)
            .unwrap()
            .is_ok());
        assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 2);
        assert_eq!(bus.read_word(0x0A60) as i16, 0);
        for (res_type, res_id) in [
            (*b"ALRT", alrt_id),
            (*b"DITL", ditl_id),
            (*b"CNTL", cntl_id),
            (*b"ICON", icon_id),
            (*b"PICT", pict_id),
        ] {
            assert_resource_nonpurgeable(&disp, &bus, res_type, res_id);
        }

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, alrt_id as u16);
        assert!(disp
            .dispatch_dialog(true, 0x18A, &mut cpu, &mut bus)
            .unwrap()
            .is_ok());
        assert_eq!(bus.read_word(0x0A60) as i16, 0);
        for (res_type, res_id) in [
            (*b"ALRT", alrt_id),
            (*b"DITL", ditl_id),
            (*b"CNTL", cntl_id),
            (*b"ICON", icon_id),
            (*b"PICT", pict_id),
        ] {
            assert_resource_purgeable(&disp, res_type, res_id);
        }
    }

    #[test]
    fn alert_returns_item_1_when_stage_1_bold_flag_clear() {
        // stages = 0x4444 → all 4 nibbles have boxDrwn set and
        // bold item clear, so item 1 (OK button) is the default.
        let (mut disp, mut cpu, mut bus) = setup();
        let alrt = build_alrt_template(/*itemsID*/ 200, /*stages*/ 0x4444);
        disp.install_test_resource(&mut bus, *b"ALRT", 128, &alrt);
        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 0); // stage 1
        bus.write_word(TEST_SP + 4, 128);
        let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            bus.read_word(TEST_SP + 6) as i16,
            1,
            "Alert with stage 1 + bold flag clear must return item 1 (OK)"
        );
    }

    #[test]
    fn alert_returns_item_2_when_stage_1_bold_flag_set() {
        // stages = 0xCCCC → all 4 nibbles have boxDrwn and bit 3 set
        // (okDismissal mask per IM:I I-424), so item 2 is default.
        let (mut disp, mut cpu, mut bus) = setup();
        let alrt = build_alrt_template(200, 0xCCCC);
        disp.install_test_resource(&mut bus, *b"ALRT", 129, &alrt);
        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 0);
        bus.write_word(TEST_SP + 4, 129);
        let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            bus.read_word(TEST_SP + 6) as i16,
            2,
            "Alert with stage 1 + bold flag set must return item 2"
        );
    }

    #[test]
    fn alert_steps_through_stages_and_caps_at_stage_4() {
        // stages = 0xC4C4 → boxDrwn set in every nibble; bit 3 of
        // stage 1/3 nibble = 0 (item 1), bit 3 of stage 2/4 nibble
        // = 1 (item 2). Per IM:I I-422 okDismissal mask = 8.
        let (mut disp, mut cpu, mut bus) = setup();
        let alrt = build_alrt_template(200, 0xC4C4);
        disp.install_test_resource(&mut bus, *b"ALRT", 130, &alrt);
        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 0);
        bus.write_word(TEST_SP + 4, 130);

        let expected = [1i16, 2, 1, 2, 2, 2];
        for (i, want) in expected.iter().enumerate() {
            cpu.write_reg(Register::A7, TEST_SP);
            let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            assert_eq!(
                bus.read_word(TEST_SP + 6) as i16,
                *want,
                "call #{} (stage {}): expected item {}",
                i + 1,
                (i + 1).min(4),
                want
            );
        }
        // After at least 4 calls the AlertStage word is capped at 3.
        assert_eq!(
            bus.read_word(crate::memory::globals::addr::ALERT_STAGE),
            3,
            "AlertStage must cap at 3 (stages are 1..4 → word 0..3)"
        );
    }

    #[test]
    fn alert_increments_alert_stage_word_after_dispatch() {
        // First call from stage 0 must leave AlertStage = 1. Read
        // and write as 16-bit WORD per IM:I I-423 + MTb 1992 22620.
        let (mut disp, mut cpu, mut bus) = setup();
        let alrt = build_alrt_template(200, 0x4444);
        disp.install_test_resource(&mut bus, *b"ALRT", 131, &alrt);
        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 0);
        bus.write_word(TEST_SP + 4, 131);
        let _ = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
        assert_eq!(
            bus.read_word(crate::memory::globals::addr::ALERT_STAGE),
            1,
            "Alert must increment AlertStage 0 → 1 after dispatch"
        );
    }

    #[test]
    fn alert_does_not_increment_alert_stage_when_alrt_missing() {
        // No ALRT → -1 path must NOT touch AlertStage so the next
        // call (with a real ALRT installed) sees the original
        // stage. This is critical for apps that defensively call
        // Alert(missingID) before Alert(realID).
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 2);
        bus.write_word(TEST_SP + 4, 999); // no ALRT 999
        let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(TEST_SP + 6) as i16, -1);
        assert_eq!(
            bus.read_word(crate::memory::globals::addr::ALERT_STAGE),
            2,
            "missing-ALRT must NOT increment AlertStage"
        );
    }

    #[test]
    fn alert_returns_minus_one_when_stage_box_drawn_clear_but_updates_stage() {
        // IM:I I-418 and MTE 1992 p. 6-106: Alert calls the alert
        // sound procedure for the stage and returns -1 when boxDrwn
        // is clear, but it is still an occurrence of that alert.
        let (mut disp, mut cpu, mut bus) = setup();
        let alrt = build_alrt_template(200, 0x0000);
        disp.install_test_resource(&mut bus, *b"ALRT", 133, &alrt);
        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 0);
        bus.write_word(crate::memory::globals::addr::ANUMBER, 0xCAFE);
        bus.write_word(TEST_SP + 4, 133);

        let result = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(
            bus.read_word(TEST_SP + 6) as i16,
            -1,
            "boxDrwn clear must suppress the alert box and return -1"
        );
        assert_eq!(
            bus.read_word(crate::memory::globals::addr::ALERT_STAGE),
            1,
            "suppressed present ALRT must still advance ACount"
        );
        assert_eq!(
            bus.read_word(crate::memory::globals::addr::ANUMBER) as i16,
            133,
            "suppressed present ALRT must still record ANumber"
        );
    }

    #[test]
    fn alert_writes_anumber_with_alert_id_after_successful_dispatch() {
        // ANumber at $0A98 records the resource ID of the last
        // alert that occurred per IM:I I-423.
        let (mut disp, mut cpu, mut bus) = setup();
        let alrt = build_alrt_template(200, 0x4444);
        disp.install_test_resource(&mut bus, *b"ALRT", 250, &alrt);
        bus.write_word(crate::memory::globals::addr::ANUMBER, 0xCAFE);
        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 0);
        bus.write_word(TEST_SP + 4, 250);
        let _ = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
        assert_eq!(
            bus.read_word(crate::memory::globals::addr::ANUMBER) as i16,
            250,
            "Alert must write the alertID to ANumber per IM:I I-423"
        );
    }

    #[test]
    fn alert_does_not_overwrite_anumber_when_alrt_missing() {
        // Missing-ALRT path returns -1 without overwriting ANumber
        // (defensive: callers reading ANumber after a probe call
        // must see the prior real value, not the failed probe ID).
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_word(crate::memory::globals::addr::ANUMBER, 0xC0DE);
        bus.write_word(TEST_SP + 4, 999); // no ALRT 999
        let _ = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
        assert_eq!(
            bus.read_word(crate::memory::globals::addr::ANUMBER),
            0xC0DE,
            "missing-ALRT must NOT overwrite ANumber"
        );
    }

    #[test]
    fn alert_family_share_dispatch_path_so_stop_note_caution_match_alert() {
        // The four icon variants ($A985 Alert / $A986 StopAlert /
        // $A987 NoteAlert / $A988 CautionAlert) differ only by
        // displayed icon — their dispatch into the ALRT template
        // is identical. With the same ALRT installed and the same
        // AlertStage all four must return the same item number.
        let alrt = build_alrt_template(200, 0x4444);
        let trap_words = [
            (0x185, "Alert"),
            (0x186, "StopAlert"),
            (0x187, "NoteAlert"),
            (0x188, "CautionAlert"),
        ];
        for (trap, name) in trap_words.iter() {
            let (mut disp, mut cpu, mut bus) = setup();
            disp.install_test_resource(&mut bus, *b"ALRT", 132, &alrt);
            bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 0);
            bus.write_word(TEST_SP + 4, 132);
            let result = disp.dispatch_dialog(true, *trap, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            assert_eq!(
                bus.read_word(TEST_SP + 6) as i16,
                1,
                "{name} must return item 1 (bold flag clear, stage 1) — \
                 same as Alert"
            );
        }
    }

    #[test]
    fn alert_pops_eight_bytes_per_pascal_signature() {
        // FUNCTION Alert(alertID: INTEGER; filterProc: ProcPtr): INTEGER;
        // Pascal stack frame: result(2) + filterProc(4) +
        // alertID(2) + return PC slot mock at SP+0 (caller-set
        // here). Trap pops 6 bytes (filterProc + alertID + return)
        // leaving result at new SP+0 = TEST_SP+6.
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP + 4, 999); // alertID
        let pre_a7 = cpu.read_reg(Register::A7);
        let _ = disp.dispatch_dialog(true, 0x185, &mut cpu, &mut bus);
        let post_a7 = cpu.read_reg(Register::A7);
        assert_eq!(
            post_a7,
            pre_a7 + 6,
            "Alert must advance A7 by 6 bytes (filterProc + alertID + return)"
        );
    }

    // ---- InitDialogs ($A97B) — IM:I I-411 init contract ----

    #[test]
    fn init_dialogs_stores_resume_proc_at_lowmem_a8c() {
        // PROCEDURE InitDialogs(resumeProc: ProcPtr);
        // The non-NIL ProcPtr argument must land at $0A8C
        // (ResumeProc global) per IM:I I-411 + the assembly note.
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_long(crate::memory::globals::addr::RESUME_PROC, 0xCAFEBABE);
        bus.write_long(TEST_SP, 0x00123456); // resumeProc parameter
        let _ = disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus);
        assert_eq!(
            bus.read_long(crate::memory::globals::addr::RESUME_PROC),
            0x00123456,
            "InitDialogs must store resumeProc at $0A8C"
        );
    }

    #[test]
    fn init_dialogs_accepts_nil_resume_proc() {
        // The IM-canonical default is NIL ("no resume procedure
        // is desired"). Pin that NIL is stored as 0 — not a
        // sentinel like -1 — so the System Error Handler's
        // `if (resume) call(*resume)` path takes the no-op branch.
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_long(crate::memory::globals::addr::RESUME_PROC, 0xDEADBEEF);
        bus.write_long(TEST_SP, 0); // NIL
        let _ = disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus);
        assert_eq!(
            bus.read_long(crate::memory::globals::addr::RESUME_PROC),
            0,
            "InitDialogs must store NIL resumeProc as 0 at $0A8C"
        );
    }

    #[test]
    fn init_dialogs_zeros_dabeeper_at_lowmem_a9c() {
        // IM:I I-411: "It installs the standard sound procedure."
        // Systemless's HLE has no menu-bar-blink sound, so we install
        // NIL (== silent) per the IM:I I-411 ErrorSound semantic
        // ("If you pass NIL for soundProc, there will be no sound
        // ... at all"). A subsequent ErrorSound ($A98C) call can
        // override.
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_long(crate::memory::globals::addr::DA_BEEPER, 0xDEADBEEF);
        bus.write_long(TEST_SP, 0);
        let _ = disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus);
        assert_eq!(
            bus.read_long(crate::memory::globals::addr::DA_BEEPER),
            0,
            "InitDialogs must store NIL (silent default) at DABeeper $0A9C"
        );
    }

    #[test]
    fn init_dialogs_zeros_alert_stage_at_lowmem_a9a() {
        // First call to Alert/StopAlert/NoteAlert/CautionAlert
        // after InitDialogs must start at stage 1 (= AlertStage
        // word value 0). This is critical for apps that call
        // InitDialogs at re-launch but already have a stale stage
        // byte from a prior crashed run.
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 3);
        bus.write_long(TEST_SP, 0);
        let _ = disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus);
        assert_eq!(
            bus.read_word(crate::memory::globals::addr::ALERT_STAGE),
            0,
            "InitDialogs must zero AlertStage at $0A9A so the next Alert starts at stage 1"
        );
    }

    #[test]
    fn init_dialogs_zeros_dastrings_array_at_lowmem_aa0() {
        // IM:I I-411: "It passes empty strings to ParamText."
        // The DAStrings global at $0AA0 is a 16-byte array of 4
        // Handles; zeroing all 4 = ParamText('','','','').
        let (mut disp, mut cpu, mut bus) = setup();
        for i in 0..4u32 {
            bus.write_long(
                crate::memory::globals::addr::DA_STRINGS + i * 4,
                0xDEAD0000 | i,
            );
        }
        bus.write_long(TEST_SP, 0);
        let _ = disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus);
        for i in 0..4u32 {
            assert_eq!(
                bus.read_long(crate::memory::globals::addr::DA_STRINGS + i * 4),
                0,
                "InitDialogs must zero DAStrings[{}] at $0AA0+{}",
                i,
                i * 4
            );
        }
    }

    #[test]
    fn init_dialogs_pops_four_bytes_resume_proc_param() {
        // PROCEDURE → no result slot, single 4-byte ProcPtr arg.
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0);
        let pre_a7 = cpu.read_reg(Register::A7);
        let _ = disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus);
        assert_eq!(
            cpu.read_reg(Register::A7),
            pre_a7 + 4,
            "InitDialogs must pop 4 bytes (resumeProc ProcPtr)"
        );
    }

    // ---- ErrorSound ($A98C) ----

    #[test]
    fn error_sound_stores_sound_proc_pointer_in_dabeeper() {
        // Inside Macintosh Volume I, I-411: ErrorSound sets the current
        // alert sound procedure, and the assembly-language note says this
        // pointer is stored in DABeeper.
        let (mut disp, mut cpu, mut bus) = setup();
        let sound_proc = 0x0012_3456u32;
        bus.write_long(crate::memory::globals::addr::DA_BEEPER, 0);
        bus.write_long(TEST_SP, sound_proc);

        let pre_a7 = cpu.read_reg(Register::A7);
        let result = disp.dispatch_dialog(true, 0x18C, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            bus.read_long(crate::memory::globals::addr::DA_BEEPER),
            sound_proc
        );
        assert_eq!(cpu.read_reg(Register::A7), pre_a7 + 4);
    }

    #[test]
    fn error_sound_nil_clears_dabeeper_to_disable_alert_sound() {
        // Inside Macintosh Volume I, I-411: passing NIL for soundProc means
        // "no sound (or menu bar blinking) at all".
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_long(crate::memory::globals::addr::DA_BEEPER, 0xDEADBEEF);
        bus.write_long(TEST_SP, 0);

        let result = disp.dispatch_dialog(true, 0x18C, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_long(crate::memory::globals::addr::DA_BEEPER), 0);
    }

    #[test]
    fn error_sound_overrides_init_dialogs_default_sound_proc() {
        // IM:I I-411: InitDialogs installs the default alert sound procedure,
        // and ErrorSound replaces it with the caller's soundProc.
        let (mut disp, mut cpu, mut bus) = setup();
        let sound_proc = 0x0012_3456u32;

        bus.write_long(crate::memory::globals::addr::DA_BEEPER, 0xDEADBEEF);
        bus.write_long(TEST_SP, 0);
        let result = disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_long(crate::memory::globals::addr::DA_BEEPER), 0);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, sound_proc);
        let result = disp.dispatch_dialog(true, 0x18C, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            bus.read_long(crate::memory::globals::addr::DA_BEEPER),
            sound_proc
        );

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, 0);
        let result = disp.dispatch_dialog(true, 0x18C, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_long(crate::memory::globals::addr::DA_BEEPER), 0);
    }

    // ---- IsDialogEvent ($A97F) ----

    #[test]
    fn is_dialog_event_returns_false() {
        let (mut disp, mut cpu, mut bus) = setup();
        // SP+0: event_ptr (4 bytes)
        bus.write_long(TEST_SP, 0x300000);

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

    #[test]
    fn is_dialog_event_true_for_mouse_down_in_front_dialog() {
        // Inside Macintosh Volume I, I-417: mouse-down in content region of
        // active dialog window is a dialog event (TRUE).
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let event_ptr = 0x300000u32;

        disp.front_window = dialog_ptr;
        disp.enable_input_trace_capture();
        bus.write_word(dialog_ptr + 8, (-100i16) as u16);
        bus.write_word(dialog_ptr + 10, (-200i16) as u16);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 80);
        bus.write_word(dialog_ptr + 22, 120);
        disp.dialog_items.insert(dialog_ptr, Vec::new());

        bus.write_word(event_ptr, 1);
        bus.write_long(event_ptr + 2, 0);
        bus.write_long(event_ptr + 6, 0);
        bus.write_word(event_ptr + 10, 120);
        bus.write_word(event_ptr + 12, 240);
        bus.write_word(event_ptr + 14, 0x0080);
        bus.write_long(TEST_SP, event_ptr);

        let result = disp.dispatch_dialog(true, 0x17F, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(TEST_SP + 4), 0xFFFF);
        let trace = disp.input_trace_text();
        assert!(trace.contains("A97F action=guest_event:mouseDown"));
        assert!(trace.contains("tracking=menu:idle dialog:idle control:idle"));
        assert!(trace.contains("result=true outcome=is_dialog_event"));
    }

    #[test]
    fn is_dialog_event_false_for_mouse_down_outside_front_dialog() {
        // Inside Macintosh Volume I, I-417: only mouse-down events in the
        // content region of an active dialog window return TRUE.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let event_ptr = 0x300000u32;

        disp.front_window = dialog_ptr;
        disp.enable_input_trace_capture();
        bus.write_word(dialog_ptr + 8, (-100i16) as u16);
        bus.write_word(dialog_ptr + 10, (-200i16) as u16);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 80);
        bus.write_word(dialog_ptr + 22, 120);
        disp.dialog_items.insert(dialog_ptr, Vec::new());

        bus.write_word(event_ptr, 1); // mouseDown
        bus.write_long(event_ptr + 2, 0);
        bus.write_long(event_ptr + 6, 0);
        bus.write_word(event_ptr + 10, 40); // well outside dialog content
        bus.write_word(event_ptr + 12, 40);
        bus.write_word(event_ptr + 14, 0x0080);
        bus.write_long(TEST_SP, event_ptr);

        let result = disp.dispatch_dialog(true, 0x17F, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(TEST_SP + 4), 0);
    }

    #[test]
    fn is_dialog_event_true_for_update_event_targeting_dialog_window() {
        // Inside Macintosh Volume I, I-417: activate/update events for a
        // dialog window are dialog events.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let event_ptr = 0x300000u32;

        disp.dialog_items.insert(dialog_ptr, Vec::new());
        bus.write_word(event_ptr, 6); // updateEvt
        bus.write_long(event_ptr + 2, dialog_ptr);
        bus.write_long(TEST_SP, event_ptr);

        let result = disp.dispatch_dialog(true, 0x17F, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(TEST_SP + 4), 0xFFFF);
    }

    #[test]
    fn is_dialog_event_true_for_activate_event_targeting_dialog_window() {
        // Inside Macintosh Volume I, I-417: activate events for a dialog
        // window are dialog events as well.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let event_ptr = 0x300000u32;

        disp.dialog_items.insert(dialog_ptr, Vec::new());
        bus.write_word(event_ptr, 8); // activateEvt
        bus.write_long(event_ptr + 2, dialog_ptr);
        bus.write_long(TEST_SP, event_ptr);

        let result = disp.dispatch_dialog(true, 0x17F, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(TEST_SP + 4), 0xFFFF);
    }

    // ---- DialogSelect ($A980) ----

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

        let result = disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
        assert_eq!(bus.read_word(TEST_SP + 12), 0);
    }

    #[test]
    fn dialog_select_returns_hit_for_enabled_user_item() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let event_ptr = 0x300000u32;
        let dialog_out_ptr = 0x300100u32;
        let item_hit_ptr = 0x300104u32;

        disp.front_window = dialog_ptr;
        disp.enable_input_trace_capture();
        bus.write_word(dialog_ptr + 8, (-100i16) as u16);
        bus.write_word(dialog_ptr + 10, (-200i16) as u16);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 100);
        bus.write_word(dialog_ptr + 22, 160);
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 0,
                rect: (20, 30, 60, 110),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        bus.write_word(event_ptr, 1);
        bus.write_long(event_ptr + 2, 0);
        bus.write_long(event_ptr + 6, 0);
        bus.write_word(event_ptr + 10, 130);
        bus.write_word(event_ptr + 12, 240);
        bus.write_word(event_ptr + 14, 0x0080);

        bus.write_long(TEST_SP, item_hit_ptr);
        bus.write_long(TEST_SP + 4, dialog_out_ptr);
        bus.write_long(TEST_SP + 8, event_ptr);

        let result = disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
        assert_eq!(bus.read_word(TEST_SP + 12), 0xFFFF);
        assert_eq!(bus.read_long(dialog_out_ptr), dialog_ptr);
        assert_eq!(bus.read_word(item_hit_ptr), 1);
        let trace = disp.input_trace_text();
        assert!(trace.contains("A980 action=guest_event:mouseDown"));
        assert!(trace.contains("item_hit=1 item_type=$00 disabled=false outcome=enabled_item"));
    }

    fn dialog_select_enabled_user_item_hit_for_theme(theme_id: UiThemeId) -> (u16, u32, u16, u32) {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.set_ui_theme_id(theme_id);
        let dialog_ptr = bus.alloc(170);
        let event_ptr = 0x300000u32;
        let dialog_out_ptr = 0x300100u32;
        let item_hit_ptr = 0x300104u32;

        disp.front_window = dialog_ptr;
        bus.write_word(dialog_ptr + 8, (-100i16) as u16);
        bus.write_word(dialog_ptr + 10, (-200i16) as u16);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 100);
        bus.write_word(dialog_ptr + 22, 160);
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 0,
                rect: (20, 30, 60, 110),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        bus.write_word(event_ptr, 1);
        bus.write_long(event_ptr + 2, 0);
        bus.write_long(event_ptr + 6, 0);
        bus.write_word(event_ptr + 10, 130);
        bus.write_word(event_ptr + 12, 240);
        bus.write_word(event_ptr + 14, 0x0080);
        bus.write_long(TEST_SP, item_hit_ptr);
        bus.write_long(TEST_SP + 4, dialog_out_ptr);
        bus.write_long(TEST_SP + 8, event_ptr);

        disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        (
            bus.read_word(TEST_SP + 12),
            bus.read_long(dialog_out_ptr),
            bus.read_word(item_hit_ptr),
            cpu.read_reg(Register::A7),
        )
    }

    fn dialog_select_enabled_checkbox_hit_for_theme(
        theme_id: UiThemeId,
    ) -> (u16, u32, u16, u32, u16, u16) {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.set_ui_theme_id(theme_id);
        let dialog_ptr = bus.alloc(170);
        let event_ptr = 0x300000u32;
        let dialog_out_ptr = 0x300100u32;
        let item_hit_ptr = 0x300104u32;
        let box_ptr = 0x300108u32;
        let item_ptr = 0x300110u32;
        let type_ptr = 0x300114u32;

        disp.front_window = dialog_ptr;
        bus.write_word(dialog_ptr + 8, 0);
        bus.write_word(dialog_ptr + 10, 0);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 100);
        bus.write_word(dialog_ptr + 22, 160);
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 5,
                rect: (20, 30, 40, 130),
                text: "Enabled".to_string(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );
        disp.dialog_control_values.insert((dialog_ptr, 1), 1);

        bus.write_long(TEST_SP, box_ptr);
        bus.write_long(TEST_SP + 4, item_ptr);
        bus.write_long(TEST_SP + 8, type_ptr);
        bus.write_word(TEST_SP + 12, 1);
        bus.write_long(TEST_SP + 14, dialog_ptr);
        disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 18);
        let ctrl_handle = bus.read_long(item_ptr);
        let ctrl_ptr = bus.read_long(ctrl_handle);
        assert_ne!(ctrl_ptr, 0);
        assert_eq!(bus.read_word(type_ptr), 5);
        assert_eq!(bus.read_word(ctrl_ptr + 18), 1);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(event_ptr, 1);
        bus.write_long(event_ptr + 2, 0);
        bus.write_long(event_ptr + 6, 0);
        bus.write_word(event_ptr + 10, 30);
        bus.write_word(event_ptr + 12, 40);
        bus.write_word(event_ptr + 14, 0x0080);
        bus.write_long(TEST_SP, item_hit_ptr);
        bus.write_long(TEST_SP + 4, dialog_out_ptr);
        bus.write_long(TEST_SP + 8, event_ptr);

        disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        (
            bus.read_word(TEST_SP + 12),
            bus.read_long(dialog_out_ptr),
            bus.read_word(item_hit_ptr),
            cpu.read_reg(Register::A7),
            bus.read_word(ctrl_ptr + 18),
            disp.dialog_control_values
                .get(&(dialog_ptr, 1))
                .copied()
                .unwrap_or(-1) as u16,
        )
    }

    fn findditem_results_for_theme(theme_id: UiThemeId) -> Vec<(i16, u32)> {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.set_ui_theme_id(theme_id);
        let dialog_ptr = 0x220400u32;
        disp.dialog_items.insert(
            dialog_ptr,
            vec![
                DialogItem {
                    item_type: 0x80 | 4,
                    rect: (10, 20, 40, 60),
                    text: "DisabledFirst".into(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
                DialogItem {
                    item_type: 4,
                    rect: (20, 30, 55, 80),
                    text: "EnabledSecond".into(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
                DialogItem {
                    item_type: 16,
                    rect: (60, 16384 + 20, 80, 16384 + 100),
                    text: "HiddenEdit".into(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
            ],
        );

        let mut out = Vec::new();
        for (pt_v, pt_h) in [(25i16, 35i16), (45, 70), (65, 40), (90, 110)] {
            cpu.write_reg(Register::A7, TEST_SP);
            bus.write_word(TEST_SP, pt_v as u16);
            bus.write_word(TEST_SP + 2, pt_h as u16);
            bus.write_long(TEST_SP + 4, dialog_ptr);
            bus.write_word(TEST_SP + 8, 0xBEEF);
            disp.dispatch_dialog(true, 0x184, &mut cpu, &mut bus)
                .unwrap()
                .unwrap();
            out.push((
                bus.read_word(TEST_SP + 8) as i16,
                cpu.read_reg(Register::A7),
            ));
        }
        out
    }

    #[derive(Debug, PartialEq, Eq)]
    struct DItemGetSnapshot {
        item_type: u16,
        item: u32,
        rect: (u16, u16, u16, u16),
        stack_after: u32,
    }

    #[derive(Debug, PartialEq, Eq)]
    struct DItemUserItemRecordSnapshot {
        initial_get: DItemGetSnapshot,
        set_stack_after: u32,
        stored_type: u8,
        stored_proc_ptr: u32,
        stored_rect: (i16, i16, i16, i16),
        post_set_get: DItemGetSnapshot,
    }

    #[derive(Debug, PartialEq, Eq)]
    struct DItemTextControlRecordSnapshot {
        text_initial_get: DItemGetSnapshot,
        text_set_stack_after: u32,
        text_old_handle_mapped_after: bool,
        text_new_handle_map_value_after: Option<(u32, usize)>,
        text_ditl_handle_after: u32,
        text_ditl_rect_after: (u16, u16, u16, u16),
        text_ditl_type_after: u8,
        text_stored_type_after: u8,
        text_stored_rect_after: (i16, i16, i16, i16),
        text_cached_text_after: String,
        text_handle_bytes_after: Vec<u8>,
        text_post_set_get: DItemGetSnapshot,
        control_initial_get: DItemGetSnapshot,
        control_set_stack_after: u32,
        control_old_handle_mapped_after: bool,
        control_new_handle_map_value_after: Option<(u32, i16)>,
        control_ditl_handle_after: u32,
        control_ditl_rect_after: (u16, u16, u16, u16),
        control_ditl_type_after: u8,
        control_stored_type_after: u8,
        control_stored_rect_after: (i16, i16, i16, i16),
        control_record_rect_after: (u16, u16, u16, u16),
        control_value_after: Option<i16>,
        control_post_set_get: DItemGetSnapshot,
    }

    #[derive(Debug, PartialEq, Eq)]
    struct DItemVisibilitySnapshot {
        initial_get: DItemGetSnapshot,
        hide_stack_after: u32,
        hidden_item_rect: (i16, i16, i16, i16),
        hidden_saved_rect: Option<(i16, i16, i16, i16)>,
        hidden_control_rect: (u16, u16, u16, u16),
        hidden_get: DItemGetSnapshot,
        hidden_find: (i16, u32),
        show_stack_after: u32,
        restored_item_rect: (i16, i16, i16, i16),
        restored_saved_rect: Option<(i16, i16, i16, i16)>,
        restored_control_rect: (u16, u16, u16, u16),
        restored_get: DItemGetSnapshot,
        restored_find: (i16, u32),
    }

    #[derive(Debug, PartialEq, Eq)]
    struct ParamTextSnapshot {
        initial_slots: [Vec<u8>; 4],
        initial_da_handles: [u32; 4],
        initial_da_strings: [Vec<u8>; 4],
        initial_stack_after: u32,
        initial_substitution: String,
        nil_slots: [Vec<u8>; 4],
        nil_da_handles: [u32; 4],
        nil_da_strings: [Vec<u8>; 4],
        nil_stack_after: u32,
        nil_substitution: String,
    }

    #[derive(Debug, PartialEq, Eq)]
    struct DialogItemTextSnapshot {
        initial_get_text: Vec<u8>,
        initial_get_stack_after: u32,
        set_stack_after: u32,
        handle_size_after_set: Option<u32>,
        handle_bytes_after_set: Vec<u8>,
        cached_text_after_set: String,
        post_set_get_text: Vec<u8>,
        post_set_get_stack_after: u32,
    }

    #[derive(Debug, PartialEq, Eq)]
    struct DialogItemTextSelectionSnapshot {
        whole_selection: (i16, i16),
        whole_edit_field: u16,
        whole_te_selection: (u16, u16),
        whole_stack_after: u32,
        normalized_selection: (i16, i16),
        normalized_edit_field: u16,
        normalized_te_selection: (u16, u16),
        normalized_stack_after: u32,
        non_edit_selection: (i16, i16),
        non_edit_edit_field: u16,
        non_edit_te_selection: (u16, u16),
        non_edit_stack_after: u32,
    }

    #[derive(Debug, PartialEq, Eq)]
    struct IsDialogEventSnapshot {
        null_event: (u16, u32),
        key_down: (u16, u32),
        mouse_inside: (u16, u32),
        mouse_outside: (u16, u32),
        update_target: (u16, u32),
        activate_target: (u16, u32),
    }

    #[derive(Debug, PartialEq, Eq)]
    struct DialogSelectWindowEventSnapshot {
        dialog_ptr: u32,
        update_result: u16,
        update_dialog_out: u32,
        update_item_hit: u16,
        update_stack_after: u32,
        update_deferred_after: bool,
        update_region_after: Option<(i16, i16, i16, i16)>,
        update_vis_region_after: Option<(i16, i16, i16, i16)>,
        update_the_port_after: u32,
        update_current_port_after: u32,
        update_saved_vis_after: bool,
        update_queued_draw_procs: Vec<(u32, u32, i16)>,
        update_item_count_after: usize,
        activate_result: u16,
        activate_dialog_out: u32,
        activate_item_hit: u16,
        activate_stack_after: u32,
    }

    #[derive(Debug, PartialEq, Eq)]
    struct DialogSelectEditTextMouseSnapshot {
        mouse_result: u16,
        mouse_dialog_out: u32,
        mouse_item_hit: u16,
        mouse_stack_after: u32,
        mouse_edit_field: u16,
        mouse_item_selection: (i16, i16),
        mouse_te_text: Vec<u8>,
        mouse_te_length: u16,
        mouse_te_selection: (u16, u16),
        null_result: u16,
        null_dialog_out: u32,
        null_item_hit: u16,
        null_stack_after: u32,
        null_edit_field: u16,
        null_te_text: Vec<u8>,
        null_te_length: u16,
        null_te_selection: (u16, u16),
    }

    #[derive(Debug, PartialEq, Eq)]
    struct ModalDialogEditTextMouseSnapshot {
        item_hit: u16,
        stack_after: u32,
        tracking_finished: bool,
        retained_visible_snapshot: bool,
        saved_background_retained: bool,
        queued_mouse_up_consumed: bool,
        edit_field: u16,
        item_selection: (i16, i16),
        te_text: Vec<u8>,
        te_length: u16,
        te_selection: (u16, u16),
        handle_bytes: Vec<u8>,
    }

    #[derive(Debug, PartialEq, Eq)]
    struct ModalDialogKeyboardButtonSnapshot {
        return_key: ModalDialogKeyboardButtonCase,
        enter_key: ModalDialogKeyboardButtonCase,
        escape_key: ModalDialogKeyboardButtonCase,
        command_period: ModalDialogKeyboardButtonCase,
    }

    #[derive(Debug, PartialEq, Eq)]
    struct ModalDialogKeyboardButtonCase {
        flash_item_after_key: i16,
        stack_after_key: u32,
        item_hit_after_key: u16,
        item_hit_after_flash: u16,
        stack_after_flash: u32,
        tracking_finished: bool,
    }

    #[derive(Debug, PartialEq, Eq)]
    struct ModalDialogUpdateEventSnapshot {
        dialog_ptr: u32,
        item_hit_after: u16,
        stack_after: u32,
        tracking_finished: bool,
        rendered_pixels_final: bool,
        retained_pixel_after: u8,
        retained_snapshot_pixel_after: u8,
        update_region_after: Option<(i16, i16, i16, i16)>,
        vis_region_after: Option<(i16, i16, i16, i16)>,
        the_port_after: u32,
        current_port_after: u32,
        saved_vis_after: bool,
    }

    #[derive(Debug, PartialEq, Eq)]
    struct DialogLifecycleCleanupSnapshot {
        close_stack_after: u32,
        close_tracking_cleared: bool,
        close_retained_click_cleared: bool,
        close_visible_snapshot_cleared: bool,
        close_modal_entered_cleared: bool,
        close_saved_pixels_cleared: bool,
        close_window_list_contains_dialog: bool,
        close_front_window_after: u32,
        close_the_port_after: u32,
        close_current_port_after: u32,
        close_update_event_for_previous_window: bool,
        close_dialog_items_present_after: bool,
        dispose_stack_after: u32,
        dispose_tracking_cleared: bool,
        dispose_retained_click_cleared: bool,
        dispose_visible_snapshot_cleared: bool,
        dispose_modal_entered_cleared: bool,
        dispose_saved_pixels_cleared: bool,
        dispose_window_list_contains_dialog: bool,
        dispose_front_window_after: u32,
        dispose_the_port_after: u32,
        dispose_current_port_after: u32,
        dispose_update_event_for_previous_window: bool,
        dispose_dialog_items_present_after: bool,
        dispose_other_dialog_items_present_after: bool,
        dispose_dialog_item_handle_present_after: bool,
        dispose_other_dialog_item_handle_present_after: bool,
        dispose_dialog_control_handle_present_after: bool,
        dispose_other_dialog_control_handle_present_after: bool,
        dispose_dialog_control_value_present_after: bool,
        dispose_other_dialog_control_value_present_after: bool,
        dispose_hidden_rect_present_after: bool,
        dispose_other_hidden_rect_present_after: bool,
        dispose_cancel_item_present_after: bool,
        dispose_other_cancel_item_present_after: bool,
        dispose_pending_popup_cleared: bool,
    }

    #[derive(Debug, PartialEq, Eq)]
    struct DialogCreationSnapshot {
        new_dialog_ptr: u32,
        new_stack_after: u32,
        new_result_slot: u32,
        new_window_list: Vec<u32>,
        new_front_window: u32,
        new_current_port: u32,
        new_the_port: u32,
        new_visible_byte: u8,
        new_goaway_byte: u8,
        new_refcon: u32,
        new_window_kind: i16,
        new_proc_id: i16,
        new_port_rect: (i16, i16, i16, i16),
        new_items_handle: u32,
        new_items_data_ptr: u32,
        new_first_item_handle_nonzero: bool,
        new_cached_items: Vec<(u8, (i16, i16, i16, i16), String, i16)>,
        new_update_region: Option<(i16, i16, i16, i16)>,
        new_update_event_queued: bool,
        new_edit_field: u16,
        new_default_item: u16,
        get_dialog_ptr: u32,
        get_stack_after: u32,
        get_result_slot: u32,
        get_window_list: Vec<u32>,
        get_front_window: u32,
        get_current_port: u32,
        get_the_port: u32,
        get_visible_byte: u8,
        get_refcon: u32,
        get_window_kind: i16,
        get_proc_id: i16,
        get_port_rect: (i16, i16, i16, i16),
        get_items_handle: u32,
        get_items_data_ptr: u32,
        get_uses_distinct_ditl_copy: bool,
        get_original_ditl_handle_field: u32,
        get_first_item_handle_nonzero: bool,
        get_cached_items: Vec<(u8, (i16, i16, i16, i16), String, i16)>,
        get_update_region: Option<(i16, i16, i16, i16)>,
        get_update_event_queued: bool,
        get_edit_field: u16,
        get_default_item: u16,
    }

    #[derive(Debug, PartialEq, Eq)]
    struct AlertTrapCallSnapshot {
        trap_word: u16,
        result: i16,
        stack_after: u32,
        alert_stage_after: u16,
        anumber_after: u16,
    }

    #[derive(Debug, PartialEq, Eq)]
    struct AlertResourcePrepSnapshot {
        could_present: (i16, u32),
        free_present: (i16, u32),
        could_missing: (i16, u32),
        free_missing: (i16, u32),
    }

    #[derive(Debug, PartialEq, Eq)]
    struct AlertFamilySnapshot {
        staged_calls: Vec<AlertTrapCallSnapshot>,
        missing_call: AlertTrapCallSnapshot,
        resource_prep: AlertResourcePrepSnapshot,
    }

    #[derive(Debug, PartialEq, Eq)]
    struct DialogInitSoundSnapshot {
        init_stack_after: u32,
        resume_proc_after_init: u32,
        da_beeper_after_init: u32,
        alert_stage_after_init: u16,
        anumber_after_init: u16,
        da_strings_after_init: [u32; 4],
        error_sound_stack_after: u32,
        da_beeper_after_error_sound: u32,
        nil_error_sound_stack_after: u32,
        da_beeper_after_nil_error_sound: u32,
    }

    #[derive(Debug, PartialEq, Eq)]
    struct DialogDispatchDefaultCancelSnapshot {
        default_result: u16,
        default_stack_after: u32,
        default_adef_item: u16,
        default_tracking_item: i16,
        cancel_result: u16,
        cancel_stack_after: u32,
        cancel_map_item: Option<i16>,
        cancel_tracking_item: i16,
        tracks_result: u16,
        tracks_stack_after: u32,
        tracks_adef_item: u16,
        tracks_cancel_map_item: Option<i16>,
        tracks_tracking_items: (i16, i16),
    }

    #[derive(Debug, PartialEq, Eq)]
    struct UpdtDialogUpdateRegionSnapshot {
        stack_after: u32,
        the_port_after: u32,
        current_port_after: u32,
        deferred_after: bool,
        queued_draw_procs: Vec<(u32, u32, i16)>,
        item_count_after: usize,
        inside_update_pixel_after: u8,
        outside_update_pixel_after: u8,
        outside_item_pixel_after: u8,
    }

    #[derive(Debug, PartialEq, Eq)]
    struct DialogSelectEditTextKeySnapshot {
        keydown_result: u16,
        keydown_dialog_out: u32,
        keydown_item_hit: u16,
        keydown_stack_after: u32,
        keydown_handle_bytes: Vec<u8>,
        keydown_cached_text: String,
        keydown_item_selection: (i16, i16),
        keydown_te_text: Vec<u8>,
        keydown_te_length: u16,
        keydown_te_selection: (u16, u16),
        autokey_result: u16,
        autokey_dialog_out: u32,
        autokey_item_hit: u16,
        autokey_stack_after: u32,
        autokey_handle_bytes: Vec<u8>,
        autokey_cached_text: String,
        autokey_item_selection: (i16, i16),
        autokey_te_text: Vec<u8>,
        autokey_te_length: u16,
        autokey_te_selection: (u16, u16),
    }

    #[derive(Debug, PartialEq, Eq)]
    struct TextEditScrapEditCase {
        text: Vec<u8>,
        selection: (u16, u16),
        scrap_length: u16,
        scrap_bytes: Vec<u8>,
        stack_after: u32,
    }

    #[derive(Debug, PartialEq, Eq)]
    struct TextEditPrivateScrapEditingSnapshot {
        copy: TextEditScrapEditCase,
        cut: TextEditScrapEditCase,
        paste: TextEditScrapEditCase,
        delete: TextEditScrapEditCase,
    }

    fn paramtext_da_strings(bus: &MacMemoryBus) -> ([u32; 4], [Vec<u8>; 4]) {
        let mut handles = [0u32; 4];
        let mut strings: [Vec<u8>; 4] = std::array::from_fn(|_| Vec::new());
        for i in 0..4 {
            let handle = bus.read_long(crate::memory::globals::addr::DA_STRINGS + i as u32 * 4);
            handles[i] = handle;
            if handle != 0 {
                let data_ptr = bus.read_long(handle);
                if data_ptr != 0 {
                    strings[i] = bus.read_pstring(data_ptr);
                }
            }
        }
        (handles, strings)
    }

    fn paramtext_results_for_theme(theme_id: UiThemeId) -> ParamTextSnapshot {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.set_ui_theme_id(theme_id);
        let p0 = bus.alloc(32);
        let p1 = bus.alloc(32);
        let p2 = bus.alloc(32);
        let p3 = bus.alloc(32);

        bus.write_pstring(p0, b"Doc");
        bus.write_pstring(p1, b"42");
        bus.write_pstring(p2, b"");
        bus.write_pstring(p3, b"Tail");

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, p3);
        bus.write_long(TEST_SP + 4, p2);
        bus.write_long(TEST_SP + 8, p1);
        bus.write_long(TEST_SP + 12, p0);
        disp.dispatch_dialog(true, 0x18B, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        let initial_slots = [
            disp.param_text[0].clone(),
            disp.param_text[1].clone(),
            disp.param_text[2].clone(),
            disp.param_text[3].clone(),
        ];
        let (initial_da_handles, initial_da_strings) = paramtext_da_strings(&bus);
        let initial_stack_after = cpu.read_reg(Register::A7);
        let initial_substitution = disp
            .apply_param_text("Cannot open ^0 (^1)^2^3 ^9")
            .into_owned();

        let new0 = bus.alloc(32);
        bus.write_pstring(new0, b"NewDoc");
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, 0); // param3 = NIL; preserve previous slot
        bus.write_long(TEST_SP + 4, 0); // param2 = NIL; preserve previous slot
        bus.write_long(TEST_SP + 8, 0); // param1 = NIL; preserve previous slot
        bus.write_long(TEST_SP + 12, new0);
        disp.dispatch_dialog(true, 0x18B, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        let nil_slots = [
            disp.param_text[0].clone(),
            disp.param_text[1].clone(),
            disp.param_text[2].clone(),
            disp.param_text[3].clone(),
        ];
        let (nil_da_handles, nil_da_strings) = paramtext_da_strings(&bus);
        let nil_stack_after = cpu.read_reg(Register::A7);
        let nil_substitution = disp
            .apply_param_text("Cannot open ^0 (^1)^2^3 ^9")
            .into_owned();

        ParamTextSnapshot {
            initial_slots,
            initial_da_handles,
            initial_da_strings,
            initial_stack_after,
            initial_substitution,
            nil_slots,
            nil_da_handles,
            nil_da_strings,
            nil_stack_after,
            nil_substitution,
        }
    }

    fn dialog_item_text_results_for_theme(theme_id: UiThemeId) -> DialogItemTextSnapshot {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.set_ui_theme_id(theme_id);
        let dialog_ptr = bus.alloc(170);
        let text_handle = bus.alloc(4);
        let text_ptr = bus.alloc(3);
        let out_ptr = bus.alloc(256);
        let new_text_ptr = bus.alloc(32);

        bus.write_long(text_handle, text_ptr);
        bus.write_bytes(text_ptr, b"Old");
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 16,
                rect: (10, 20, 30, 120),
                text: "Old".to_string(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );
        disp.dialog_item_handles
            .insert(text_handle, (dialog_ptr, 0));

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, out_ptr);
        bus.write_long(TEST_SP + 4, text_handle);
        disp.dispatch_dialog(true, 0x190, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let initial_get_text = bus.read_pstring(out_ptr);
        let initial_get_stack_after = cpu.read_reg(Register::A7);

        bus.write_pstring(new_text_ptr, b"Edited Text");
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, new_text_ptr);
        bus.write_long(TEST_SP + 4, text_handle);
        disp.dispatch_dialog(true, 0x18F, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let set_stack_after = cpu.read_reg(Register::A7);
        let data_ptr_after_set = bus.read_long(text_handle);
        let handle_size_after_set = bus.get_alloc_size(data_ptr_after_set);
        let handle_bytes_after_set = bus.read_bytes(
            data_ptr_after_set,
            handle_size_after_set.unwrap_or(0) as usize,
        );
        let cached_text_after_set = disp.dialog_items.get(&dialog_ptr).unwrap()[0].text.clone();

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, out_ptr);
        bus.write_long(TEST_SP + 4, text_handle);
        disp.dispatch_dialog(true, 0x190, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let post_set_get_text = bus.read_pstring(out_ptr);
        let post_set_get_stack_after = cpu.read_reg(Register::A7);

        DialogItemTextSnapshot {
            initial_get_text,
            initial_get_stack_after,
            set_stack_after,
            handle_size_after_set,
            handle_bytes_after_set,
            cached_text_after_set,
            post_set_get_text,
            post_set_get_stack_after,
        }
    }

    fn select_dialog_item_text_results_for_theme(
        theme_id: UiThemeId,
    ) -> DialogItemTextSelectionSnapshot {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.set_ui_theme_id(theme_id);
        let dialog_ptr = bus.alloc(170);
        let text_handle = bus.alloc(4);
        let te_ptr = bus.alloc(0x40);

        bus.write_long(dialog_ptr + 160, text_handle);
        bus.write_long(text_handle, te_ptr);
        bus.write_word(dialog_ptr + 164, 0xFFFF);
        disp.dialog_items.insert(
            dialog_ptr,
            vec![
                DialogItem {
                    item_type: 16,
                    rect: (10, 20, 30, 120),
                    text: "ABCDE".to_string(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 1,
                    sel_end: 1,
                },
                DialogItem {
                    item_type: 8,
                    rect: (34, 20, 50, 120),
                    text: "STATIC".to_string(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 1,
                    sel_end: 3,
                },
            ],
        );

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 32767); // endSel
        bus.write_word(TEST_SP + 2, 0); // strtSel
        bus.write_word(TEST_SP + 4, 1); // itemNo
        bus.write_long(TEST_SP + 6, dialog_ptr); // theDialog
        disp.dispatch_dialog(true, 0x17E, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let item = &disp.dialog_items[&dialog_ptr][0];
        let whole_selection = (item.sel_start, item.sel_end);
        let whole_edit_field = bus.read_word(dialog_ptr + 164);
        let whole_te_selection = (
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
        );
        let whole_stack_after = cpu.read_reg(Register::A7);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 2); // endSel
        bus.write_word(TEST_SP + 2, 9); // strtSel, beyond text length
        bus.write_word(TEST_SP + 4, 1); // itemNo
        bus.write_long(TEST_SP + 6, dialog_ptr); // theDialog
        disp.dispatch_dialog(true, 0x17E, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let item = &disp.dialog_items[&dialog_ptr][0];
        let normalized_selection = (item.sel_start, item.sel_end);
        let normalized_edit_field = bus.read_word(dialog_ptr + 164);
        let normalized_te_selection = (
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
        );
        let normalized_stack_after = cpu.read_reg(Register::A7);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 4); // endSel
        bus.write_word(TEST_SP + 2, 0); // strtSel
        bus.write_word(TEST_SP + 4, 2); // non-edit itemNo
        bus.write_long(TEST_SP + 6, dialog_ptr); // theDialog
        disp.dispatch_dialog(true, 0x17E, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let item = &disp.dialog_items[&dialog_ptr][1];
        let non_edit_selection = (item.sel_start, item.sel_end);
        let non_edit_edit_field = bus.read_word(dialog_ptr + 164);
        let non_edit_te_selection = (
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
        );
        let non_edit_stack_after = cpu.read_reg(Register::A7);

        DialogItemTextSelectionSnapshot {
            whole_selection,
            whole_edit_field,
            whole_te_selection,
            whole_stack_after,
            normalized_selection,
            normalized_edit_field,
            normalized_te_selection,
            normalized_stack_after,
            non_edit_selection,
            non_edit_edit_field,
            non_edit_te_selection,
            non_edit_stack_after,
        }
    }

    fn is_dialog_event_results_for_theme(theme_id: UiThemeId) -> IsDialogEventSnapshot {
        fn dispatch_is_dialog_event(
            disp: &mut TrapDispatcher,
            cpu: &mut impl CpuOps,
            bus: &mut MacMemoryBus,
            event_ptr: u32,
            what: u16,
            message: u32,
            where_v: i16,
            where_h: i16,
        ) -> (u16, u32) {
            cpu.write_reg(Register::A7, TEST_SP);
            bus.write_word(event_ptr, what);
            bus.write_long(event_ptr + 2, message);
            bus.write_long(event_ptr + 6, 0);
            bus.write_word(event_ptr + 10, where_v as u16);
            bus.write_word(event_ptr + 12, where_h as u16);
            bus.write_word(event_ptr + 14, 0);
            bus.write_word(TEST_SP + 4, 0xBEEF);
            bus.write_long(TEST_SP, event_ptr);

            disp.dispatch_dialog(true, 0x17F, cpu, bus)
                .unwrap()
                .unwrap();
            (bus.read_word(TEST_SP + 4), cpu.read_reg(Register::A7))
        }

        let (mut disp, mut cpu, mut bus) = setup();
        disp.set_ui_theme_id(theme_id);
        let dialog_ptr = bus.alloc(170);
        let event_ptr = 0x300000u32;

        disp.front_window = dialog_ptr;
        bus.write_word(dialog_ptr + 8, (-100i16) as u16);
        bus.write_word(dialog_ptr + 10, (-200i16) as u16);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 80);
        bus.write_word(dialog_ptr + 22, 120);
        disp.dialog_items.insert(dialog_ptr, Vec::new());

        let null_event =
            dispatch_is_dialog_event(&mut disp, &mut cpu, &mut bus, event_ptr, 0, 0, 0, 0);
        let key_down = dispatch_is_dialog_event(
            &mut disp,
            &mut cpu,
            &mut bus,
            event_ptr,
            3,
            u32::from(b'A'),
            0,
            0,
        );
        let mouse_inside =
            dispatch_is_dialog_event(&mut disp, &mut cpu, &mut bus, event_ptr, 1, 0, 120, 240);
        let mouse_outside =
            dispatch_is_dialog_event(&mut disp, &mut cpu, &mut bus, event_ptr, 1, 0, 40, 40);
        let update_target = dispatch_is_dialog_event(
            &mut disp, &mut cpu, &mut bus, event_ptr, 6, dialog_ptr, 0, 0,
        );
        let activate_target = dispatch_is_dialog_event(
            &mut disp, &mut cpu, &mut bus, event_ptr, 8, dialog_ptr, 0, 0,
        );

        IsDialogEventSnapshot {
            null_event,
            key_down,
            mouse_inside,
            mouse_outside,
            update_target,
            activate_target,
        }
    }

    fn text_handle_bytes(bus: &MacMemoryBus, handle: u32) -> Vec<u8> {
        let ptr = bus.read_long(handle);
        if ptr == 0 {
            return Vec::new();
        }
        let len = bus.get_alloc_size(ptr).unwrap_or(0) as usize;
        bus.read_bytes(ptr, len)
    }

    fn textedit_scrap_contents(bus: &MacMemoryBus) -> (u16, Vec<u8>) {
        let scrap_length = bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH);
        let scrap_handle = bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE);
        if scrap_length == 0 || scrap_handle == 0 {
            return (scrap_length, Vec::new());
        }
        let scrap_ptr = bus.read_long(scrap_handle);
        if scrap_ptr == 0 {
            return (scrap_length, Vec::new());
        }
        (
            scrap_length,
            bus.read_bytes(scrap_ptr, scrap_length as usize),
        )
    }

    fn textedit_scrap_edit_case(
        bus: &MacMemoryBus,
        te_handle: u32,
        stack_after: u32,
    ) -> TextEditScrapEditCase {
        let te_ptr = bus.read_long(te_handle);
        let (scrap_length, scrap_bytes) = textedit_scrap_contents(bus);
        TextEditScrapEditCase {
            text: TrapDispatcher::te_text_bytes(bus, te_handle),
            selection: (
                bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
                bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
            ),
            scrap_length,
            scrap_bytes,
            stack_after,
        }
    }

    fn dialog_select_window_event_results_for_theme(
        theme_id: UiThemeId,
    ) -> DialogSelectWindowEventSnapshot {
        fn install_region(
            bus: &mut MacMemoryBus,
            handle: u32,
            data: u32,
            rect: (i16, i16, i16, i16),
        ) {
            bus.write_long(handle, data);
            bus.write_word(data, 10);
            bus.write_word(data + 2, rect.0 as u16);
            bus.write_word(data + 4, rect.1 as u16);
            bus.write_word(data + 6, rect.2 as u16);
            bus.write_word(data + 8, rect.3 as u16);
        }

        fn dispatch_dialog_select(
            disp: &mut TrapDispatcher,
            cpu: &mut impl CpuOps,
            bus: &mut MacMemoryBus,
            event_ptr: u32,
            dialog_out_ptr: u32,
            item_hit_ptr: u32,
            what: u16,
            message: u32,
        ) -> (u16, u32, u16, u32) {
            cpu.write_reg(Register::A7, TEST_SP);
            bus.write_word(event_ptr, what);
            bus.write_long(event_ptr + 2, message);
            bus.write_long(event_ptr + 6, 0);
            bus.write_word(event_ptr + 10, 0);
            bus.write_word(event_ptr + 12, 0);
            bus.write_word(event_ptr + 14, 0);
            bus.write_word(TEST_SP + 12, 0xBEEF);
            bus.write_long(dialog_out_ptr, 0xDEAD_BEEF);
            bus.write_word(item_hit_ptr, 0xCAFE);
            bus.write_long(TEST_SP, item_hit_ptr);
            bus.write_long(TEST_SP + 4, dialog_out_ptr);
            bus.write_long(TEST_SP + 8, event_ptr);

            disp.dispatch_dialog(true, 0x180, cpu, bus)
                .unwrap()
                .unwrap();

            (
                bus.read_word(TEST_SP + 12),
                bus.read_long(dialog_out_ptr),
                bus.read_word(item_hit_ptr),
                cpu.read_reg(Register::A7),
            )
        }

        let (mut disp, mut cpu, mut bus) = setup_with_port();
        disp.set_ui_theme_id(theme_id);
        let initial_port = 0x181000;
        disp.set_current_port_state(&mut bus, &mut cpu, initial_port, None);

        let screen_base = bus.alloc(100 * 100);
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 100, 100, 100, 8);

        let dialog_ptr = bus.alloc(256);
        let event_ptr = 0x300000u32;
        let dialog_out_ptr = 0x300100u32;
        let item_hit_ptr = 0x300104u32;

        let vis_rgn = bus.alloc(10);
        let vis_rgn_handle = bus.alloc(4);
        install_region(&mut bus, vis_rgn_handle, vis_rgn, (0, 0, 100, 100));
        let clip_rgn = bus.alloc(10);
        let clip_rgn_handle = bus.alloc(4);
        install_region(&mut bus, clip_rgn_handle, clip_rgn, (0, 0, 100, 100));
        let update_rgn = bus.alloc(10);
        let update_rgn_handle = bus.alloc(4);
        install_region(&mut bus, update_rgn_handle, update_rgn, (0, 0, 40, 40));

        disp.front_window = dialog_ptr;
        disp.window_list.push(dialog_ptr);
        bus.write_word(dialog_ptr, 0);
        bus.write_long(dialog_ptr + 2, screen_base);
        bus.write_word(dialog_ptr + 6, 100);
        bus.write_word(dialog_ptr + 8, 0);
        bus.write_word(dialog_ptr + 10, 0);
        bus.write_word(dialog_ptr + 12, 100);
        bus.write_word(dialog_ptr + 14, 100);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 100);
        bus.write_word(dialog_ptr + 22, 100);
        bus.write_long(dialog_ptr + 24, vis_rgn_handle);
        bus.write_long(dialog_ptr + 28, clip_rgn_handle);
        bus.write_word(dialog_ptr + 108, 2);
        bus.write_long(dialog_ptr + 122, update_rgn_handle);
        disp.dialog_items.insert(
            dialog_ptr,
            vec![
                DialogItem {
                    item_type: 0,
                    rect: (8, 8, 24, 32),
                    proc_ptr: 0x500000,
                    ..Default::default()
                },
                DialogItem {
                    item_type: 0,
                    rect: (60, 60, 80, 80),
                    proc_ptr: 0x600000,
                    ..Default::default()
                },
                DialogItem {
                    item_type: 8,
                    rect: (90, 90, 96, 96),
                    text: "x".to_string(),
                    ..Default::default()
                },
            ],
        );
        disp.dialog_initial_draw_deferred.insert(dialog_ptr);

        let (update_result, update_dialog_out, update_item_hit, update_stack_after) =
            dispatch_dialog_select(
                &mut disp,
                &mut cpu,
                &mut bus,
                event_ptr,
                dialog_out_ptr,
                item_hit_ptr,
                6,
                dialog_ptr,
            );
        let update_deferred_after = disp.dialog_initial_draw_deferred.contains(&dialog_ptr);
        let update_region_after =
            TrapDispatcher::region_handle_rect(&bus, bus.read_long(dialog_ptr + 122));
        let update_vis_region_after =
            TrapDispatcher::region_handle_rect(&bus, bus.read_long(dialog_ptr + 24));
        let update_the_port_after = bus.read_long(crate::memory::globals::addr::THE_PORT);
        let update_current_port_after = disp.current_port;
        let update_saved_vis_after = disp.saved_vis_regions.contains_key(&dialog_ptr);
        let update_queued_draw_procs = disp
            .modeless_dialog_draw_proc_queue
            .iter()
            .copied()
            .collect();
        let update_item_count_after = disp
            .dialog_items
            .get(&dialog_ptr)
            .map(Vec::len)
            .unwrap_or_default();

        let (activate_result, activate_dialog_out, activate_item_hit, activate_stack_after) =
            dispatch_dialog_select(
                &mut disp,
                &mut cpu,
                &mut bus,
                event_ptr,
                dialog_out_ptr,
                item_hit_ptr,
                8,
                dialog_ptr,
            );

        DialogSelectWindowEventSnapshot {
            dialog_ptr,
            update_result,
            update_dialog_out,
            update_item_hit,
            update_stack_after,
            update_deferred_after,
            update_region_after,
            update_vis_region_after,
            update_the_port_after,
            update_current_port_after,
            update_saved_vis_after,
            update_queued_draw_procs,
            update_item_count_after,
            activate_result,
            activate_dialog_out,
            activate_item_hit,
            activate_stack_after,
        }
    }

    fn dialog_select_edit_text_mouse_results_for_theme(
        theme_id: UiThemeId,
    ) -> DialogSelectEditTextMouseSnapshot {
        fn dispatch_dialog_select(
            disp: &mut TrapDispatcher,
            cpu: &mut impl CpuOps,
            bus: &mut MacMemoryBus,
            event_ptr: u32,
            dialog_out_ptr: u32,
            item_hit_ptr: u32,
            what: u16,
            where_v: i16,
            where_h: i16,
        ) -> (u16, u32, u16, u32) {
            cpu.write_reg(Register::A7, TEST_SP);
            bus.write_word(event_ptr, what);
            bus.write_long(event_ptr + 2, 0);
            bus.write_long(event_ptr + 6, 0);
            bus.write_word(event_ptr + 10, where_v as u16);
            bus.write_word(event_ptr + 12, where_h as u16);
            bus.write_word(event_ptr + 14, 0);
            bus.write_word(TEST_SP + 12, 0xBEEF);
            bus.write_long(dialog_out_ptr, 0xDEAD_BEEF);
            bus.write_word(item_hit_ptr, 0xCAFE);
            bus.write_long(TEST_SP, item_hit_ptr);
            bus.write_long(TEST_SP + 4, dialog_out_ptr);
            bus.write_long(TEST_SP + 8, event_ptr);

            disp.dispatch_dialog(true, 0x180, cpu, bus)
                .unwrap()
                .unwrap();

            (
                bus.read_word(TEST_SP + 12),
                bus.read_long(dialog_out_ptr),
                bus.read_word(item_hit_ptr),
                cpu.read_reg(Register::A7),
            )
        }

        fn write_ditl_item(
            bus: &mut MacMemoryBus,
            ditl_ptr: u32,
            offset: u32,
            handle: u32,
            rect: (i16, i16, i16, i16),
            item_type: u8,
        ) {
            bus.write_long(ditl_ptr + offset, handle);
            bus.write_word(ditl_ptr + offset + 4, rect.0 as u16);
            bus.write_word(ditl_ptr + offset + 6, rect.1 as u16);
            bus.write_word(ditl_ptr + offset + 8, rect.2 as u16);
            bus.write_word(ditl_ptr + offset + 10, rect.3 as u16);
            bus.write_byte(ditl_ptr + offset + 12, item_type);
            bus.write_byte(ditl_ptr + offset + 13, 0);
        }

        let (mut disp, mut cpu, mut bus) = setup();
        disp.set_ui_theme_id(theme_id);
        let dialog_ptr = bus.alloc(170);
        let event_ptr = 0x300000u32;
        let dialog_out_ptr = 0x300100u32;
        let item_hit_ptr = 0x300104u32;
        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(30);
        let item1_text_handle = bus.alloc(4);
        let item1_text_ptr = bus.alloc(5);
        let item2_text_handle = bus.alloc(4);
        let item2_text_ptr = bus.alloc(6);
        let text_h = TrapDispatcher::allocate_te_handle(&mut bus);

        disp.front_window = dialog_ptr;
        bus.write_word(dialog_ptr + 8, (-100i16) as u16);
        bus.write_word(dialog_ptr + 10, (-200i16) as u16);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 100);
        bus.write_word(dialog_ptr + 22, 160);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_long(dialog_ptr + 160, text_h);
        bus.write_word(dialog_ptr + 164, 0); // editField = first item

        bus.write_long(items_handle, ditl_ptr);
        bus.write_word(ditl_ptr, 1); // two items
        write_ditl_item(
            &mut bus,
            ditl_ptr,
            2,
            item1_text_handle,
            (20, 20, 38, 120),
            16,
        );
        write_ditl_item(
            &mut bus,
            ditl_ptr,
            16,
            item2_text_handle,
            (42, 20, 62, 120),
            16,
        );
        bus.write_long(item1_text_handle, item1_text_ptr);
        bus.write_bytes(item1_text_ptr, b"First");
        bus.write_long(item2_text_handle, item2_text_ptr);
        bus.write_bytes(item2_text_ptr, b"Second");

        disp.te_set_text_contents(&mut bus, text_h, b"First");
        let te_ptr = bus.read_long(text_h);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 1);
        disp.dialog_items.insert(
            dialog_ptr,
            vec![
                DialogItem {
                    item_type: 16,
                    rect: (20, 20, 38, 120),
                    text: "First".to_string(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 1,
                    sel_end: 1,
                },
                DialogItem {
                    item_type: 16,
                    rect: (42, 20, 62, 120),
                    text: "Second".to_string(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 2,
                    sel_end: 4,
                },
            ],
        );

        let (mouse_result, mouse_dialog_out, mouse_item_hit, mouse_stack_after) =
            dispatch_dialog_select(
                &mut disp,
                &mut cpu,
                &mut bus,
                event_ptr,
                dialog_out_ptr,
                item_hit_ptr,
                1,
                150,
                240,
            );
        let mouse_edit_field = bus.read_word(dialog_ptr + 164);
        let mouse_item = &disp.dialog_items[&dialog_ptr][1];
        let mouse_item_selection = (mouse_item.sel_start, mouse_item.sel_end);
        let mouse_te_text = TrapDispatcher::te_text_bytes(&bus, text_h);
        let mouse_te_length = bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET);
        let mouse_te_selection = (
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
        );

        let (null_result, null_dialog_out, null_item_hit, null_stack_after) =
            dispatch_dialog_select(
                &mut disp,
                &mut cpu,
                &mut bus,
                event_ptr,
                dialog_out_ptr,
                item_hit_ptr,
                0,
                0,
                0,
            );
        let null_edit_field = bus.read_word(dialog_ptr + 164);
        let null_te_text = TrapDispatcher::te_text_bytes(&bus, text_h);
        let null_te_length = bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET);
        let null_te_selection = (
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
        );

        DialogSelectEditTextMouseSnapshot {
            mouse_result,
            mouse_dialog_out,
            mouse_item_hit,
            mouse_stack_after,
            mouse_edit_field,
            mouse_item_selection,
            mouse_te_text,
            mouse_te_length,
            mouse_te_selection,
            null_result,
            null_dialog_out,
            null_item_hit,
            null_stack_after,
            null_edit_field,
            null_te_text,
            null_te_length,
            null_te_selection,
        }
    }

    fn modal_dialog_edit_text_mouse_results_for_theme(
        theme_id: UiThemeId,
    ) -> ModalDialogEditTextMouseSnapshot {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.set_ui_theme_id(theme_id);
        let dialog_ptr = bus.alloc(170);
        let item_hit_ptr = 0x300100u32;
        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(30);
        let item1_text_handle = bus.alloc(4);
        let item1_text_ptr = bus.alloc(5);
        let item2_text_handle = bus.alloc(4);
        let item2_text_ptr = bus.alloc(6);
        let text_h = TrapDispatcher::allocate_te_handle(&mut bus);

        bus.write_word(dialog_ptr + 8, (-100i16) as u16);
        bus.write_word(dialog_ptr + 10, (-200i16) as u16);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 100);
        bus.write_word(dialog_ptr + 22, 160);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_long(dialog_ptr + 160, text_h);
        bus.write_word(dialog_ptr + 164, 0); // editField = first item

        bus.write_long(items_handle, ditl_ptr);
        bus.write_word(ditl_ptr, 1); // two items
        bus.write_long(ditl_ptr + 2, item1_text_handle);
        bus.write_word(ditl_ptr + 6, 20);
        bus.write_word(ditl_ptr + 8, 20);
        bus.write_word(ditl_ptr + 10, 38);
        bus.write_word(ditl_ptr + 12, 120);
        bus.write_byte(ditl_ptr + 14, 16);
        bus.write_byte(ditl_ptr + 15, 0);
        bus.write_long(ditl_ptr + 16, item2_text_handle);
        bus.write_word(ditl_ptr + 20, 42);
        bus.write_word(ditl_ptr + 22, 20);
        bus.write_word(ditl_ptr + 24, 62);
        bus.write_word(ditl_ptr + 26, 120);
        bus.write_byte(ditl_ptr + 28, 16);
        bus.write_byte(ditl_ptr + 29, 0);

        bus.write_long(item1_text_handle, item1_text_ptr);
        bus.write_bytes(item1_text_ptr, b"First");
        bus.write_long(item2_text_handle, item2_text_ptr);
        bus.write_bytes(item2_text_ptr, b"Second");
        disp.te_set_text_contents(&mut bus, text_h, b"First");
        let te_ptr = bus.read_long(text_h);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 1);

        let items = vec![
            DialogItem {
                item_type: 16,
                rect: (20, 20, 38, 120),
                text: "First".to_string(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 1,
                sel_end: 1,
            },
            DialogItem {
                item_type: 16,
                rect: (42, 20, 62, 120),
                text: "Second".to_string(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 2,
                sel_end: 4,
            },
        ];
        disp.dialog_items.insert(dialog_ptr, items.clone());
        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
            dialog_ptr,
            bounds: (100, 200, 200, 360),
            title: String::new(),
            proc_id: 2,
            items,
            default_item: 0,
            cancel_item: 0,
            edit_text: "First".to_string(),
            edit_item: 1,
            saved_pixels: vec![0x11, 0x22, 0x33],
            stack_ptr: TEST_SP,
            item_hit_ptr,
            rendered_pixels: Vec::new(),
            flash_remaining: 0,
            flash_delay: 0,
            flash_item: 0,
            edit_text_modified: false,
            draw_proc_queue: VecDeque::new(),
            draw_procs_done: true,
            rendered_pixels_final: true,
            filter_proc: 0,
            game_managed: false,
            last_filter_event: None,
            popup_draws: Vec::new(),
            active_popup: None,
            active_button: None,
            active_user_item: None,
        });
        bus.write_word(item_hit_ptr, 0xCAFE);
        cpu.write_reg(Register::A7, TEST_SP);
        disp.mouse_button = true;
        disp.mouse_pos = (150, 240);
        disp.event_queue
            .push_back(crate::trap::dispatch::QueuedEvent {
                what: 1,
                message: 0,
                where_v: 150,
                where_h: 240,
                modifiers: 0,
            });
        disp.event_queue
            .push_back(crate::trap::dispatch::QueuedEvent {
                what: 2,
                message: 0,
                where_v: 150,
                where_h: 240,
                modifiers: 0,
            });

        disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let item = &disp.dialog_items[&dialog_ptr][1];

        ModalDialogEditTextMouseSnapshot {
            item_hit: bus.read_word(item_hit_ptr),
            stack_after: cpu.read_reg(Register::A7),
            tracking_finished: disp.dialog_tracking.is_none(),
            retained_visible_snapshot: disp.dialog_visible_snapshots.contains_key(&dialog_ptr),
            saved_background_retained: disp.dialog_saved_pixels.contains_key(&dialog_ptr),
            queued_mouse_up_consumed: !disp.event_queue.iter().any(|event| event.what == 2),
            edit_field: bus.read_word(dialog_ptr + 164),
            item_selection: (item.sel_start, item.sel_end),
            te_text: TrapDispatcher::te_text_bytes(&bus, text_h),
            te_length: bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET),
            te_selection: (
                bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
                bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
            ),
            handle_bytes: text_handle_bytes(&bus, item2_text_handle),
        }
    }

    fn modal_dialog_keyboard_button_results_for_theme(
        theme_id: UiThemeId,
    ) -> ModalDialogKeyboardButtonSnapshot {
        fn run_key(
            theme_id: UiThemeId,
            key_code: u8,
            char_code: u8,
            modifiers: u16,
        ) -> ModalDialogKeyboardButtonCase {
            let (mut disp, mut cpu, mut bus) = setup_with_port();
            disp.set_ui_theme_id(theme_id);
            let dialog_ptr = 0x200000u32;
            let item_hit_ptr = 0x300000u32;
            let items = vec![
                DialogItem {
                    item_type: 4,
                    rect: (20, 30, 60, 110),
                    text: "OK".to_string(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
                DialogItem {
                    item_type: 4,
                    rect: (20, 124, 60, 214),
                    text: "Cancel".to_string(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
            ];
            disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
                dialog_ptr,
                bounds: (100, 200, 200, 440),
                title: String::new(),
                proc_id: 2,
                items,
                default_item: 1,
                cancel_item: 2,
                edit_text: String::new(),
                edit_item: 0,
                saved_pixels: Vec::new(),
                stack_ptr: TEST_SP,
                item_hit_ptr,
                rendered_pixels: Vec::new(),
                flash_remaining: 0,
                flash_delay: 0,
                flash_item: 0,
                edit_text_modified: false,
                draw_proc_queue: VecDeque::new(),
                draw_procs_done: true,
                rendered_pixels_final: true,
                filter_proc: 0,
                game_managed: false,
                last_filter_event: None,
                popup_draws: Vec::new(),
                active_popup: None,
                active_button: None,
                active_user_item: None,
            });
            disp.event_queue
                .push_back(crate::trap::dispatch::QueuedEvent {
                    what: 3,
                    message: (u32::from(key_code) << 8) | u32::from(char_code),
                    where_v: 0,
                    where_h: 0,
                    modifiers,
                });
            bus.write_word(item_hit_ptr, 0xCAFE);
            cpu.write_reg(Register::A7, TEST_SP);

            disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus)
                .unwrap()
                .unwrap();
            let flash_item_after_key = disp
                .dialog_tracking
                .as_ref()
                .map(|tracking| tracking.flash_item)
                .unwrap_or(0);
            let stack_after_key = cpu.read_reg(Register::A7);
            let item_hit_after_key = bus.read_word(item_hit_ptr);

            {
                let tracking = disp.dialog_tracking.as_mut().unwrap();
                tracking.flash_remaining = 1;
                tracking.flash_delay = 0;
            }
            disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus)
                .unwrap()
                .unwrap();

            ModalDialogKeyboardButtonCase {
                flash_item_after_key,
                stack_after_key,
                item_hit_after_key,
                item_hit_after_flash: bus.read_word(item_hit_ptr),
                stack_after_flash: cpu.read_reg(Register::A7),
                tracking_finished: disp.dialog_tracking.is_none(),
            }
        }

        ModalDialogKeyboardButtonSnapshot {
            return_key: run_key(theme_id, 0x24, 0x0D, 0),
            enter_key: run_key(theme_id, 0x4C, 0x03, 0),
            escape_key: run_key(theme_id, 0x35, 0x1B, 0),
            command_period: run_key(theme_id, 0x2F, b'.', 0x0100),
        }
    }

    fn modal_dialog_update_event_results_for_theme(
        theme_id: UiThemeId,
    ) -> ModalDialogUpdateEventSnapshot {
        fn install_region(
            bus: &mut MacMemoryBus,
            handle: u32,
            data: u32,
            rect: (i16, i16, i16, i16),
        ) {
            bus.write_long(handle, data);
            bus.write_word(data, 10);
            bus.write_word(data + 2, rect.0 as u16);
            bus.write_word(data + 4, rect.1 as u16);
            bus.write_word(data + 6, rect.2 as u16);
            bus.write_word(data + 8, rect.3 as u16);
        }

        let (mut disp, mut cpu, mut bus) = setup_with_port();
        disp.set_ui_theme_id(theme_id);

        let initial_port = 0x181000;
        disp.set_current_port_state(&mut bus, &mut cpu, initial_port, None);

        let screen_base = bus.alloc(80 * 80);
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 80, 80, 80, 8);

        let dialog_ptr = bus.alloc(256);
        let item_hit_ptr = 0x300100u32;
        let bounds = (10, 10, 40, 40);
        let snapshot_width = (bounds.3 - bounds.1 + TrapDispatcher::DBOX_FRAME_MARGIN * 2) as usize;
        let snapshot_height =
            (bounds.2 - bounds.0 + TrapDispatcher::DBOX_FRAME_MARGIN * 2) as usize;
        let snapshot_index = (17 - (bounds.0 - TrapDispatcher::DBOX_FRAME_MARGIN)) as usize
            * snapshot_width
            + (19 - (bounds.1 - TrapDispatcher::DBOX_FRAME_MARGIN)) as usize;
        let mut rendered_pixels = vec![0x00; snapshot_width * snapshot_height];
        rendered_pixels[snapshot_index] = 0x44;

        let vis_rgn = bus.alloc(10);
        let vis_rgn_handle = bus.alloc(4);
        install_region(&mut bus, vis_rgn_handle, vis_rgn, (0, 0, 80, 80));
        let clip_rgn = bus.alloc(10);
        let clip_rgn_handle = bus.alloc(4);
        install_region(&mut bus, clip_rgn_handle, clip_rgn, (0, 0, 80, 80));
        let update_rgn = bus.alloc(10);
        let update_rgn_handle = bus.alloc(4);
        install_region(&mut bus, update_rgn_handle, update_rgn, (0, 0, 40, 40));

        bus.write_word(dialog_ptr, 0);
        bus.write_long(dialog_ptr + 2, screen_base);
        bus.write_word(dialog_ptr + 6, 80);
        bus.write_word(dialog_ptr + 8, 0);
        bus.write_word(dialog_ptr + 10, 0);
        bus.write_word(dialog_ptr + 12, 80);
        bus.write_word(dialog_ptr + 14, 80);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 80);
        bus.write_word(dialog_ptr + 22, 80);
        bus.write_long(dialog_ptr + 24, vis_rgn_handle);
        bus.write_long(dialog_ptr + 28, clip_rgn_handle);
        bus.write_long(dialog_ptr + 122, update_rgn_handle);

        bus.write_word(item_hit_ptr, 0xCAFE);
        disp.front_window = dialog_ptr;
        disp.window_bounds = bounds;
        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
            dialog_ptr,
            bounds,
            title: String::new(),
            proc_id: 2,
            items: Vec::new(),
            default_item: 0,
            cancel_item: 0,
            edit_text: String::new(),
            edit_item: 0,
            saved_pixels: Vec::new(),
            stack_ptr: TEST_SP,
            item_hit_ptr,
            rendered_pixels,
            flash_remaining: 0,
            flash_delay: 0,
            flash_item: 0,
            edit_text_modified: false,
            draw_proc_queue: VecDeque::new(),
            draw_procs_done: true,
            rendered_pixels_final: true,
            filter_proc: 0,
            game_managed: false,
            last_filter_event: None,
            popup_draws: Vec::new(),
            active_popup: None,
            active_button: None,
            active_user_item: None,
        });
        disp.event_queue
            .push_back(crate::trap::dispatch::QueuedEvent {
                what: 6,
                message: dialog_ptr,
                where_v: 0,
                where_h: 0,
                modifiers: 0,
            });
        cpu.write_reg(Register::A7, TEST_SP);

        disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        let tracking = disp.dialog_tracking.as_ref().unwrap();
        ModalDialogUpdateEventSnapshot {
            dialog_ptr,
            item_hit_after: bus.read_word(item_hit_ptr),
            stack_after: cpu.read_reg(Register::A7),
            tracking_finished: disp.dialog_tracking.is_none(),
            rendered_pixels_final: tracking.rendered_pixels_final,
            retained_pixel_after: bus.read_byte(screen_base + 17 * 80 + 19),
            retained_snapshot_pixel_after: tracking.rendered_pixels[snapshot_index],
            update_region_after: TrapDispatcher::region_handle_rect(
                &bus,
                bus.read_long(dialog_ptr + 122),
            ),
            vis_region_after: TrapDispatcher::region_handle_rect(
                &bus,
                bus.read_long(dialog_ptr + 24),
            ),
            the_port_after: bus.read_long(crate::memory::globals::addr::THE_PORT),
            current_port_after: disp.current_port,
            saved_vis_after: disp.saved_vis_regions.contains_key(&dialog_ptr),
        }
    }

    fn dialog_select_edit_text_key_results_for_theme(
        theme_id: UiThemeId,
    ) -> DialogSelectEditTextKeySnapshot {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.set_ui_theme_id(theme_id);
        let dialog_ptr = bus.alloc(170);
        let event_ptr = 0x300000u32;
        let dialog_out_ptr = 0x300100u32;
        let item_hit_ptr = 0x300104u32;
        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(16);
        let item_text_handle = bus.alloc(4);
        let item_text_ptr = bus.alloc(5);
        let text_h = TrapDispatcher::allocate_te_handle(&mut bus);

        disp.front_window = dialog_ptr;
        bus.write_word(dialog_ptr + 8, 0);
        bus.write_word(dialog_ptr + 10, 0);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 100);
        bus.write_word(dialog_ptr + 22, 160);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_long(dialog_ptr + 160, text_h);
        bus.write_word(dialog_ptr + 164, 0); // editField = first item
        bus.write_long(items_handle, ditl_ptr);
        bus.write_word(ditl_ptr, 0); // one item
        bus.write_long(ditl_ptr + 2, item_text_handle);
        bus.write_word(ditl_ptr + 6, 20);
        bus.write_word(ditl_ptr + 8, 30);
        bus.write_word(ditl_ptr + 10, 60);
        bus.write_word(ditl_ptr + 12, 110);
        bus.write_byte(ditl_ptr + 14, 16); // editText
        bus.write_byte(ditl_ptr + 15, 0);
        bus.write_long(item_text_handle, item_text_ptr);
        bus.write_bytes(item_text_ptr, b"Hello");

        disp.te_set_text_contents(&mut bus, text_h, b"Hello");
        let te_ptr = bus.read_long(text_h);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 16,
                rect: (20, 30, 60, 110),
                text: "Hello".to_string(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 1,
                sel_end: 4,
            }],
        );

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(event_ptr, 3); // keyDown
        bus.write_long(event_ptr + 2, u32::from(b'Y'));
        bus.write_long(event_ptr + 6, 0);
        bus.write_word(event_ptr + 10, 0);
        bus.write_word(event_ptr + 12, 0);
        bus.write_word(event_ptr + 14, 0);
        bus.write_word(TEST_SP + 12, 0xBEEF);
        bus.write_long(dialog_out_ptr, 0xDEAD_BEEF);
        bus.write_word(item_hit_ptr, 0xCAFE);
        bus.write_long(TEST_SP, item_hit_ptr);
        bus.write_long(TEST_SP + 4, dialog_out_ptr);
        bus.write_long(TEST_SP + 8, event_ptr);
        disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let keydown_result = bus.read_word(TEST_SP + 12);
        let keydown_dialog_out = bus.read_long(dialog_out_ptr);
        let keydown_item_hit = bus.read_word(item_hit_ptr);
        let keydown_stack_after = cpu.read_reg(Register::A7);
        let keydown_handle_bytes = text_handle_bytes(&bus, item_text_handle);
        let keydown_cached_text = disp.dialog_items[&dialog_ptr][0].text.clone();
        let keydown_item_selection = (
            disp.dialog_items[&dialog_ptr][0].sel_start,
            disp.dialog_items[&dialog_ptr][0].sel_end,
        );
        let keydown_te_text = TrapDispatcher::te_text_bytes(&bus, text_h);
        let keydown_te_length = bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET);
        let keydown_te_selection = (
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
        );

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(event_ptr, 5); // autoKey
        bus.write_long(event_ptr + 2, 0x0000_0008); // backspace
        bus.write_word(TEST_SP + 12, 0xBEEF);
        bus.write_long(dialog_out_ptr, 0xDEAD_BEEF);
        bus.write_word(item_hit_ptr, 0xCAFE);
        bus.write_long(TEST_SP, item_hit_ptr);
        bus.write_long(TEST_SP + 4, dialog_out_ptr);
        bus.write_long(TEST_SP + 8, event_ptr);
        disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let autokey_result = bus.read_word(TEST_SP + 12);
        let autokey_dialog_out = bus.read_long(dialog_out_ptr);
        let autokey_item_hit = bus.read_word(item_hit_ptr);
        let autokey_stack_after = cpu.read_reg(Register::A7);
        let autokey_handle_bytes = text_handle_bytes(&bus, item_text_handle);
        let autokey_cached_text = disp.dialog_items[&dialog_ptr][0].text.clone();
        let autokey_item_selection = (
            disp.dialog_items[&dialog_ptr][0].sel_start,
            disp.dialog_items[&dialog_ptr][0].sel_end,
        );
        let autokey_te_text = TrapDispatcher::te_text_bytes(&bus, text_h);
        let autokey_te_length = bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET);
        let autokey_te_selection = (
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
        );

        DialogSelectEditTextKeySnapshot {
            keydown_result,
            keydown_dialog_out,
            keydown_item_hit,
            keydown_stack_after,
            keydown_handle_bytes,
            keydown_cached_text,
            keydown_item_selection,
            keydown_te_text,
            keydown_te_length,
            keydown_te_selection,
            autokey_result,
            autokey_dialog_out,
            autokey_item_hit,
            autokey_stack_after,
            autokey_handle_bytes,
            autokey_cached_text,
            autokey_item_selection,
            autokey_te_text,
            autokey_te_length,
            autokey_te_selection,
        }
    }

    fn getsetditem_useritem_record_results_for_theme(
        theme_id: UiThemeId,
    ) -> DItemUserItemRecordSnapshot {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.set_ui_theme_id(theme_id);
        let dialog_ptr = bus.alloc(170);
        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(16);
        let initial_proc_ptr = 0x0012_3456u32;
        let new_proc_ptr = 0x00AB_CDEFu32;
        let set_box_ptr = bus.alloc(8);
        let get_box_ptr = bus.alloc(8);
        let get_item_ptr = bus.alloc(4);
        let get_type_ptr = bus.alloc(2);

        bus.write_long(items_handle, ditl_ptr);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_word(ditl_ptr, 0); // one item
        bus.write_long(ditl_ptr + 2, initial_proc_ptr);
        bus.write_word(ditl_ptr + 6, 12);
        bus.write_word(ditl_ptr + 8, 24);
        bus.write_word(ditl_ptr + 10, 36);
        bus.write_word(ditl_ptr + 12, 48);
        bus.write_byte(ditl_ptr + 14, 0);
        bus.write_byte(ditl_ptr + 15, 0);
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 0,
                rect: (12, 24, 36, 48),
                text: String::new(),
                resource_id: 0,
                proc_ptr: initial_proc_ptr,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, get_box_ptr);
        bus.write_long(TEST_SP + 4, get_item_ptr);
        bus.write_long(TEST_SP + 8, get_type_ptr);
        bus.write_word(TEST_SP + 12, 1);
        bus.write_long(TEST_SP + 14, dialog_ptr);
        disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let initial_get = DItemGetSnapshot {
            item_type: bus.read_word(get_type_ptr),
            item: bus.read_long(get_item_ptr),
            rect: (
                bus.read_word(get_box_ptr),
                bus.read_word(get_box_ptr + 2),
                bus.read_word(get_box_ptr + 4),
                bus.read_word(get_box_ptr + 6),
            ),
            stack_after: cpu.read_reg(Register::A7),
        };

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(set_box_ptr, 50);
        bus.write_word(set_box_ptr + 2, 60);
        bus.write_word(set_box_ptr + 4, 70);
        bus.write_word(set_box_ptr + 6, 80);
        bus.write_long(TEST_SP, set_box_ptr);
        bus.write_long(TEST_SP + 4, new_proc_ptr);
        bus.write_word(TEST_SP + 8, 0);
        bus.write_word(TEST_SP + 10, 1);
        bus.write_long(TEST_SP + 12, dialog_ptr);
        disp.dispatch_dialog(true, 0x18E, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let set_stack_after = cpu.read_reg(Register::A7);
        let stored_item = disp
            .dialog_items
            .get(&dialog_ptr)
            .and_then(|items| items.first())
            .cloned()
            .unwrap();

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, get_box_ptr);
        bus.write_long(TEST_SP + 4, get_item_ptr);
        bus.write_long(TEST_SP + 8, get_type_ptr);
        bus.write_word(TEST_SP + 12, 1);
        bus.write_long(TEST_SP + 14, dialog_ptr);
        disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let post_set_get = DItemGetSnapshot {
            item_type: bus.read_word(get_type_ptr),
            item: bus.read_long(get_item_ptr),
            rect: (
                bus.read_word(get_box_ptr),
                bus.read_word(get_box_ptr + 2),
                bus.read_word(get_box_ptr + 4),
                bus.read_word(get_box_ptr + 6),
            ),
            stack_after: cpu.read_reg(Register::A7),
        };

        DItemUserItemRecordSnapshot {
            initial_get,
            set_stack_after,
            stored_type: stored_item.item_type,
            stored_proc_ptr: stored_item.proc_ptr,
            stored_rect: stored_item.rect,
            post_set_get,
        }
    }

    fn get_ditem_snapshot_for_test(
        disp: &mut TrapDispatcher,
        cpu: &mut MockCpu,
        bus: &mut MacMemoryBus,
        dialog_ptr: u32,
        item_no: i16,
        box_ptr: u32,
        item_ptr: u32,
        type_ptr: u32,
    ) -> DItemGetSnapshot {
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, box_ptr);
        bus.write_long(TEST_SP + 4, item_ptr);
        bus.write_long(TEST_SP + 8, type_ptr);
        bus.write_word(TEST_SP + 12, item_no as u16);
        bus.write_long(TEST_SP + 14, dialog_ptr);
        disp.dispatch_dialog(true, 0x18D, cpu, bus)
            .unwrap()
            .unwrap();

        DItemGetSnapshot {
            item_type: bus.read_word(type_ptr),
            item: bus.read_long(item_ptr),
            rect: (
                bus.read_word(box_ptr),
                bus.read_word(box_ptr + 2),
                bus.read_word(box_ptr + 4),
                bus.read_word(box_ptr + 6),
            ),
            stack_after: cpu.read_reg(Register::A7),
        }
    }

    fn write_control_record_for_ditem_test(
        bus: &mut MacMemoryBus,
        handle: u32,
        ctrl_ptr: u32,
        dialog_ptr: u32,
        rect: (i16, i16, i16, i16),
        value: i16,
        title: &[u8],
    ) {
        bus.write_long(handle, ctrl_ptr);
        bus.write_long(ctrl_ptr, 0);
        bus.write_long(ctrl_ptr + 4, dialog_ptr);
        bus.write_word(ctrl_ptr + 8, rect.0 as u16);
        bus.write_word(ctrl_ptr + 10, rect.1 as u16);
        bus.write_word(ctrl_ptr + 12, rect.2 as u16);
        bus.write_word(ctrl_ptr + 14, rect.3 as u16);
        bus.write_byte(ctrl_ptr + 16, 255);
        bus.write_byte(ctrl_ptr + 17, 0);
        bus.write_word(ctrl_ptr + 18, value as u16);
        bus.write_word(ctrl_ptr + 20, 0);
        bus.write_word(ctrl_ptr + 22, 1);
        bus.write_byte(ctrl_ptr + 40, title.len() as u8);
        bus.write_bytes(ctrl_ptr + 41, title);
    }

    fn getsetditem_text_control_record_results_for_theme(
        theme_id: UiThemeId,
    ) -> DItemTextControlRecordSnapshot {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.set_ui_theme_id(theme_id);
        let dialog_ptr = bus.alloc(170);
        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(32);
        let old_text_handle = bus.alloc(4);
        let old_text_ptr = bus.alloc(3);
        let new_text_handle = bus.alloc(4);
        let new_text_ptr = bus.alloc(7);
        let old_control_handle = bus.alloc(4);
        let old_control_ptr = bus.alloc(44);
        let new_control_handle = bus.alloc(4);
        let new_control_ptr = bus.alloc(47);
        let text_set_box_ptr = bus.alloc(8);
        let control_set_box_ptr = bus.alloc(8);
        let get_box_ptr = bus.alloc(8);
        let get_item_ptr = bus.alloc(4);
        let get_type_ptr = bus.alloc(2);
        let text_entry = ditl_ptr + 2;
        let control_entry = ditl_ptr + 16;

        bus.write_bytes(old_text_ptr, b"Old");
        bus.write_bytes(new_text_ptr, b"Updated");
        bus.write_long(old_text_handle, old_text_ptr);
        bus.write_long(new_text_handle, new_text_ptr);
        write_control_record_for_ditem_test(
            &mut bus,
            old_control_handle,
            old_control_ptr,
            dialog_ptr,
            (30, 40, 50, 140),
            1,
            b"OK",
        );
        write_control_record_for_ditem_test(
            &mut bus,
            new_control_handle,
            new_control_ptr,
            dialog_ptr,
            (1, 2, 3, 4),
            2,
            b"Apply",
        );

        bus.write_long(items_handle, ditl_ptr);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_word(ditl_ptr, 1); // two items
        bus.write_long(text_entry, old_text_handle);
        bus.write_word(text_entry + 4, 10);
        bus.write_word(text_entry + 6, 20);
        bus.write_word(text_entry + 8, 24);
        bus.write_word(text_entry + 10, 160);
        bus.write_byte(text_entry + 12, 16);
        bus.write_byte(text_entry + 13, 0);
        bus.write_long(control_entry, old_control_handle);
        bus.write_word(control_entry + 4, 30);
        bus.write_word(control_entry + 6, 40);
        bus.write_word(control_entry + 8, 50);
        bus.write_word(control_entry + 10, 140);
        bus.write_byte(control_entry + 12, 4);
        bus.write_byte(control_entry + 13, 2);
        bus.write_bytes(control_entry + 14, b"OK");

        disp.dialog_items.insert(
            dialog_ptr,
            vec![
                DialogItem {
                    item_type: 16,
                    rect: (10, 20, 24, 160),
                    text: "Old".to_string(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
                DialogItem {
                    item_type: 4,
                    rect: (30, 40, 50, 140),
                    text: "OK".to_string(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
            ],
        );
        disp.dialog_item_handles
            .insert(old_text_handle, (dialog_ptr, 0));
        disp.dialog_control_handles
            .insert(old_control_handle, (dialog_ptr, 2));
        disp.dialog_control_values.insert((dialog_ptr, 2), 1);

        let text_initial_get = get_ditem_snapshot_for_test(
            &mut disp,
            &mut cpu,
            &mut bus,
            dialog_ptr,
            1,
            get_box_ptr,
            get_item_ptr,
            get_type_ptr,
        );
        let control_initial_get = get_ditem_snapshot_for_test(
            &mut disp,
            &mut cpu,
            &mut bus,
            dialog_ptr,
            2,
            get_box_ptr,
            get_item_ptr,
            get_type_ptr,
        );

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(text_set_box_ptr, 12);
        bus.write_word(text_set_box_ptr + 2, 22);
        bus.write_word(text_set_box_ptr + 4, 28);
        bus.write_word(text_set_box_ptr + 6, 168);
        bus.write_long(TEST_SP, text_set_box_ptr);
        bus.write_long(TEST_SP + 4, new_text_handle);
        bus.write_word(TEST_SP + 8, 16);
        bus.write_word(TEST_SP + 10, 1);
        bus.write_long(TEST_SP + 12, dialog_ptr);
        disp.dispatch_dialog(true, 0x18E, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let text_set_stack_after = cpu.read_reg(Register::A7);
        let text_stored_item = disp.dialog_items.get(&dialog_ptr).unwrap()[0].clone();
        let text_post_set_get = get_ditem_snapshot_for_test(
            &mut disp,
            &mut cpu,
            &mut bus,
            dialog_ptr,
            1,
            get_box_ptr,
            get_item_ptr,
            get_type_ptr,
        );

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(control_set_box_ptr, 42);
        bus.write_word(control_set_box_ptr + 2, 52);
        bus.write_word(control_set_box_ptr + 4, 62);
        bus.write_word(control_set_box_ptr + 6, 172);
        bus.write_long(TEST_SP, control_set_box_ptr);
        bus.write_long(TEST_SP + 4, new_control_handle);
        bus.write_word(TEST_SP + 8, 5);
        bus.write_word(TEST_SP + 10, 2);
        bus.write_long(TEST_SP + 12, dialog_ptr);
        disp.dispatch_dialog(true, 0x18E, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let control_set_stack_after = cpu.read_reg(Register::A7);
        let control_stored_item = disp.dialog_items.get(&dialog_ptr).unwrap()[1].clone();
        let control_post_set_get = get_ditem_snapshot_for_test(
            &mut disp,
            &mut cpu,
            &mut bus,
            dialog_ptr,
            2,
            get_box_ptr,
            get_item_ptr,
            get_type_ptr,
        );

        DItemTextControlRecordSnapshot {
            text_initial_get,
            text_set_stack_after,
            text_old_handle_mapped_after: disp.dialog_item_handles.contains_key(&old_text_handle),
            text_new_handle_map_value_after: disp
                .dialog_item_handles
                .get(&new_text_handle)
                .copied(),
            text_ditl_handle_after: bus.read_long(text_entry),
            text_ditl_rect_after: (
                bus.read_word(text_entry + 4),
                bus.read_word(text_entry + 6),
                bus.read_word(text_entry + 8),
                bus.read_word(text_entry + 10),
            ),
            text_ditl_type_after: bus.read_byte(text_entry + 12),
            text_stored_type_after: text_stored_item.item_type,
            text_stored_rect_after: text_stored_item.rect,
            text_cached_text_after: text_stored_item.text,
            text_handle_bytes_after: text_handle_bytes(&bus, new_text_handle),
            text_post_set_get,
            control_initial_get,
            control_set_stack_after,
            control_old_handle_mapped_after: disp
                .dialog_control_handles
                .contains_key(&old_control_handle),
            control_new_handle_map_value_after: disp
                .dialog_control_handles
                .get(&new_control_handle)
                .copied(),
            control_ditl_handle_after: bus.read_long(control_entry),
            control_ditl_rect_after: (
                bus.read_word(control_entry + 4),
                bus.read_word(control_entry + 6),
                bus.read_word(control_entry + 8),
                bus.read_word(control_entry + 10),
            ),
            control_ditl_type_after: bus.read_byte(control_entry + 12),
            control_stored_type_after: control_stored_item.item_type,
            control_stored_rect_after: control_stored_item.rect,
            control_record_rect_after: (
                bus.read_word(new_control_ptr + 8),
                bus.read_word(new_control_ptr + 10),
                bus.read_word(new_control_ptr + 12),
                bus.read_word(new_control_ptr + 14),
            ),
            control_value_after: disp.dialog_control_values.get(&(dialog_ptr, 2)).copied(),
            control_post_set_get,
        }
    }

    fn hide_show_ditem_visibility_results_for_theme(
        theme_id: UiThemeId,
    ) -> DItemVisibilitySnapshot {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.set_ui_theme_id(theme_id);
        let dialog_ptr = bus.alloc(170);
        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(18);
        let get_box_ptr = bus.alloc(8);
        let get_item_ptr = bus.alloc(4);
        let get_type_ptr = bus.alloc(2);

        bus.write_long(items_handle, ditl_ptr);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_word(ditl_ptr, 0); // one item
        bus.write_long(ditl_ptr + 2, 0);
        bus.write_word(ditl_ptr + 6, 10);
        bus.write_word(ditl_ptr + 8, 20);
        bus.write_word(ditl_ptr + 10, 30);
        bus.write_word(ditl_ptr + 12, 120);
        bus.write_byte(ditl_ptr + 14, 4);
        bus.write_byte(ditl_ptr + 15, 2);
        bus.write_bytes(ditl_ptr + 16, b"OK");
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 4,
                rect: (10, 20, 30, 120),
                text: "OK".into(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, get_box_ptr);
        bus.write_long(TEST_SP + 4, get_item_ptr);
        bus.write_long(TEST_SP + 8, get_type_ptr);
        bus.write_word(TEST_SP + 12, 1);
        bus.write_long(TEST_SP + 14, dialog_ptr);
        disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let initial_get = DItemGetSnapshot {
            item_type: bus.read_word(get_type_ptr),
            item: bus.read_long(get_item_ptr),
            rect: (
                bus.read_word(get_box_ptr),
                bus.read_word(get_box_ptr + 2),
                bus.read_word(get_box_ptr + 4),
                bus.read_word(get_box_ptr + 6),
            ),
            stack_after: cpu.read_reg(Register::A7),
        };
        assert_ne!(initial_get.item, 0);
        let ctrl_ptr = bus.read_long(initial_get.item);
        assert_ne!(ctrl_ptr, 0);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 1);
        bus.write_long(TEST_SP + 2, dialog_ptr);
        disp.dispatch_dialog(true, 0x027, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let hide_stack_after = cpu.read_reg(Register::A7);
        let hidden_item_rect = disp.dialog_items.get(&dialog_ptr).unwrap()[0].rect;
        let hidden_saved_rect = disp.hidden_dialog_item_rects.get(&(dialog_ptr, 1)).copied();
        let hidden_control_rect = (
            bus.read_word(ctrl_ptr + 8),
            bus.read_word(ctrl_ptr + 10),
            bus.read_word(ctrl_ptr + 12),
            bus.read_word(ctrl_ptr + 14),
        );

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, get_box_ptr);
        bus.write_long(TEST_SP + 4, get_item_ptr);
        bus.write_long(TEST_SP + 8, get_type_ptr);
        bus.write_word(TEST_SP + 12, 1);
        bus.write_long(TEST_SP + 14, dialog_ptr);
        disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let hidden_get = DItemGetSnapshot {
            item_type: bus.read_word(get_type_ptr),
            item: bus.read_long(get_item_ptr),
            rect: (
                bus.read_word(get_box_ptr),
                bus.read_word(get_box_ptr + 2),
                bus.read_word(get_box_ptr + 4),
                bus.read_word(get_box_ptr + 6),
            ),
            stack_after: cpu.read_reg(Register::A7),
        };

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 20);
        bus.write_word(TEST_SP + 2, 40);
        bus.write_long(TEST_SP + 4, dialog_ptr);
        bus.write_word(TEST_SP + 8, 0xBEEF);
        disp.dispatch_dialog(true, 0x184, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let hidden_find = (
            bus.read_word(TEST_SP + 8) as i16,
            cpu.read_reg(Register::A7),
        );

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 1);
        bus.write_long(TEST_SP + 2, dialog_ptr);
        disp.dispatch_dialog(true, 0x028, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let show_stack_after = cpu.read_reg(Register::A7);
        let restored_item_rect = disp.dialog_items.get(&dialog_ptr).unwrap()[0].rect;
        let restored_saved_rect = disp.hidden_dialog_item_rects.get(&(dialog_ptr, 1)).copied();
        let restored_control_rect = (
            bus.read_word(ctrl_ptr + 8),
            bus.read_word(ctrl_ptr + 10),
            bus.read_word(ctrl_ptr + 12),
            bus.read_word(ctrl_ptr + 14),
        );

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, get_box_ptr);
        bus.write_long(TEST_SP + 4, get_item_ptr);
        bus.write_long(TEST_SP + 8, get_type_ptr);
        bus.write_word(TEST_SP + 12, 1);
        bus.write_long(TEST_SP + 14, dialog_ptr);
        disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let restored_get = DItemGetSnapshot {
            item_type: bus.read_word(get_type_ptr),
            item: bus.read_long(get_item_ptr),
            rect: (
                bus.read_word(get_box_ptr),
                bus.read_word(get_box_ptr + 2),
                bus.read_word(get_box_ptr + 4),
                bus.read_word(get_box_ptr + 6),
            ),
            stack_after: cpu.read_reg(Register::A7),
        };

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 20);
        bus.write_word(TEST_SP + 2, 40);
        bus.write_long(TEST_SP + 4, dialog_ptr);
        bus.write_word(TEST_SP + 8, 0xBEEF);
        disp.dispatch_dialog(true, 0x184, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let restored_find = (
            bus.read_word(TEST_SP + 8) as i16,
            cpu.read_reg(Register::A7),
        );

        DItemVisibilitySnapshot {
            initial_get,
            hide_stack_after,
            hidden_item_rect,
            hidden_saved_rect,
            hidden_control_rect,
            hidden_get,
            hidden_find,
            show_stack_after,
            restored_item_rect,
            restored_saved_rect,
            restored_control_rect,
            restored_get,
            restored_find,
        }
    }

    fn drawdialog_useritem_pixel_results_for_theme(theme_id: UiThemeId) -> (u8, u32, u32) {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.set_ui_theme_id(theme_id);
        let dialog_ptr = bus.alloc(170);
        let (screen_base, row_bytes, _w, _h, pixel_size) = disp.screen_mode;
        assert_eq!(pixel_size, 8);

        bus.write_word(dialog_ptr + 8, 0);
        bus.write_word(dialog_ptr + 10, 0);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 40);
        bus.write_word(dialog_ptr + 22, 80);
        disp.dialog_items.insert(
            dialog_ptr,
            vec![
                DialogItem {
                    item_type: 0, // enabled userItem
                    rect: (8, 8, 20, 32),
                    text: String::new(),
                    resource_id: 0,
                    proc_ptr: 0x00C0_FFEE,
                    sel_start: 0,
                    sel_end: 0,
                },
                DialogItem {
                    item_type: 4,
                    rect: (24, 8, 36, 40),
                    text: "OK".to_string(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
            ],
        );

        let user_x = 10u32;
        let user_y = 10u32;
        let user_pixel = screen_base + user_y * row_bytes + user_x;
        bus.write_byte(user_pixel, 0xA5);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, dialog_ptr);
        bus.write_long(TEST_SP + 4, 0xCAFE_BABE);

        disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        (
            bus.read_byte(user_pixel),
            cpu.read_reg(Register::A7),
            bus.read_long(TEST_SP + 4),
        )
    }

    fn monochrome_icon_resource_with_points(points: &[(u8, u8)]) -> Vec<u8> {
        let mut icon = vec![0u8; 128];
        for &(x, y) in points {
            let byte = y as usize * 4 + x as usize / 8;
            icon[byte] |= 0x80 >> (x & 7);
        }
        icon
    }

    fn write_be_word(data: &mut [u8], offset: usize, value: u16) {
        data[offset] = (value >> 8) as u8;
        data[offset + 1] = value as u8;
    }

    fn cicn_resource_with_points(width: u16, height: u16, points: &[(u8, u8, u8)]) -> Vec<u8> {
        let pixel_row_bytes = u32::from(width);
        let mask_row_bytes = u32::from(width).div_ceil(8);
        let mask_size = mask_row_bytes * u32::from(height);
        let bmap_size = mask_row_bytes * u32::from(height);
        let ctab_size = 16u32;
        let pixel_size = pixel_row_bytes * u32::from(height);
        let bmap_offset = 82 + mask_size as usize;
        let ctab_offset = bmap_offset + bmap_size as usize;
        let pixel_offset = ctab_offset + ctab_size as usize;
        let mut data = vec![0u8; pixel_offset + pixel_size as usize];

        write_be_word(&mut data, 4, pixel_row_bytes as u16);
        write_be_word(&mut data, 10, height);
        write_be_word(&mut data, 12, width);
        write_be_word(&mut data, 32, 8);

        write_be_word(&mut data, 54, mask_row_bytes as u16);
        write_be_word(&mut data, 60, height);
        write_be_word(&mut data, 62, width);

        write_be_word(&mut data, 68, mask_row_bytes as u16);
        write_be_word(&mut data, 74, height);
        write_be_word(&mut data, 76, width);

        write_be_word(&mut data, ctab_offset + 6, 0);

        for &(x, y, pixel) in points {
            let mask_row = 82 + y as usize * mask_row_bytes as usize;
            data[mask_row + x as usize / 8] |= 0x80 >> (x & 7);
            let pixel_row = pixel_offset + y as usize * pixel_row_bytes as usize;
            data[pixel_row + x as usize] = pixel;
        }
        data
    }

    fn drawdialog_icon_item_pixel_results_for_theme(theme_id: UiThemeId) -> ([u8; 3], u32, u32) {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.set_ui_theme_id(theme_id);
        let dialog_ptr = bus.alloc(170);
        let (screen_base, row_bytes, _w, _h, pixel_size) = disp.screen_mode;
        assert_eq!(pixel_size, 8);

        bus.write_word(dialog_ptr + 8, 0);
        bus.write_word(dialog_ptr + 10, 0);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 56);
        bus.write_word(dialog_ptr + 22, 88);
        disp.install_test_resource(
            &mut bus,
            *b"ICON",
            421,
            &monochrome_icon_resource_with_points(&[(0, 0), (15, 15), (31, 31)]),
        );
        disp.dialog_items.insert(
            dialog_ptr,
            vec![
                DialogItem {
                    item_type: 32, // icon item
                    rect: (8, 8, 40, 40),
                    text: String::new(),
                    resource_id: 421,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
                DialogItem {
                    item_type: 4,
                    rect: (44, 8, 54, 40),
                    text: "OK".to_string(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
            ],
        );

        for (x, y) in [(8u32, 8u32), (23, 23), (39, 39)] {
            bus.write_byte(screen_base + y * row_bytes + x, 0x42);
        }
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, dialog_ptr);
        bus.write_long(TEST_SP + 4, 0xCAFE_BABE);

        disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        (
            [
                bus.read_byte(screen_base + 8 * row_bytes + 8),
                bus.read_byte(screen_base + 23 * row_bytes + 23),
                bus.read_byte(screen_base + 39 * row_bytes + 39),
            ],
            cpu.read_reg(Register::A7),
            bus.read_long(TEST_SP + 4),
        )
    }

    fn drawdialog_cicn_item_pixel_results_for_theme(theme_id: UiThemeId) -> ([u8; 3], u32, u32) {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.set_ui_theme_id(theme_id);
        let dialog_ptr = bus.alloc(170);
        let (screen_base, row_bytes, _w, _h, pixel_size) = disp.screen_mode;
        assert_eq!(pixel_size, 8);

        bus.write_word(dialog_ptr + 8, 0);
        bus.write_word(dialog_ptr + 10, 0);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 56);
        bus.write_word(dialog_ptr + 22, 88);
        let sample_points = [(0, 0), (15, 15), (31, 31)];
        disp.install_test_resource(
            &mut bus,
            *b"ICON",
            422,
            &monochrome_icon_resource_with_points(&sample_points),
        );
        disp.install_test_resource(
            &mut bus,
            *b"cicn",
            422,
            &cicn_resource_with_points(32, 32, &[(0, 0, 0x33), (15, 15, 0x55), (31, 31, 0x77)]),
        );
        disp.dialog_items.insert(
            dialog_ptr,
            vec![
                DialogItem {
                    item_type: 32, // icon item with same-ID cicn override
                    rect: (8, 8, 40, 40),
                    text: String::new(),
                    resource_id: 422,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
                DialogItem {
                    item_type: 4,
                    rect: (44, 8, 54, 40),
                    text: "OK".to_string(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
            ],
        );

        for (x, y) in [(8u32, 8u32), (23, 23), (39, 39)] {
            bus.write_byte(screen_base + y * row_bytes + x, 0x42);
        }
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, dialog_ptr);
        bus.write_long(TEST_SP + 4, 0xCAFE_BABE);

        disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        (
            [
                bus.read_byte(screen_base + 8 * row_bytes + 8),
                bus.read_byte(screen_base + 23 * row_bytes + 23),
                bus.read_byte(screen_base + 39 * row_bytes + 39),
            ],
            cpu.read_reg(Register::A7),
            bus.read_long(TEST_SP + 4),
        )
    }

    fn solid_fill_pict_resource(width: i16, height: i16) -> Vec<u8> {
        let mut data = Vec::new();
        data.extend_from_slice(&0u16.to_be_bytes()); // picSize placeholder
        for value in [0i16, 0, height, width] {
            data.extend_from_slice(&(value as u16).to_be_bytes());
        }
        data.push(0x11); // versionOp
        data.push(0x01); // PICT v1
        data.push(0x0A); // FillPat
        data.extend_from_slice(&[0xFF; 8]);
        data.push(0x34); // fillRect
        for value in [0i16, 0, height, width] {
            data.extend_from_slice(&(value as u16).to_be_bytes());
        }
        data.push(0xFF); // EndOfPicture
        let size = data.len() as u16;
        data[0..2].copy_from_slice(&size.to_be_bytes());
        data
    }

    fn drawdialog_picture_item_pixel_results_for_theme(theme_id: UiThemeId) -> ([u8; 3], u32, u32) {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.set_ui_theme_id(theme_id);
        let dialog_ptr = bus.alloc(170);
        let (screen_base, row_bytes, _w, _h, pixel_size) = disp.screen_mode;
        assert_eq!(pixel_size, 8);

        bus.write_word(dialog_ptr + 8, 0);
        bus.write_word(dialog_ptr + 10, 0);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 48);
        bus.write_word(dialog_ptr + 22, 80);
        disp.install_test_resource(&mut bus, *b"PICT", 420, &solid_fill_pict_resource(16, 16));
        disp.dialog_items.insert(
            dialog_ptr,
            vec![
                DialogItem {
                    item_type: 64, // picture item
                    rect: (8, 8, 24, 24),
                    text: String::new(),
                    resource_id: 420,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
                DialogItem {
                    item_type: 4,
                    rect: (30, 8, 42, 40),
                    text: "OK".to_string(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
            ],
        );

        for (x, y) in [(9u32, 9u32), (16, 16), (22, 22)] {
            bus.write_byte(screen_base + y * row_bytes + x, 0x42);
        }
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, dialog_ptr);
        bus.write_long(TEST_SP + 4, 0xCAFE_BABE);

        disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        (
            [
                bus.read_byte(screen_base + 9 * row_bytes + 9),
                bus.read_byte(screen_base + 16 * row_bytes + 16),
                bus.read_byte(screen_base + 22 * row_bytes + 22),
            ],
            cpu.read_reg(Register::A7),
            bus.read_long(TEST_SP + 4),
        )
    }

    fn dialog_tracking_state_for_test(dialog_ptr: u32) -> DialogTrackingState {
        DialogTrackingState {
            dialog_ptr,
            bounds: (0, 0, 0, 0),
            title: String::new(),
            proc_id: 0,
            items: Vec::new(),
            default_item: 1,
            cancel_item: 2,
            edit_text: String::new(),
            edit_item: 0,
            saved_pixels: Vec::new(),
            stack_ptr: TEST_SP,
            item_hit_ptr: 0,
            rendered_pixels: Vec::new(),
            flash_remaining: 0,
            flash_delay: 0,
            flash_item: 0,
            edit_text_modified: false,
            draw_proc_queue: VecDeque::new(),
            draw_procs_done: true,
            rendered_pixels_final: true,
            filter_proc: 0,
            game_managed: false,
            last_filter_event: None,
            popup_draws: Vec::new(),
            active_popup: None,
            active_button: None,
            active_user_item: None,
        }
    }

    fn dialog_lifecycle_cleanup_results_for_theme(
        theme_id: UiThemeId,
    ) -> DialogLifecycleCleanupSnapshot {
        let bounds = (100, 100, 150, 200);
        let previous_bounds = (0, 0, 342, 512);
        let saved_pixels = vec![0x33; 66 * 116];

        let (mut close_disp, mut close_cpu, mut close_bus) = setup();
        close_disp.set_ui_theme_id(theme_id);
        close_disp.set_screen_mode_for_test(0x300000, 640, 640, 480, 8);
        let close_dialog_ptr = close_bus.alloc(170);
        let close_previous_window = close_bus.alloc(170);
        seed_window_regions(&mut close_bus, close_dialog_ptr, bounds);
        seed_window_regions(&mut close_bus, close_previous_window, previous_bounds);
        close_bus.write_word(close_dialog_ptr + 108, 2);
        close_disp.front_window = close_dialog_ptr;
        close_disp.current_port = close_dialog_ptr;
        close_disp.window_bounds = bounds;
        close_disp.window_proc_id = 2;
        close_disp.window_list = vec![close_dialog_ptr, close_previous_window];
        close_disp.window_stack.push((
            close_previous_window,
            previous_bounds,
            0,
            "Previous".to_string(),
        ));
        close_disp.dialog_items.insert(
            close_dialog_ptr,
            vec![DialogItem {
                item_type: 4,
                rect: (20, 40, 40, 100),
                text: "OK".to_string(),
                ..Default::default()
            }],
        );
        close_disp.dialog_tracking = Some(dialog_tracking_state_for_test(close_dialog_ptr));
        close_disp.retained_modal_dialog_click = Some(RetainedModalDialogClickState {
            dialog_ptr: close_dialog_ptr,
            item_no: 1,
            rect: (20, 40, 40, 100),
            title: "OK".to_string(),
            is_default: true,
            highlighted: true,
            delivered_to_app: false,
        });
        close_disp.dialog_visible_snapshots.insert(
            close_dialog_ptr,
            PersistentDialogSnapshot {
                bounds,
                pixels: saved_pixels.clone(),
            },
        );
        close_disp.dialog_modal_entered.insert(close_dialog_ptr);
        close_disp
            .dialog_saved_pixels
            .insert(close_dialog_ptr, saved_pixels.clone());
        close_bus.write_long(crate::memory::globals::addr::THE_PORT, close_dialog_ptr);
        let close_global_ptr = close_bus.read_long(close_cpu.read_reg(Register::A5));
        close_bus.write_long(close_global_ptr, close_dialog_ptr);
        close_cpu.write_reg(Register::A7, TEST_SP);
        close_bus.write_long(TEST_SP, close_dialog_ptr);
        close_disp
            .dispatch_dialog(true, 0x182, &mut close_cpu, &mut close_bus)
            .unwrap()
            .unwrap();

        let close_stack_after = close_cpu.read_reg(Register::A7);
        let close_tracking_cleared = close_disp.dialog_tracking.is_none();
        let close_retained_click_cleared = close_disp.retained_modal_dialog_click.is_none();
        let close_visible_snapshot_cleared = !close_disp
            .dialog_visible_snapshots
            .contains_key(&close_dialog_ptr);
        let close_modal_entered_cleared =
            !close_disp.dialog_modal_entered.contains(&close_dialog_ptr);
        let close_saved_pixels_cleared = !close_disp
            .dialog_saved_pixels
            .contains_key(&close_dialog_ptr);
        let close_window_list_contains_dialog = close_disp.window_list.contains(&close_dialog_ptr);
        let close_front_window_after = close_disp.front_window;
        let close_the_port_after = close_bus.read_long(crate::memory::globals::addr::THE_PORT);
        let close_current_port_after = close_disp.current_port;
        let close_update_event_for_previous_window = close_disp
            .event_queue
            .iter()
            .any(|event| event.what == 6 && event.message == close_previous_window);
        let close_dialog_items_present_after =
            close_disp.dialog_items.contains_key(&close_dialog_ptr);

        let (mut dispose_disp, mut dispose_cpu, mut dispose_bus) = setup();
        dispose_disp.set_ui_theme_id(theme_id);
        dispose_disp.set_screen_mode_for_test(0x300000, 640, 640, 480, 8);
        let dispose_dialog_ptr = dispose_bus.alloc(170);
        let dispose_other_dialog_ptr = dispose_bus.alloc(170);
        let dispose_previous_window = dispose_bus.alloc(170);
        let dispose_text_handle = dispose_bus.alloc(4);
        let dispose_other_text_handle = dispose_bus.alloc(4);
        let dispose_ctrl_handle = dispose_bus.alloc(4);
        let dispose_other_ctrl_handle = dispose_bus.alloc(4);
        seed_window_regions(&mut dispose_bus, dispose_dialog_ptr, bounds);
        seed_window_regions(&mut dispose_bus, dispose_previous_window, previous_bounds);
        dispose_bus.write_word(dispose_dialog_ptr + 108, 2);
        dispose_disp.front_window = dispose_dialog_ptr;
        dispose_disp.current_port = dispose_dialog_ptr;
        dispose_disp.window_bounds = bounds;
        dispose_disp.window_proc_id = 2;
        dispose_disp.window_list = vec![
            dispose_dialog_ptr,
            dispose_previous_window,
            dispose_other_dialog_ptr,
        ];
        dispose_disp.window_stack.push((
            dispose_previous_window,
            previous_bounds,
            0,
            "Previous".to_string(),
        ));
        dispose_disp
            .dialog_items
            .insert(dispose_dialog_ptr, vec![DialogItem::default()]);
        dispose_disp
            .dialog_items
            .insert(dispose_other_dialog_ptr, vec![DialogItem::default()]);
        dispose_disp.dialog_tracking = Some(dialog_tracking_state_for_test(dispose_dialog_ptr));
        dispose_disp.retained_modal_dialog_click = Some(RetainedModalDialogClickState {
            dialog_ptr: dispose_dialog_ptr,
            item_no: 1,
            rect: (20, 40, 40, 100),
            title: "OK".to_string(),
            is_default: true,
            highlighted: true,
            delivered_to_app: false,
        });
        dispose_disp.dialog_visible_snapshots.insert(
            dispose_dialog_ptr,
            PersistentDialogSnapshot {
                bounds,
                pixels: saved_pixels.clone(),
            },
        );
        dispose_disp.dialog_modal_entered.insert(dispose_dialog_ptr);
        dispose_disp
            .dialog_saved_pixels
            .insert(dispose_dialog_ptr, saved_pixels);
        dispose_disp
            .dialog_item_handles
            .insert(dispose_text_handle, (dispose_dialog_ptr, 0));
        dispose_disp
            .dialog_item_handles
            .insert(dispose_other_text_handle, (dispose_other_dialog_ptr, 0));
        dispose_disp
            .dialog_control_handles
            .insert(dispose_ctrl_handle, (dispose_dialog_ptr, 1));
        dispose_disp
            .dialog_control_handles
            .insert(dispose_other_ctrl_handle, (dispose_other_dialog_ptr, 1));
        dispose_disp
            .dialog_control_values
            .insert((dispose_dialog_ptr, 1), 1);
        dispose_disp
            .dialog_control_values
            .insert((dispose_other_dialog_ptr, 1), 1);
        dispose_disp
            .hidden_dialog_item_rects
            .insert((dispose_dialog_ptr, 1), (10, 20, 30, 40));
        dispose_disp
            .hidden_dialog_item_rects
            .insert((dispose_other_dialog_ptr, 1), (50, 60, 70, 80));
        dispose_disp
            .dialog_cancel_items
            .insert(dispose_dialog_ptr, 2);
        dispose_disp
            .dialog_cancel_items
            .insert(dispose_other_dialog_ptr, 3);
        dispose_disp.pending_dialog_popup_menu = Some(PendingDialogPopupMenu {
            dialog_ptr: dispose_dialog_ptr,
            item_no: 1,
            menu_id: 900,
            rect: (10, 20, 30, 130),
        });
        dispose_bus.write_long(crate::memory::globals::addr::THE_PORT, dispose_dialog_ptr);
        let dispose_global_ptr = dispose_bus.read_long(dispose_cpu.read_reg(Register::A5));
        dispose_bus.write_long(dispose_global_ptr, dispose_dialog_ptr);
        dispose_cpu.write_reg(Register::A7, TEST_SP);
        dispose_bus.write_long(TEST_SP, dispose_dialog_ptr);
        dispose_disp
            .dispatch_dialog(true, 0x183, &mut dispose_cpu, &mut dispose_bus)
            .unwrap()
            .unwrap();

        DialogLifecycleCleanupSnapshot {
            close_stack_after,
            close_tracking_cleared,
            close_retained_click_cleared,
            close_visible_snapshot_cleared,
            close_modal_entered_cleared,
            close_saved_pixels_cleared,
            close_window_list_contains_dialog,
            close_front_window_after,
            close_the_port_after,
            close_current_port_after,
            close_update_event_for_previous_window,
            close_dialog_items_present_after,
            dispose_stack_after: dispose_cpu.read_reg(Register::A7),
            dispose_tracking_cleared: dispose_disp.dialog_tracking.is_none(),
            dispose_retained_click_cleared: dispose_disp.retained_modal_dialog_click.is_none(),
            dispose_visible_snapshot_cleared: !dispose_disp
                .dialog_visible_snapshots
                .contains_key(&dispose_dialog_ptr),
            dispose_modal_entered_cleared: !dispose_disp
                .dialog_modal_entered
                .contains(&dispose_dialog_ptr),
            dispose_saved_pixels_cleared: !dispose_disp
                .dialog_saved_pixels
                .contains_key(&dispose_dialog_ptr),
            dispose_window_list_contains_dialog: dispose_disp
                .window_list
                .contains(&dispose_dialog_ptr),
            dispose_front_window_after: dispose_disp.front_window,
            dispose_the_port_after: dispose_bus.read_long(crate::memory::globals::addr::THE_PORT),
            dispose_current_port_after: dispose_disp.current_port,
            dispose_update_event_for_previous_window: dispose_disp
                .event_queue
                .iter()
                .any(|event| event.what == 6 && event.message == dispose_previous_window),
            dispose_dialog_items_present_after: dispose_disp
                .dialog_items
                .contains_key(&dispose_dialog_ptr),
            dispose_other_dialog_items_present_after: dispose_disp
                .dialog_items
                .contains_key(&dispose_other_dialog_ptr),
            dispose_dialog_item_handle_present_after: dispose_disp
                .dialog_item_handles
                .contains_key(&dispose_text_handle),
            dispose_other_dialog_item_handle_present_after: dispose_disp
                .dialog_item_handles
                .contains_key(&dispose_other_text_handle),
            dispose_dialog_control_handle_present_after: dispose_disp
                .dialog_control_handles
                .contains_key(&dispose_ctrl_handle),
            dispose_other_dialog_control_handle_present_after: dispose_disp
                .dialog_control_handles
                .contains_key(&dispose_other_ctrl_handle),
            dispose_dialog_control_value_present_after: dispose_disp
                .dialog_control_values
                .contains_key(&(dispose_dialog_ptr, 1)),
            dispose_other_dialog_control_value_present_after: dispose_disp
                .dialog_control_values
                .contains_key(&(dispose_other_dialog_ptr, 1)),
            dispose_hidden_rect_present_after: dispose_disp
                .hidden_dialog_item_rects
                .contains_key(&(dispose_dialog_ptr, 1)),
            dispose_other_hidden_rect_present_after: dispose_disp
                .hidden_dialog_item_rects
                .contains_key(&(dispose_other_dialog_ptr, 1)),
            dispose_cancel_item_present_after: dispose_disp
                .dialog_cancel_items
                .contains_key(&dispose_dialog_ptr),
            dispose_other_cancel_item_present_after: dispose_disp
                .dialog_cancel_items
                .contains_key(&dispose_other_dialog_ptr),
            dispose_pending_popup_cleared: dispose_disp.pending_dialog_popup_menu.is_none(),
        }
    }

    fn cached_dialog_items(
        disp: &TrapDispatcher,
        dialog_ptr: u32,
    ) -> Vec<(u8, (i16, i16, i16, i16), String, i16)> {
        disp.dialog_items
            .get(&dialog_ptr)
            .into_iter()
            .flatten()
            .map(|item| {
                (
                    item.item_type,
                    item.rect,
                    item.text.clone(),
                    item.resource_id,
                )
            })
            .collect()
    }

    fn dialog_port_rect(bus: &MacMemoryBus, dialog_ptr: u32) -> (i16, i16, i16, i16) {
        (
            bus.read_word(dialog_ptr + 16) as i16,
            bus.read_word(dialog_ptr + 18) as i16,
            bus.read_word(dialog_ptr + 20) as i16,
            bus.read_word(dialog_ptr + 22) as i16,
        )
    }

    fn dialog_creation_results_for_theme(theme_id: UiThemeId) -> DialogCreationSnapshot {
        let (mut new_disp, mut new_cpu, mut new_bus) = setup();
        new_disp.set_ui_theme_id(theme_id);
        let new_screen_base = new_bus.alloc((640 * 480) as u32);
        new_bus.write_long(0x0824, new_screen_base);
        new_disp.screen_mode = (new_screen_base, 640, 640, 480, 8);

        let new_ditl = build_test_ditl_items(&[
            (16, (10, 12, 28, 120), b"Alpha".as_slice()),
            (4, (44, 70, 66, 132), b"OK".as_slice()),
        ]);
        let new_items_data_ptr = new_bus.alloc(new_ditl.len() as u32);
        new_bus.write_bytes(new_items_data_ptr, &new_ditl);
        let new_items_handle = new_bus.alloc(4);
        new_bus.write_long(new_items_handle, new_items_data_ptr);

        let new_title = new_bus.alloc(32);
        new_bus.write_pstring(new_title, b"Modeless");
        let new_bounds = new_bus.alloc(8);
        new_bus.write_word(new_bounds, 60);
        new_bus.write_word(new_bounds + 2, 70);
        new_bus.write_word(new_bounds + 4, 140);
        new_bus.write_word(new_bounds + 6, 220);
        let new_storage = new_bus.alloc(170);
        let new_sp = TEST_SP - 30;
        new_cpu.write_reg(Register::A7, new_sp);
        new_bus.write_long(new_sp, new_items_handle);
        new_bus.write_long(new_sp + 4, 0x1234_5678);
        new_bus.write_byte(new_sp + 8, 0xFF); // goAwayFlag
        new_bus.write_long(new_sp + 10, 0xFFFF_FFFF); // behind = front
        new_bus.write_word(new_sp + 14, 4); // noGrowDocProc/modeless
        new_bus.write_byte(new_sp + 16, 0xFF); // visible
        new_bus.write_long(new_sp + 18, new_title);
        new_bus.write_long(new_sp + 22, new_bounds);
        new_bus.write_long(new_sp + 26, new_storage);
        new_bus.write_long(new_sp + 30, 0xDEAD_BEEF);
        new_disp
            .dispatch_dialog(true, 0x17D, &mut new_cpu, &mut new_bus)
            .unwrap()
            .unwrap();
        let new_dialog_ptr = new_bus.read_long(new_sp + 30);
        let new_update_region =
            TrapDispatcher::region_handle_rect(&new_bus, new_bus.read_long(new_dialog_ptr + 122));
        let new_update_event_queued = new_disp
            .event_queue
            .iter()
            .any(|event| event.what == 6 && event.message == new_dialog_ptr);

        let (mut get_disp, mut get_cpu, mut get_bus) = setup();
        get_disp.set_ui_theme_id(theme_id);
        let get_screen_base = get_bus.alloc((640 * 480) as u32);
        get_bus.write_long(0x0824, get_screen_base);
        get_disp.screen_mode = (get_screen_base, 640, 640, 480, 8);

        let mut dlog = build_test_dlog((80, 90, 160, 240), 1901, 0);
        dlog[10] = 1; // visible
        let get_ditl = build_test_ditl_items(&[
            (16, (12, 18, 30, 128), b"Beta".as_slice()),
            (4, (46, 78, 68, 138), b"OK".as_slice()),
        ]);
        get_disp.install_test_resource(&mut get_bus, *b"DLOG", 1900, &dlog);
        let original_ditl_ptr =
            get_disp.install_test_resource(&mut get_bus, *b"DITL", 1901, &get_ditl);
        let get_storage = get_bus.alloc(170);
        get_cpu.write_reg(Register::A7, TEST_SP);
        get_bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind = front
        get_bus.write_long(TEST_SP + 4, get_storage);
        get_bus.write_word(TEST_SP + 8, 1900);
        get_bus.write_long(TEST_SP + 10, 0xDEAD_BEEF);
        get_disp
            .dispatch_dialog(true, 0x17C, &mut get_cpu, &mut get_bus)
            .unwrap()
            .unwrap();
        let get_dialog_ptr = get_bus.read_long(TEST_SP + 10);
        let get_items_handle = get_bus.read_long(get_dialog_ptr + 156);
        let get_items_data_ptr = get_bus.read_long(get_items_handle);
        let get_update_region =
            TrapDispatcher::region_handle_rect(&get_bus, get_bus.read_long(get_dialog_ptr + 122));
        let get_update_event_queued = get_disp
            .event_queue
            .iter()
            .any(|event| event.what == 6 && event.message == get_dialog_ptr);

        DialogCreationSnapshot {
            new_dialog_ptr,
            new_stack_after: new_cpu.read_reg(Register::A7),
            new_result_slot: new_bus.read_long(new_sp + 30),
            new_window_list: new_disp.window_list.clone(),
            new_front_window: new_disp.front_window,
            new_current_port: new_disp.current_port,
            new_the_port: new_bus.read_long(crate::memory::globals::addr::THE_PORT),
            new_visible_byte: new_bus.read_byte(new_dialog_ptr + 110),
            new_goaway_byte: new_bus.read_byte(new_dialog_ptr + 112),
            new_refcon: new_bus.read_long(new_dialog_ptr + 152),
            new_window_kind: new_bus.read_word(new_dialog_ptr + 108) as i16,
            new_proc_id: new_disp
                .window_proc_ids
                .get(&new_dialog_ptr)
                .copied()
                .unwrap_or_default(),
            new_port_rect: dialog_port_rect(&new_bus, new_dialog_ptr),
            new_items_handle: new_bus.read_long(new_dialog_ptr + 156),
            new_items_data_ptr: new_bus.read_long(new_items_handle),
            new_first_item_handle_nonzero: new_bus.read_long(new_items_data_ptr + 2) != 0,
            new_cached_items: cached_dialog_items(&new_disp, new_dialog_ptr),
            new_update_region,
            new_update_event_queued,
            new_edit_field: new_bus.read_word(new_dialog_ptr + 164),
            new_default_item: new_bus.read_word(new_dialog_ptr + 168),
            get_dialog_ptr,
            get_stack_after: get_cpu.read_reg(Register::A7),
            get_result_slot: get_bus.read_long(TEST_SP + 10),
            get_window_list: get_disp.window_list.clone(),
            get_front_window: get_disp.front_window,
            get_current_port: get_disp.current_port,
            get_the_port: get_bus.read_long(crate::memory::globals::addr::THE_PORT),
            get_visible_byte: get_bus.read_byte(get_dialog_ptr + 110),
            get_refcon: get_bus.read_long(get_dialog_ptr + 152),
            get_window_kind: get_bus.read_word(get_dialog_ptr + 108) as i16,
            get_proc_id: get_disp
                .window_proc_ids
                .get(&get_dialog_ptr)
                .copied()
                .unwrap_or_default(),
            get_port_rect: dialog_port_rect(&get_bus, get_dialog_ptr),
            get_items_handle,
            get_items_data_ptr,
            get_uses_distinct_ditl_copy: get_items_data_ptr != original_ditl_ptr,
            get_original_ditl_handle_field: get_bus.read_long(original_ditl_ptr + 2),
            get_first_item_handle_nonzero: get_bus.read_long(get_items_data_ptr + 2) != 0,
            get_cached_items: cached_dialog_items(&get_disp, get_dialog_ptr),
            get_update_region,
            get_update_event_queued,
            get_edit_field: get_bus.read_word(get_dialog_ptr + 164),
            get_default_item: get_bus.read_word(get_dialog_ptr + 168),
        }
    }

    fn alert_family_results_for_theme(theme_id: UiThemeId) -> AlertFamilySnapshot {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.set_ui_theme_id(theme_id);

        let ditl = build_test_ditl_items(&[
            (4, (48, 70, 70, 132), b"OK".as_slice()),
            // Keep this fixture on the auto-return path while still verifying
            // that stage defaults can report item 2.
            (0x84, (48, 144, 70, 212), b"Cancel".as_slice()),
        ]);
        let alrt = build_alrt_template(2100, 0xC4C4);
        disp.install_test_resource(&mut bus, *b"DITL", 2100, &ditl);
        disp.install_test_resource(&mut bus, *b"ALRT", 2099, &alrt);
        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 0);
        bus.write_word(crate::memory::globals::addr::ANUMBER, 0xCAFE);

        let mut staged_calls = Vec::new();
        for trap_word in [0x185u16, 0x186, 0x187, 0x188, 0x185] {
            cpu.write_reg(Register::A7, TEST_SP);
            bus.write_word(TEST_SP + 4, 2099);
            disp.dispatch_dialog(true, trap_word, &mut cpu, &mut bus)
                .unwrap()
                .unwrap();
            staged_calls.push(AlertTrapCallSnapshot {
                trap_word,
                result: bus.read_word(TEST_SP + 6) as i16,
                stack_after: cpu.read_reg(Register::A7),
                alert_stage_after: bus.read_word(crate::memory::globals::addr::ALERT_STAGE),
                anumber_after: bus.read_word(crate::memory::globals::addr::ANUMBER),
            });
        }

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(crate::memory::globals::addr::ALERT_STAGE, 2);
        bus.write_word(crate::memory::globals::addr::ANUMBER, 0xBEEF);
        bus.write_word(TEST_SP + 4, 2999);
        disp.dispatch_dialog(true, 0x187, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let missing_call = AlertTrapCallSnapshot {
            trap_word: 0x187,
            result: bus.read_word(TEST_SP + 6) as i16,
            stack_after: cpu.read_reg(Register::A7),
            alert_stage_after: bus.read_word(crate::memory::globals::addr::ALERT_STAGE),
            anumber_after: bus.read_word(crate::memory::globals::addr::ANUMBER),
        };

        let (mut prep_disp, mut prep_cpu, mut prep_bus) = setup();
        prep_disp.set_ui_theme_id(theme_id);
        let prep_alrt = build_alrt_template(2101, 0);
        prep_disp.install_test_resource(&mut prep_bus, *b"ALRT", 2102, &prep_alrt);
        let mut prep_call = |trap_word: u16, alert_id: i16| -> (i16, u32) {
            prep_cpu.write_reg(Register::A7, TEST_SP);
            prep_bus.write_word(0x0A60, 0x7FFF);
            prep_bus.write_word(TEST_SP, alert_id as u16);
            prep_disp
                .dispatch_dialog(true, trap_word, &mut prep_cpu, &mut prep_bus)
                .unwrap()
                .unwrap();
            (
                prep_bus.read_word(0x0A60) as i16,
                prep_cpu.read_reg(Register::A7),
            )
        };
        let resource_prep = AlertResourcePrepSnapshot {
            could_present: prep_call(0x189, 2102),
            free_present: prep_call(0x18A, 2102),
            could_missing: prep_call(0x189, 2998),
            free_missing: prep_call(0x18A, 2998),
        };

        AlertFamilySnapshot {
            staged_calls,
            missing_call,
            resource_prep,
        }
    }

    fn dialog_init_sound_results_for_theme(theme_id: UiThemeId) -> DialogInitSoundSnapshot {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.set_ui_theme_id(theme_id);
        use crate::memory::globals::addr;

        bus.write_long(addr::RESUME_PROC, 0xDEAD_BEEF);
        bus.write_long(addr::DA_BEEPER, 0x00AA_BBCC);
        bus.write_word(addr::ALERT_STAGE, 3);
        bus.write_word(addr::ANUMBER, 0xCAFE);
        for i in 0..4u32 {
            bus.write_long(addr::DA_STRINGS + i * 4, 0x00D0_0000 | i);
        }

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, 0x0012_3456);
        disp.dispatch_dialog(true, 0x17B, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let init_stack_after = cpu.read_reg(Register::A7);
        let resume_proc_after_init = bus.read_long(addr::RESUME_PROC);
        let da_beeper_after_init = bus.read_long(addr::DA_BEEPER);
        let alert_stage_after_init = bus.read_word(addr::ALERT_STAGE);
        let anumber_after_init = bus.read_word(addr::ANUMBER);
        let da_strings_after_init =
            std::array::from_fn(|i| bus.read_long(addr::DA_STRINGS + i as u32 * 4));

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, 0x00AB_CDEF);
        disp.dispatch_dialog(true, 0x18C, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let error_sound_stack_after = cpu.read_reg(Register::A7);
        let da_beeper_after_error_sound = bus.read_long(addr::DA_BEEPER);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, 0);
        disp.dispatch_dialog(true, 0x18C, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let nil_error_sound_stack_after = cpu.read_reg(Register::A7);
        let da_beeper_after_nil_error_sound = bus.read_long(addr::DA_BEEPER);

        DialogInitSoundSnapshot {
            init_stack_after,
            resume_proc_after_init,
            da_beeper_after_init,
            alert_stage_after_init,
            anumber_after_init,
            da_strings_after_init,
            error_sound_stack_after,
            da_beeper_after_error_sound,
            nil_error_sound_stack_after,
            da_beeper_after_nil_error_sound,
        }
    }

    fn dialogdispatch_default_cancel_results_for_theme(
        theme_id: UiThemeId,
    ) -> DialogDispatchDefaultCancelSnapshot {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.set_ui_theme_id(theme_id);
        let dialog_ptr = bus.alloc(256);
        bus.write_word(dialog_ptr + 168, 1);
        disp.dialog_tracking = Some(dialog_tracking_state_for_test(dialog_ptr));

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 9); // newItem
        bus.write_long(TEST_SP + 2, dialog_ptr); // theDialog
        bus.write_word(TEST_SP + 6, 0xBEEF); // OSErr result slot
        cpu.write_reg(Register::D0, 0x0304); // selector 4, 6 param bytes
        disp.dispatch_dialog(true, 0x268, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let default_result = bus.read_word(TEST_SP + 6);
        let default_stack_after = cpu.read_reg(Register::A7);
        let default_adef_item = bus.read_word(dialog_ptr + 168);
        let default_tracking_item = disp
            .dialog_tracking
            .as_ref()
            .map(|tracking| tracking.default_item)
            .unwrap_or(-1);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 7); // newItem
        bus.write_long(TEST_SP + 2, dialog_ptr); // theDialog
        bus.write_word(TEST_SP + 6, 0xCAFE); // OSErr result slot
        cpu.write_reg(Register::D0, 0x0305); // selector 5, 6 param bytes
        disp.dispatch_dialog(true, 0x268, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let cancel_result = bus.read_word(TEST_SP + 6);
        let cancel_stack_after = cpu.read_reg(Register::A7);
        let cancel_map_item = disp.dialog_cancel_items.get(&dialog_ptr).copied();
        let cancel_tracking_item = disp
            .dialog_tracking
            .as_ref()
            .map(|tracking| tracking.cancel_item)
            .unwrap_or(-1);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 1); // tracks = TRUE
        bus.write_long(TEST_SP + 2, dialog_ptr); // theDialog
        bus.write_word(TEST_SP + 6, 0xCAFE); // OSErr result slot
        cpu.write_reg(Register::D0, 0x0306); // selector 6, 6 param bytes
        disp.dispatch_dialog(true, 0x268, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let tracks_result = bus.read_word(TEST_SP + 6);
        let tracks_stack_after = cpu.read_reg(Register::A7);
        let tracks_adef_item = bus.read_word(dialog_ptr + 168);
        let tracks_cancel_map_item = disp.dialog_cancel_items.get(&dialog_ptr).copied();
        let tracks_tracking_items = disp
            .dialog_tracking
            .as_ref()
            .map(|tracking| (tracking.default_item, tracking.cancel_item))
            .unwrap_or((-1, -1));

        DialogDispatchDefaultCancelSnapshot {
            default_result,
            default_stack_after,
            default_adef_item,
            default_tracking_item,
            cancel_result,
            cancel_stack_after,
            cancel_map_item,
            cancel_tracking_item,
            tracks_result,
            tracks_stack_after,
            tracks_adef_item,
            tracks_cancel_map_item,
            tracks_tracking_items,
        }
    }

    fn updtdialog_update_region_results_for_theme(
        theme_id: UiThemeId,
    ) -> UpdtDialogUpdateRegionSnapshot {
        let (mut disp, mut cpu, mut bus) = setup_with_port();
        disp.set_ui_theme_id(theme_id);

        let initial_port = 0x181000;
        disp.set_current_port_state(&mut bus, &mut cpu, initial_port, None);

        let screen_base = bus.alloc(100 * 100);
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 100, 100, 100, 8);

        let dialog_ptr = bus.alloc(256);
        bus.write_word(dialog_ptr, 0);
        bus.write_long(dialog_ptr + 2, screen_base);
        bus.write_word(dialog_ptr + 6, 100);
        bus.write_word(dialog_ptr + 8, 0);
        bus.write_word(dialog_ptr + 10, 0);
        bus.write_word(dialog_ptr + 12, 100);
        bus.write_word(dialog_ptr + 14, 100);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 100);
        bus.write_word(dialog_ptr + 22, 100);
        bus.write_word(dialog_ptr + 108, 2);
        disp.window_list.push(dialog_ptr);
        disp.front_window = dialog_ptr;
        disp.dialog_initial_draw_deferred.insert(dialog_ptr);
        disp.dialog_items.insert(
            dialog_ptr,
            vec![
                DialogItem {
                    item_type: 0,
                    rect: (8, 8, 24, 32),
                    proc_ptr: 0x500000,
                    ..Default::default()
                },
                DialogItem {
                    item_type: 0,
                    rect: (60, 60, 80, 80),
                    proc_ptr: 0x600000,
                    ..Default::default()
                },
                DialogItem {
                    item_type: 8,
                    rect: (90, 90, 96, 96),
                    text: "x".to_string(),
                    ..Default::default()
                },
                DialogItem {
                    item_type: 4,
                    rect: (52, 8, 72, 44),
                    text: "OK".to_string(),
                    ..Default::default()
                },
            ],
        );
        let inside_update_pixel = screen_base + 4 * 100 + 4;
        let outside_update_pixel = screen_base + 70 * 100 + 70;
        let outside_item_pixel = screen_base + 52 * 100 + 9;
        bus.write_byte(inside_update_pixel, 0xA5);
        bus.write_byte(outside_update_pixel, 0x5A);
        bus.write_byte(outside_item_pixel, 0x3C);

        let update_rgn_ptr = bus.alloc(10);
        let update_rgn = bus.alloc(4);
        bus.write_long(update_rgn, update_rgn_ptr);
        bus.write_word(update_rgn_ptr, 10);
        bus.write_word(update_rgn_ptr + 2, 0);
        bus.write_word(update_rgn_ptr + 4, 0);
        bus.write_word(update_rgn_ptr + 6, 40);
        bus.write_word(update_rgn_ptr + 8, 40);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, update_rgn);
        bus.write_long(TEST_SP + 4, dialog_ptr);
        disp.dispatch_dialog(true, 0x178, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        UpdtDialogUpdateRegionSnapshot {
            stack_after: cpu.read_reg(Register::A7),
            the_port_after: bus.read_long(crate::memory::globals::addr::THE_PORT),
            current_port_after: disp.current_port,
            deferred_after: disp.dialog_initial_draw_deferred.contains(&dialog_ptr),
            queued_draw_procs: disp
                .modeless_dialog_draw_proc_queue
                .iter()
                .copied()
                .collect(),
            item_count_after: disp
                .dialog_items
                .get(&dialog_ptr)
                .map(Vec::len)
                .unwrap_or_default(),
            inside_update_pixel_after: bus.read_byte(inside_update_pixel),
            outside_update_pixel_after: bus.read_byte(outside_update_pixel),
            outside_item_pixel_after: bus.read_byte(outside_item_pixel),
        }
    }

    #[test]
    fn systemless_theme_does_not_change_dialogselect_item_hits() {
        // IM:I I-417: DialogSelect reports enabled dialog item hits through
        // its Boolean result, dialog pointer, and itemHit. Theme rendering is
        // outside that guest-visible event contract.
        let classic = dialog_select_enabled_user_item_hit_for_theme(UiThemeId::ClassicSystem7);
        let themed = dialog_select_enabled_user_item_hit_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(classic.0, 0xFFFF);
        assert_eq!(classic.2, 1);
        assert_eq!(classic.3, TEST_SP + 12);
        assert_eq!(
            themed, classic,
            "systemless-default must not change DialogSelect item-hit semantics"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_dialogselect_control_item_hits() {
        // IM:I I-417: for mouseDown in an enabled control, DialogSelect
        // returns TRUE and itemHit after Control Manager tracking succeeds.
        // The app owns checkbox value changes, so theme chrome must not alter
        // the ControlRecord value or dialog-scoped control-value mirror.
        let classic = dialog_select_enabled_checkbox_hit_for_theme(UiThemeId::ClassicSystem7);
        let themed = dialog_select_enabled_checkbox_hit_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(classic.0, 0xFFFF);
        assert_eq!(classic.2, 1);
        assert_eq!(classic.3, TEST_SP + 12);
        assert_eq!(classic.4, 1);
        assert_eq!(classic.5, 1);
        assert_eq!(
            themed, classic,
            "systemless-default must not change DialogSelect control item-hit or value semantics"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_dialogselect_window_events() {
        // MTE 1992 p. 6-139 and IM:I I-417: activate/update events for a
        // dialog window are handled by DialogSelect and return FALSE rather
        // than reporting an enabled item through theDialog/itemHit.
        // MTE 1992 pp. 6-141 and 6-143 plus IM:I I-291: update handling
        // brackets the redraw with BeginUpdate/EndUpdate, uses the dialog
        // port, redraws the dialog, and leaves app-owned userItem drawing
        // to the guest draw procs.
        // Theme rendering can change pixels, but not this function ABI,
        // output-slot behavior, stack protocol, or update bookkeeping.
        let classic = dialog_select_window_event_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = dialog_select_window_event_results_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(classic.update_result, 0);
        assert_eq!(classic.update_dialog_out, 0xDEAD_BEEF);
        assert_eq!(classic.update_item_hit, 0xCAFE);
        assert_eq!(classic.update_stack_after, TEST_SP + 12);
        assert!(!classic.update_deferred_after);
        assert_eq!(classic.update_region_after, None);
        assert_eq!(classic.update_vis_region_after, Some((0, 0, 100, 100)));
        assert_eq!(
            classic.update_the_port_after,
            classic.update_current_port_after
        );
        assert_eq!(classic.update_current_port_after, classic.dialog_ptr);
        assert!(!classic.update_saved_vis_after);
        assert_eq!(
            classic.update_queued_draw_procs,
            vec![(classic.dialog_ptr, 0x500000, 1)]
        );
        assert_eq!(classic.update_item_count_after, 3);
        assert_eq!(classic.activate_result, 0);
        assert_eq!(classic.activate_dialog_out, 0xDEAD_BEEF);
        assert_eq!(classic.activate_item_hit, 0xCAFE);
        assert_eq!(classic.activate_stack_after, TEST_SP + 12);
        assert_eq!(
            themed, classic,
            "systemless-default must not change DialogSelect update/activate handling"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_modaldialog_edit_text_mouse_handling() {
        // IM:I I-415: ModalDialog handles mouseDown in an editText item using
        // TextEdit and returns the item when it is enabled. Theme rendering
        // must not change the returned item, retained-dialog state, active
        // editField, TERecord mirroring, queued mouse-up consumption, or stack
        // protocol.
        let classic = modal_dialog_edit_text_mouse_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = modal_dialog_edit_text_mouse_results_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(classic.item_hit, 2);
        assert_eq!(classic.stack_after, TEST_SP + 8);
        assert!(classic.tracking_finished);
        assert!(classic.retained_visible_snapshot);
        assert!(classic.saved_background_retained);
        assert!(classic.queued_mouse_up_consumed);
        assert_eq!(classic.edit_field, 1);
        assert_eq!(classic.item_selection, (2, 4));
        assert_eq!(classic.te_text, b"Second".to_vec());
        assert_eq!(classic.te_length, 6);
        assert_eq!(classic.te_selection, (2, 4));
        assert_eq!(classic.handle_bytes, b"Second".to_vec());
        assert_eq!(
            themed, classic,
            "systemless-default must not change ModalDialog editText mouse handling"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_modaldialog_keyboard_default_cancel_items() {
        // MTE 1992 p. 6-30 and p. 6-138: Return/Enter activate the default
        // button; Esc and Command-period activate the Cancel button. Theme
        // chrome must not change the key-to-item mapping, flash lifecycle,
        // returned itemHit, or Pascal stack protocol.
        let classic = modal_dialog_keyboard_button_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = modal_dialog_keyboard_button_results_for_theme(UiThemeId::SystemlessDefault);

        for case in [&classic.return_key, &classic.enter_key] {
            assert_eq!(case.flash_item_after_key, 1);
            assert_eq!(case.stack_after_key, TEST_SP);
            assert_eq!(case.item_hit_after_key, 0xCAFE);
            assert_eq!(case.item_hit_after_flash, 1);
            assert_eq!(case.stack_after_flash, TEST_SP + 8);
            assert!(case.tracking_finished);
        }
        for case in [&classic.escape_key, &classic.command_period] {
            assert_eq!(case.flash_item_after_key, 2);
            assert_eq!(case.stack_after_key, TEST_SP);
            assert_eq!(case.item_hit_after_key, 0xCAFE);
            assert_eq!(case.item_hit_after_flash, 2);
            assert_eq!(case.stack_after_flash, TEST_SP + 8);
            assert!(case.tracking_finished);
        }
        assert_eq!(
            themed, classic,
            "systemless-default must not change ModalDialog keyboard default/cancel handling"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_modaldialog_update_events() {
        // MTE 1992 pp. 6-135 and 6-141: ModalDialog routes update events
        // through IsDialogEvent/DialogSelect-style handling. That brackets
        // the redraw with BeginUpdate/EndUpdate and makes the dialog the
        // current graphics port, while ModalDialog itself does not return
        // an item for update events. Theme chrome must not alter this ABI or
        // the retained visible dialog snapshot used across modal refires.
        let classic = modal_dialog_update_event_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = modal_dialog_update_event_results_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(classic.item_hit_after, 0xCAFE);
        assert_eq!(classic.stack_after, TEST_SP);
        assert!(!classic.tracking_finished);
        assert!(classic.rendered_pixels_final);
        assert_eq!(classic.retained_pixel_after, 0x44);
        assert_eq!(classic.retained_snapshot_pixel_after, 0x44);
        assert_eq!(classic.update_region_after, None);
        assert_eq!(classic.vis_region_after, Some((0, 0, 80, 80)));
        assert_eq!(classic.the_port_after, classic.current_port_after);
        assert_eq!(classic.current_port_after, classic.dialog_ptr);
        assert!(!classic.saved_vis_after);
        assert_eq!(
            themed, classic,
            "systemless-default must not change ModalDialog update-event handling"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_dialog_lifecycle_cleanup() {
        // MTE 1992 pp. 6-119..6-120: CloseDialog removes the dialog from
        // the screen/window list, while DisposeDialog calls CloseDialog and
        // additionally releases dialog-owned item-list/dialog-record storage.
        // Theme chrome must not alter Pascal stack discipline, active
        // ModalDialog cleanup, retained-click cleanup, saved snapshot
        // cleanup, front-window promotion, current-port side effects, or
        // dialog-scoped side-map cleanup.
        let classic = dialog_lifecycle_cleanup_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = dialog_lifecycle_cleanup_results_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(classic.close_stack_after, TEST_SP + 4);
        assert!(classic.close_tracking_cleared);
        assert!(classic.close_retained_click_cleared);
        assert!(classic.close_visible_snapshot_cleared);
        assert!(classic.close_modal_entered_cleared);
        assert!(classic.close_saved_pixels_cleared);
        assert!(!classic.close_window_list_contains_dialog);
        assert_eq!(
            classic.close_front_window_after,
            classic.close_the_port_after
        );
        assert_eq!(
            classic.close_front_window_after,
            classic.close_current_port_after
        );
        assert!(classic.close_update_event_for_previous_window);
        assert!(!classic.close_dialog_items_present_after);

        assert_eq!(classic.dispose_stack_after, TEST_SP + 4);
        assert!(classic.dispose_tracking_cleared);
        assert!(classic.dispose_retained_click_cleared);
        assert!(classic.dispose_visible_snapshot_cleared);
        assert!(classic.dispose_modal_entered_cleared);
        assert!(classic.dispose_saved_pixels_cleared);
        assert!(!classic.dispose_window_list_contains_dialog);
        assert_eq!(
            classic.dispose_front_window_after,
            classic.dispose_the_port_after
        );
        assert_eq!(
            classic.dispose_front_window_after,
            classic.dispose_current_port_after
        );
        assert!(classic.dispose_update_event_for_previous_window);
        assert!(!classic.dispose_dialog_items_present_after);
        assert!(classic.dispose_other_dialog_items_present_after);
        assert!(!classic.dispose_dialog_item_handle_present_after);
        assert!(classic.dispose_other_dialog_item_handle_present_after);
        assert!(!classic.dispose_dialog_control_handle_present_after);
        assert!(classic.dispose_other_dialog_control_handle_present_after);
        assert!(!classic.dispose_dialog_control_value_present_after);
        assert!(classic.dispose_other_dialog_control_value_present_after);
        assert!(!classic.dispose_hidden_rect_present_after);
        assert!(classic.dispose_other_hidden_rect_present_after);
        assert!(!classic.dispose_cancel_item_present_after);
        assert!(classic.dispose_other_cancel_item_present_after);
        assert!(classic.dispose_pending_popup_cleared);
        assert_eq!(
            themed, classic,
            "systemless-default must not change CloseDialog/DisposeDialog lifecycle cleanup"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_dialog_creation_records() {
        // IM:I I-412 and MTE 1992 pp. 6-118..6-119: NewDialog creates a
        // dialog from caller-supplied parameters and an item-list handle,
        // sets dialogKind/default dialog fields, and returns a DialogPtr.
        // IM:I I-412 and I-424: GetNewDialog reads DLOG/DITL resources and
        // uses a copy of the item list. Theme chrome must not alter resource
        // parsing, item-list ownership, DialogRecord fields, visible/update
        // bookkeeping, current-port side effects, or Pascal stack ABI.
        let classic = dialog_creation_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = dialog_creation_results_for_theme(UiThemeId::SystemlessDefault);

        assert_ne!(classic.new_dialog_ptr, 0);
        assert_eq!(classic.new_stack_after, TEST_SP);
        assert_eq!(classic.new_result_slot, classic.new_dialog_ptr);
        assert_eq!(classic.new_window_list, vec![classic.new_dialog_ptr]);
        assert_eq!(classic.new_front_window, classic.new_dialog_ptr);
        assert_eq!(classic.new_current_port, classic.new_dialog_ptr);
        assert_eq!(classic.new_the_port, classic.new_dialog_ptr);
        assert_eq!(classic.new_visible_byte, 0xFF);
        assert_eq!(classic.new_goaway_byte, 0xFF);
        assert_eq!(classic.new_refcon, 0x1234_5678);
        assert_eq!(classic.new_window_kind, 2);
        assert_eq!(classic.new_proc_id, 4);
        assert_eq!(classic.new_port_rect, (0, 0, 80, 150));
        assert_ne!(classic.new_items_handle, 0);
        assert_ne!(classic.new_items_data_ptr, 0);
        assert!(classic.new_first_item_handle_nonzero);
        assert_eq!(
            classic.new_cached_items,
            vec![
                (16, (10, 12, 28, 120), "Alpha".to_string(), 0),
                (4, (44, 70, 66, 132), "OK".to_string(), 0),
            ]
        );
        assert!(classic.new_update_region.is_some());
        assert!(classic.new_update_event_queued);
        assert_eq!(classic.new_edit_field, 0xFFFF);
        assert_eq!(classic.new_default_item, 1);

        assert_ne!(classic.get_dialog_ptr, 0);
        assert_eq!(classic.get_stack_after, TEST_SP + 10);
        assert_eq!(classic.get_result_slot, classic.get_dialog_ptr);
        assert_eq!(classic.get_window_list, vec![classic.get_dialog_ptr]);
        assert_eq!(classic.get_front_window, classic.get_dialog_ptr);
        assert_eq!(classic.get_current_port, classic.get_dialog_ptr);
        assert_eq!(classic.get_the_port, classic.get_dialog_ptr);
        assert_eq!(classic.get_visible_byte, 0xFF);
        assert_eq!(classic.get_refcon, 0);
        assert_eq!(classic.get_window_kind, 2);
        assert_eq!(classic.get_proc_id, 2);
        assert_eq!(classic.get_port_rect, (0, 0, 80, 150));
        assert_ne!(classic.get_items_handle, 0);
        assert_ne!(classic.get_items_data_ptr, 0);
        assert!(classic.get_uses_distinct_ditl_copy);
        assert_eq!(classic.get_original_ditl_handle_field, 0);
        assert!(classic.get_first_item_handle_nonzero);
        assert_eq!(
            classic.get_cached_items,
            vec![
                (16, (12, 18, 30, 128), "Beta".to_string(), 0),
                (4, (46, 78, 68, 138), "OK".to_string(), 0),
            ]
        );
        assert!(classic.get_update_region.is_some());
        assert!(classic.get_update_event_queued);
        assert_eq!(classic.get_edit_field, 0xFFFF);
        assert_eq!(classic.get_default_item, 1);
        assert_eq!(
            themed, classic,
            "systemless-default must not change NewDialog/GetNewDialog creation semantics"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_alert_family_resource_state() {
        // IM:I I-417..I-423 and MTE 1992 pp. 6-105..6-112: Alert,
        // StopAlert, NoteAlert, and CautionAlert read ALRT/DITL resources,
        // use the current alert stage to choose the default item, update
        // ACount/ANumber low-memory state, and share behavior apart from
        // icon drawing. IM:I I-420 also defines CouldAlert/FreeAlert as
        // resource-preparation routines. Theme chrome must not alter those
        // resource, low-memory, result-slot, or stack contracts.
        let classic = alert_family_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = alert_family_results_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(
            classic.staged_calls,
            vec![
                AlertTrapCallSnapshot {
                    trap_word: 0x185,
                    result: 1,
                    stack_after: TEST_SP + 6,
                    alert_stage_after: 1,
                    anumber_after: 2099,
                },
                AlertTrapCallSnapshot {
                    trap_word: 0x186,
                    result: 2,
                    stack_after: TEST_SP + 6,
                    alert_stage_after: 2,
                    anumber_after: 2099,
                },
                AlertTrapCallSnapshot {
                    trap_word: 0x187,
                    result: 1,
                    stack_after: TEST_SP + 6,
                    alert_stage_after: 3,
                    anumber_after: 2099,
                },
                AlertTrapCallSnapshot {
                    trap_word: 0x188,
                    result: 2,
                    stack_after: TEST_SP + 6,
                    alert_stage_after: 3,
                    anumber_after: 2099,
                },
                AlertTrapCallSnapshot {
                    trap_word: 0x185,
                    result: 2,
                    stack_after: TEST_SP + 6,
                    alert_stage_after: 3,
                    anumber_after: 2099,
                },
            ]
        );
        assert_eq!(
            classic.missing_call,
            AlertTrapCallSnapshot {
                trap_word: 0x187,
                result: -1,
                stack_after: TEST_SP + 6,
                alert_stage_after: 2,
                anumber_after: 0xBEEF,
            }
        );
        assert_eq!(
            classic.resource_prep,
            AlertResourcePrepSnapshot {
                could_present: (0, TEST_SP + 2),
                free_present: (0, TEST_SP + 2),
                could_missing: (0, TEST_SP + 2),
                free_missing: (0, TEST_SP + 2),
            }
        );
        assert_eq!(
            themed, classic,
            "systemless-default must not change Alert family resource or low-memory semantics"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_initdialogs_errorsound_state() {
        // IM:I I-411 and MTE 1992 pp. 6-103..6-104: InitDialogs stores
        // ResumeProc, initializes alert sound state, and prepares the
        // ParamText/DAStrings globals; ErrorSound stores the current alert
        // sound ProcPtr in DABeeper, with NIL disabling sound/menu-bar blink.
        // Those low-memory globals and procedure stack pops are app-visible
        // Dialog Manager state and must not depend on the selected renderer.
        let classic = dialog_init_sound_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = dialog_init_sound_results_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(classic.init_stack_after, TEST_SP + 4);
        assert_eq!(classic.resume_proc_after_init, 0x0012_3456);
        assert_eq!(classic.da_beeper_after_init, 0);
        assert_eq!(classic.alert_stage_after_init, 0);
        assert_eq!(classic.anumber_after_init, 0xCAFE);
        assert_eq!(classic.da_strings_after_init, [0, 0, 0, 0]);
        assert_eq!(classic.error_sound_stack_after, TEST_SP + 4);
        assert_eq!(classic.da_beeper_after_error_sound, 0x00AB_CDEF);
        assert_eq!(classic.nil_error_sound_stack_after, TEST_SP + 4);
        assert_eq!(classic.da_beeper_after_nil_error_sound, 0);
        assert_eq!(
            themed, classic,
            "systemless-default must not change InitDialogs/ErrorSound low-memory state"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_dialogdispatch_default_cancel_selectors() {
        // MTE 1992 pp. 6-162..6-166: DialogDispatch selectors extend the
        // Dialog Manager with default-item, cancel-item, and cursor-tracking
        // state. MTE 1992 p. 6-138 ties Return/Enter and Esc/Command-period
        // handling to the default and Cancel items, so theme chrome must not
        // alter selector OSErr results, stack ABI, DialogRecord.aDefItem, the
        // cancel-item side state, or active ModalDialog tracking mirrors.
        let classic = dialogdispatch_default_cancel_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = dialogdispatch_default_cancel_results_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(classic.default_result, 0);
        assert_eq!(classic.default_stack_after, TEST_SP + 6);
        assert_eq!(classic.default_adef_item, 9);
        assert_eq!(classic.default_tracking_item, 9);

        assert_eq!(classic.cancel_result, 0);
        assert_eq!(classic.cancel_stack_after, TEST_SP + 6);
        assert_eq!(classic.cancel_map_item, Some(7));
        assert_eq!(classic.cancel_tracking_item, 7);

        assert_eq!(classic.tracks_result, 0);
        assert_eq!(classic.tracks_stack_after, TEST_SP + 6);
        assert_eq!(classic.tracks_adef_item, 9);
        assert_eq!(classic.tracks_cancel_map_item, Some(7));
        assert_eq!(classic.tracks_tracking_items, (9, 7));
        assert_eq!(
            themed, classic,
            "systemless-default must not change DialogDispatch default/cancel selector semantics"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_updtdialog_update_region_semantics() {
        // MTE 1992 pp. 6-142..6-143: UpdateDialog redraws only items in the
        // supplied update region, calls application-defined item draw procs,
        // and uses SetPort to make the dialog box current before updating.
        // Theme chrome can change pixels but must not change that update-item
        // selection, current-port side effect, deferred redraw state, or stack
        // ABI.
        let classic = updtdialog_update_region_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = updtdialog_update_region_results_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(classic.stack_after, TEST_SP + 8);
        assert_ne!(classic.the_port_after, 0x181000);
        assert_eq!(classic.the_port_after, classic.current_port_after);
        assert!(!classic.deferred_after);
        assert_eq!(classic.queued_draw_procs.len(), 1);
        assert_eq!(classic.queued_draw_procs[0].0, classic.the_port_after);
        assert_eq!(classic.queued_draw_procs[0].1, 0x500000);
        assert_eq!(classic.queued_draw_procs[0].2, 1);
        assert_eq!(classic.item_count_after, 4);
        assert_eq!(classic.inside_update_pixel_after, 0);
        assert_eq!(classic.outside_update_pixel_after, 0x5A);
        assert_eq!(classic.outside_item_pixel_after, 0x3C);
        assert_eq!(
            themed, classic,
            "systemless-default must not change UpdtDialog update-region semantics"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_dialogselect_edit_text_mouse_and_null_events() {
        // MTE 1992 p. 6-139 and IM:I I-417: mouseDown in an enabled
        // editText item is handled through TextEdit and reports the item,
        // while null events with an editText item call TEIdle and return
        // FALSE. Systemless TEClick/TEIdle preserve the TERec text/selection
        // intersection currently shared with the BasiliskII strict bakes, so
        // theme rendering must not alter the active editField, result slots,
        // output-slot behavior, TERecord mirroring, or stack protocol.
        let classic = dialog_select_edit_text_mouse_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = dialog_select_edit_text_mouse_results_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(classic.mouse_result, 0xFFFF);
        assert_ne!(classic.mouse_dialog_out, 0);
        assert_eq!(classic.mouse_item_hit, 2);
        assert_eq!(classic.mouse_stack_after, TEST_SP + 12);
        assert_eq!(classic.mouse_edit_field, 1);
        assert_eq!(classic.mouse_item_selection, (2, 4));
        assert_eq!(classic.mouse_te_text, b"Second".to_vec());
        assert_eq!(classic.mouse_te_length, 6);
        assert_eq!(classic.mouse_te_selection, (2, 4));
        assert_eq!(classic.null_result, 0);
        assert_eq!(classic.null_dialog_out, 0xDEAD_BEEF);
        assert_eq!(classic.null_item_hit, 0xCAFE);
        assert_eq!(classic.null_stack_after, TEST_SP + 12);
        assert_eq!(classic.null_edit_field, 1);
        assert_eq!(classic.null_te_text, b"Second".to_vec());
        assert_eq!(classic.null_te_length, 6);
        assert_eq!(classic.null_te_selection, (2, 4));
        assert_eq!(
            themed, classic,
            "systemless-default must not change DialogSelect editText mouse/null handling"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_dialogselect_edit_text_key_handling() {
        // MTE 1992 p. 6-139: for keyDown/autoKey events in editable text
        // items, DialogSelect uses TextEdit to handle text entry/editing and
        // reports the edit item through itemHit. Text mutation, selection
        // collapse, shared TERecord state, result slots, and stack ABI are
        // guest-visible Dialog Manager behavior, not theme rendering choices.
        let classic = dialog_select_edit_text_key_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = dialog_select_edit_text_key_results_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(classic.keydown_result, 0xFFFF);
        assert_ne!(classic.keydown_dialog_out, 0);
        assert_eq!(classic.keydown_item_hit, 1);
        assert_eq!(classic.keydown_stack_after, TEST_SP + 12);
        assert_eq!(classic.keydown_handle_bytes, b"HYo".to_vec());
        assert_eq!(classic.keydown_cached_text, "HYo");
        assert_eq!(classic.keydown_item_selection, (2, 2));
        assert_eq!(classic.keydown_te_text, b"HYo".to_vec());
        assert_eq!(classic.keydown_te_length, 3);
        assert_eq!(classic.keydown_te_selection, (2, 2));
        assert_eq!(classic.autokey_result, 0xFFFF);
        assert_eq!(classic.autokey_dialog_out, classic.keydown_dialog_out);
        assert_eq!(classic.autokey_item_hit, 1);
        assert_eq!(classic.autokey_stack_after, TEST_SP + 12);
        assert_eq!(classic.autokey_handle_bytes, b"Ho".to_vec());
        assert_eq!(classic.autokey_cached_text, "Ho");
        assert_eq!(classic.autokey_item_selection, (1, 1));
        assert_eq!(classic.autokey_te_text, b"Ho".to_vec());
        assert_eq!(classic.autokey_te_length, 2);
        assert_eq!(classic.autokey_te_selection, (1, 1));
        assert_eq!(
            themed, classic,
            "systemless-default must not change DialogSelect editText key handling"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_isdialogevent_active_dialog_routing() {
        // MTE 1992 p. 6-138: IsDialogEvent returns TRUE for events that
        // belong to an active dialog, including null events; IM:I I-416 also
        // calls out active-dialog key events, mouse-downs in the content
        // region, and update/activate events targeting a dialog window.
        // Themes must not alter that event-routing or function-stack ABI.
        let classic = is_dialog_event_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = is_dialog_event_results_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(classic.null_event, (0xFFFF, TEST_SP + 4));
        assert_eq!(classic.key_down, (0xFFFF, TEST_SP + 4));
        assert_eq!(classic.mouse_inside, (0xFFFF, TEST_SP + 4));
        assert_eq!(classic.mouse_outside, (0, TEST_SP + 4));
        assert_eq!(classic.update_target, (0xFFFF, TEST_SP + 4));
        assert_eq!(classic.activate_target, (0xFFFF, TEST_SP + 4));
        assert_eq!(
            themed, classic,
            "systemless-default must not change IsDialogEvent routing"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_findditem_hit_testing() {
        // MTE 1992 p. 6-125: FindDialogItem/FindDItem returns the first
        // item-list index containing the local point, including disabled
        // items; themes must not alter this guest-visible hit test.
        let classic = findditem_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = findditem_results_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(
            classic,
            vec![
                (0, TEST_SP + 8),
                (1, TEST_SP + 8),
                (-1, TEST_SP + 8),
                (-1, TEST_SP + 8),
            ]
        );
        assert_eq!(
            themed, classic,
            "systemless-default must not change FindDItem hit-test semantics"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_getsetditem_useritem_records() {
        // MTE 1992 p. 6-121..6-123: GetDialogItem and SetDialogItem expose
        // item type, item handle/ProcPtr, and display rectangle. For
        // application-defined userItems, `item` is the app's draw ProcPtr;
        // theme chrome must not alter that record ABI or stack contract.
        let classic = getsetditem_useritem_record_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = getsetditem_useritem_record_results_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(classic.initial_get.item_type, 0);
        assert_eq!(classic.initial_get.item, 0x0012_3456);
        assert_eq!(classic.initial_get.rect, (12, 24, 36, 48));
        assert_eq!(classic.initial_get.stack_after, TEST_SP + 18);
        assert_eq!(classic.set_stack_after, TEST_SP + 16);
        assert_eq!(classic.stored_type, 0);
        assert_eq!(classic.stored_proc_ptr, 0x00AB_CDEF);
        assert_eq!(classic.stored_rect, (50, 60, 70, 80));
        assert_eq!(classic.post_set_get.item_type, 0);
        assert_eq!(classic.post_set_get.item, 0x00AB_CDEF);
        assert_eq!(classic.post_set_get.rect, (50, 60, 70, 80));
        assert_eq!(classic.post_set_get.stack_after, TEST_SP + 18);
        assert_eq!(
            themed, classic,
            "systemless-default must not change GetDItem/SetDItem userItem record semantics"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_getsetditem_text_control_records() {
        // MTE 1992 p. 6-121..6-123 and IM:I I-421: GetDialogItem and
        // SetDialogItem expose item type, item handle, and display rectangle.
        // Text items use handles consumed by Get/SetDialogItemText; control
        // items use ControlRecord handles. Theme chrome must not alter that
        // guest ABI, DITL storage, side-map relinking, or stack contract.
        let classic = getsetditem_text_control_record_results_for_theme(UiThemeId::ClassicSystem7);
        let themed =
            getsetditem_text_control_record_results_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(classic.text_initial_get.item_type, 16);
        assert_ne!(classic.text_initial_get.item, 0);
        assert_eq!(classic.text_initial_get.rect, (10, 20, 24, 160));
        assert_eq!(classic.text_initial_get.stack_after, TEST_SP + 18);
        assert_eq!(classic.text_set_stack_after, TEST_SP + 16);
        assert!(!classic.text_old_handle_mapped_after);
        let mapped_dialog = classic.text_new_handle_map_value_after.unwrap().0;
        let text_new_handle = classic.text_post_set_get.item;
        assert_ne!(mapped_dialog, 0);
        assert_eq!(classic.text_new_handle_map_value_after.unwrap().1, 0);
        assert_ne!(text_new_handle, classic.text_initial_get.item);
        assert_eq!(classic.text_ditl_handle_after, text_new_handle);
        assert_eq!(classic.text_ditl_rect_after, (12, 22, 28, 168));
        assert_eq!(classic.text_ditl_type_after, 16);
        assert_eq!(classic.text_stored_type_after, 16);
        assert_eq!(classic.text_stored_rect_after, (12, 22, 28, 168));
        assert_eq!(classic.text_cached_text_after, "Updated");
        assert_eq!(classic.text_handle_bytes_after, b"Updated".to_vec());
        assert_eq!(classic.text_post_set_get.item_type, 16);
        assert_eq!(classic.text_post_set_get.item, text_new_handle);
        assert_eq!(classic.text_post_set_get.rect, (12, 22, 28, 168));
        assert_eq!(classic.text_post_set_get.stack_after, TEST_SP + 18);

        assert_eq!(classic.control_initial_get.item_type, 4);
        assert_ne!(classic.control_initial_get.item, 0);
        assert_eq!(classic.control_initial_get.rect, (30, 40, 50, 140));
        assert_eq!(classic.control_initial_get.stack_after, TEST_SP + 18);
        assert_eq!(classic.control_set_stack_after, TEST_SP + 16);
        assert!(!classic.control_old_handle_mapped_after);
        let control_new_handle = classic.control_post_set_get.item;
        assert_eq!(
            classic.control_new_handle_map_value_after.unwrap().0,
            mapped_dialog
        );
        assert_eq!(classic.control_new_handle_map_value_after.unwrap().1, 2);
        assert_ne!(control_new_handle, classic.control_initial_get.item);
        assert_eq!(classic.control_ditl_handle_after, control_new_handle);
        assert_eq!(classic.control_ditl_rect_after, (42, 52, 62, 172));
        assert_eq!(classic.control_ditl_type_after, 5);
        assert_eq!(classic.control_stored_type_after, 5);
        assert_eq!(classic.control_stored_rect_after, (42, 52, 62, 172));
        assert_eq!(classic.control_record_rect_after, (42, 52, 62, 172));
        assert_eq!(classic.control_value_after, Some(2));
        assert_eq!(classic.control_post_set_get.item_type, 5);
        assert_eq!(classic.control_post_set_get.item, control_new_handle);
        assert_eq!(classic.control_post_set_get.rect, (42, 52, 62, 172));
        assert_eq!(classic.control_post_set_get.stack_after, TEST_SP + 18);
        assert_eq!(
            themed, classic,
            "systemless-default must not change GetDItem/SetDItem text/control record semantics"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_hide_show_ditem_visibility_records() {
        // MTE 1992 p. 6-123..6-124: HideDialogItem moves an item's display
        // rectangle offscreen by offsetting left/right, while ShowDialogItem
        // restores the visible rectangle and queues redraw. Themes must not
        // alter that item/control record state, hit testing, or stack ABI.
        let classic = hide_show_ditem_visibility_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = hide_show_ditem_visibility_results_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(classic.initial_get.item_type, 4);
        assert_eq!(classic.initial_get.rect, (10, 20, 30, 120));
        assert_eq!(classic.initial_get.stack_after, TEST_SP + 18);
        assert_eq!(classic.hide_stack_after, TEST_SP + 6);
        assert_eq!(classic.hidden_item_rect, (10, 16404, 30, 16504));
        assert_eq!(classic.hidden_saved_rect, Some((10, 20, 30, 120)));
        assert_eq!(classic.hidden_control_rect, (10, 16404, 30, 16504));
        assert_eq!(classic.hidden_get.item_type, 4);
        assert_eq!(classic.hidden_get.item, classic.initial_get.item);
        assert_eq!(classic.hidden_get.rect, (10, 16404, 30, 16504));
        assert_eq!(classic.hidden_get.stack_after, TEST_SP + 18);
        assert_eq!(classic.hidden_find, (-1, TEST_SP + 8));
        assert_eq!(classic.show_stack_after, TEST_SP + 6);
        assert_eq!(classic.restored_item_rect, (10, 20, 30, 120));
        assert_eq!(classic.restored_saved_rect, None);
        assert_eq!(classic.restored_control_rect, (10, 20, 30, 120));
        assert_eq!(classic.restored_get.item_type, 4);
        assert_eq!(classic.restored_get.item, classic.initial_get.item);
        assert_eq!(classic.restored_get.rect, (10, 20, 30, 120));
        assert_eq!(classic.restored_get.stack_after, TEST_SP + 18);
        assert_eq!(classic.restored_find, (0, TEST_SP + 8));
        assert_eq!(
            themed, classic,
            "systemless-default must not change HideDItem/ShowDItem visibility semantics"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_paramtext_storage_and_substitution() {
        // MTE 1992 p. 6-129..6-130: ParamText stores up to four strings in
        // DAStrings and substitutes ^0..^3 in subsequently created dialog or
        // alert text. This low-memory/global text contract is app-visible and
        // must not depend on the selected rendering provider.
        let classic = paramtext_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = paramtext_results_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(
            classic.initial_slots,
            [
                b"Doc".to_vec(),
                b"42".to_vec(),
                Vec::<u8>::new(),
                b"Tail".to_vec(),
            ]
        );
        assert!(classic.initial_da_handles.iter().all(|handle| *handle != 0));
        assert_eq!(classic.initial_da_strings, classic.initial_slots);
        assert_eq!(classic.initial_stack_after, TEST_SP + 16);
        assert_eq!(classic.initial_substitution, "Cannot open Doc (42)Tail ^9");
        assert_eq!(
            classic.nil_slots,
            [
                b"NewDoc".to_vec(),
                b"42".to_vec(),
                Vec::<u8>::new(),
                b"Tail".to_vec(),
            ]
        );
        assert!(classic.nil_da_handles.iter().all(|handle| *handle != 0));
        assert_eq!(classic.nil_da_strings, classic.nil_slots);
        assert_eq!(classic.nil_da_handles[1], classic.initial_da_handles[1]);
        assert_eq!(classic.nil_da_handles[2], classic.initial_da_handles[2]);
        assert_eq!(classic.nil_da_handles[3], classic.initial_da_handles[3]);
        assert_eq!(classic.nil_stack_after, TEST_SP + 16);
        assert_eq!(classic.nil_substitution, "Cannot open NewDoc (42)Tail ^9");
        assert_eq!(
            themed, classic,
            "systemless-default must not change ParamText storage or substitution semantics"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_dialog_item_text_storage() {
        // MTE 1992 p. 6-130..6-131: GetDialogItemText/GetIText returns the
        // text in a statText/editText item, and SetDialogItemText/SetIText
        // replaces that text. Theme rendering must not alter the text handle,
        // cached item text, Pascal-string output, or stack ABI.
        let classic = dialog_item_text_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = dialog_item_text_results_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(classic.initial_get_text, b"Old".to_vec());
        assert_eq!(classic.initial_get_stack_after, TEST_SP + 8);
        assert_eq!(classic.set_stack_after, TEST_SP + 8);
        assert_eq!(classic.handle_size_after_set, Some(11));
        assert_eq!(classic.handle_bytes_after_set, b"Edited Text".to_vec());
        assert_eq!(classic.cached_text_after_set, "Edited Text");
        assert_eq!(classic.post_set_get_text, b"Edited Text".to_vec());
        assert_eq!(classic.post_set_get_stack_after, TEST_SP + 8);
        assert_eq!(
            themed, classic,
            "systemless-default must not change Get/SetDialogItemText storage semantics"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_select_dialog_item_text_selection() {
        // MTE 1992 p. 6-131..6-132: SelectDialogItemText/SelIText sets the
        // selection range in an editable text item and supports the 0..32767
        // whole-text selection convention. The active editField, TERecord
        // mirrored selection, non-edit no-op, and stack ABI are guest-visible
        // Dialog Manager behavior, not theme rendering choices.
        let classic = select_dialog_item_text_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = select_dialog_item_text_results_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(classic.whole_selection, (0, 5));
        assert_eq!(classic.whole_edit_field, 0);
        assert_eq!(classic.whole_te_selection, (0, 5));
        assert_eq!(classic.whole_stack_after, TEST_SP + 10);
        assert_eq!(classic.normalized_selection, (2, 5));
        assert_eq!(classic.normalized_edit_field, 0);
        assert_eq!(classic.normalized_te_selection, (2, 5));
        assert_eq!(classic.normalized_stack_after, TEST_SP + 10);
        assert_eq!(classic.non_edit_selection, (1, 3));
        assert_eq!(classic.non_edit_edit_field, 0);
        assert_eq!(classic.non_edit_te_selection, (2, 5));
        assert_eq!(classic.non_edit_stack_after, TEST_SP + 10);
        assert_eq!(
            themed, classic,
            "systemless-default must not change SelectDialogItemText selection semantics"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_drawdialog_useritem_pixels() {
        // MTE 1992 p. 6-142: DrawDialog calls application-defined item
        // draw procedures when their rectangles are in the update region.
        // MTE 1992 p. 6-155 defines application-defined items as userItem
        // records; theme chrome must not erase the app-owned rectangle.
        let classic = drawdialog_useritem_pixel_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = drawdialog_useritem_pixel_results_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(classic.0, 0xA5);
        assert_eq!(classic.1, TEST_SP + 4);
        assert_eq!(classic.2, 0xCAFE_BABE);
        assert_eq!(
            themed, classic,
            "systemless-default must not change DrawDialog userItem pixel preservation"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_drawdialog_icon_item_pixels() {
        // MTE 1992 p. 6-155: an icon item's item-list record names an
        // `ICON` resource. MTE 1992 p. 7-63 defines `ICON` as application
        // resource content used in dialog boxes; theme chrome must not alter
        // those pixels inside the icon item rectangle.
        let classic = drawdialog_icon_item_pixel_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = drawdialog_icon_item_pixel_results_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(classic.0, [0xFF, 0xFF, 0xFF]);
        assert_eq!(classic.1, TEST_SP + 4);
        assert_eq!(classic.2, 0xCAFE_BABE);
        assert_eq!(
            themed, classic,
            "systemless-default must not change DrawDialog ICON item pixels"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_drawdialog_cicn_item_pixels() {
        // MTE 1992 p. 6-155: an icon item's item-list record names an
        // `ICON` resource and optionally a same-ID `cicn` resource. MTE 1992
        // p. 7-64 specifies that the Dialog Manager displays that color icon
        // instead of the black-and-white icon on color displays.
        let classic = drawdialog_cicn_item_pixel_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = drawdialog_cicn_item_pixel_results_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(classic.0, [0x33, 0x55, 0x77]);
        assert_eq!(classic.1, TEST_SP + 4);
        assert_eq!(classic.2, 0xCAFE_BABE);
        assert_eq!(
            themed, classic,
            "systemless-default must not change DrawDialog cicn item pixels"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_drawdialog_picture_item_pixels() {
        // MTE 1992 p. 6-155: a picture item's item-list record names a
        // `PICT` resource. That resource is application-supplied picture
        // content, so theme chrome must not alter the pixels DrawDialog
        // renders inside the picture item rectangle.
        let classic = drawdialog_picture_item_pixel_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = drawdialog_picture_item_pixel_results_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(classic.0, [0xFF, 0xFF, 0xFF]);
        assert_eq!(classic.1, TEST_SP + 4);
        assert_eq!(classic.2, 0xCAFE_BABE);
        assert_eq!(
            themed, classic,
            "systemless-default must not change DrawDialog PICT item pixels"
        );
    }

    #[test]
    fn dialog_select_disabled_item_hit_returns_false_and_leaves_outputs_unchanged() {
        // Inside Macintosh Volume I, I-417: disabled items do nothing and
        // DialogSelect returns FALSE.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let event_ptr = 0x300000u32;
        let dialog_out_ptr = 0x300100u32;
        let item_hit_ptr = 0x300104u32;

        disp.front_window = dialog_ptr;
        bus.write_word(dialog_ptr + 8, 0);
        bus.write_word(dialog_ptr + 10, 0);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 100);
        bus.write_word(dialog_ptr + 22, 160);
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 0x80, // disabled userItem
                rect: (20, 30, 60, 110),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        bus.write_word(event_ptr, 1); // mouseDown
        bus.write_long(event_ptr + 2, 0);
        bus.write_long(event_ptr + 6, 0);
        bus.write_word(event_ptr + 10, 130);
        bus.write_word(event_ptr + 12, 240);
        bus.write_word(event_ptr + 14, 0x0080);

        bus.write_long(dialog_out_ptr, 0x12345678);
        bus.write_word(item_hit_ptr, 0x7F7F);

        bus.write_long(TEST_SP, item_hit_ptr);
        bus.write_long(TEST_SP + 4, dialog_out_ptr);
        bus.write_long(TEST_SP + 8, event_ptr);

        let result = disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
        assert_eq!(bus.read_word(TEST_SP + 12), 0);
        assert_eq!(bus.read_long(dialog_out_ptr), 0x12345678);
        assert_eq!(bus.read_word(item_hit_ptr), 0x7F7F);
    }

    #[test]
    fn dialog_select_keydown_without_edit_text_item_returns_false() {
        // Inside Macintosh Volume I, I-417: keyDown handling returns TRUE only
        // when an enabled editText item is active.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let event_ptr = 0x300000u32;
        let dialog_out_ptr = 0x300100u32;
        let item_hit_ptr = 0x300104u32;

        disp.front_window = dialog_ptr;
        bus.write_word(dialog_ptr + 8, 0);
        bus.write_word(dialog_ptr + 10, 0);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 100);
        bus.write_word(dialog_ptr + 22, 160);
        bus.write_word(dialog_ptr + 164, 0); // editField points at item 1
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 0, // userItem, not editText
                rect: (20, 30, 60, 110),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        bus.write_word(event_ptr, 3); // keyDown
        bus.write_long(event_ptr + 2, 0x00000041); // 'A'
        bus.write_long(event_ptr + 6, 0);
        bus.write_word(event_ptr + 10, 0);
        bus.write_word(event_ptr + 12, 0);
        bus.write_word(event_ptr + 14, 0);

        bus.write_long(TEST_SP, item_hit_ptr);
        bus.write_long(TEST_SP + 4, dialog_out_ptr);
        bus.write_long(TEST_SP + 8, event_ptr);

        let result = disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(TEST_SP + 12), 0);
    }

    #[test]
    fn dialog_select_keydown_with_enabled_edit_text_returns_hit() {
        // Inside Macintosh Volume I, I-417: for keyDown/autoKey with an
        // enabled editText item, DialogSelect returns TRUE and item number.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let event_ptr = 0x300000u32;
        let dialog_out_ptr = 0x300100u32;
        let item_hit_ptr = 0x300104u32;

        disp.front_window = dialog_ptr;
        bus.write_word(dialog_ptr + 8, 0);
        bus.write_word(dialog_ptr + 10, 0);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 100);
        bus.write_word(dialog_ptr + 22, 160);
        bus.write_word(dialog_ptr + 164, 0); // editField = first item
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 16, // editText
                rect: (20, 30, 60, 110),
                text: "Hello".to_string(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        bus.write_word(event_ptr, 3); // keyDown
        bus.write_long(event_ptr + 2, 0x00000041); // 'A'
        bus.write_long(event_ptr + 6, 0);
        bus.write_word(event_ptr + 10, 0);
        bus.write_word(event_ptr + 12, 0);
        bus.write_word(event_ptr + 14, 0);

        bus.write_long(TEST_SP, item_hit_ptr);
        bus.write_long(TEST_SP + 4, dialog_out_ptr);
        bus.write_long(TEST_SP + 8, event_ptr);

        let result = disp.dispatch_dialog(true, 0x180, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(TEST_SP + 12), 0xFFFF);
        assert_eq!(bus.read_long(dialog_out_ptr), dialog_ptr);
        assert_eq!(bus.read_word(item_hit_ptr), 1);
    }

    #[test]
    fn dialog_select_null_event_blinks_active_edit_text_insertion_caret() {
        // MTE 1992 p. 6-139 and IM:I I-417: DialogSelect mouse-down in an
        // enabled editText item displays the insertion point, and later null
        // events call TEIdle so the insertion point blinks.
        let (mut disp, mut cpu, mut bus) = setup_with_port();
        let dialog_ptr = bus.alloc(170);
        let event_ptr = bus.alloc(16);
        let dialog_out_ptr = bus.alloc(4);
        let item_hit_ptr = bus.alloc(2);
        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(16);
        let text_item_handle = TrapDispatcher::allocate_handle_with_data(&mut bus, 0);
        let text_h = make_te_with_text(&mut disp, &mut bus, b"");
        let te_ptr = bus.read_long(text_h);
        let (screen_base, row_bytes, _screen_w, _screen_h, _pixel_size) = disp.screen_mode;

        for i in 0..(row_bytes * 80) {
            bus.write_byte(screen_base + i, 0);
        }

        disp.front_window = dialog_ptr;
        bus.write_word(dialog_ptr + 8, 0);
        bus.write_word(dialog_ptr + 10, 0);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 100);
        bus.write_word(dialog_ptr + 22, 160);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_long(dialog_ptr + 160, text_h);
        bus.write_word(dialog_ptr + 164, 0);

        bus.write_long(items_handle, ditl_ptr);
        bus.write_word(ditl_ptr, 0);
        bus.write_long(ditl_ptr + 2, text_item_handle);
        bus.write_word(ditl_ptr + 6, 20);
        bus.write_word(ditl_ptr + 8, 20);
        bus.write_word(ditl_ptr + 10, 38);
        bus.write_word(ditl_ptr + 12, 120);
        bus.write_byte(ditl_ptr + 14, 16);
        bus.write_byte(ditl_ptr + 15, 0);

        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 16,
                rect: (20, 20, 38, 120),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        let dispatch_dialog_select_event = |disp: &mut TrapDispatcher,
                                            cpu: &mut MockCpu,
                                            bus: &mut MacMemoryBus,
                                            what: u16,
                                            tick: u32| {
            disp.tick_count = tick;
            bus.write_long(crate::memory::globals::addr::TICKS, tick);
            cpu.write_reg(Register::A7, TEST_SP);
            bus.write_word(event_ptr, what);
            bus.write_long(event_ptr + 2, 0);
            bus.write_long(event_ptr + 6, 0);
            bus.write_word(event_ptr + 10, 25);
            bus.write_word(event_ptr + 12, 25);
            bus.write_word(event_ptr + 14, 0);
            bus.write_long(dialog_out_ptr, 0xDEAD_BEEF);
            bus.write_word(item_hit_ptr, 0xCAFE);
            bus.write_long(TEST_SP, item_hit_ptr);
            bus.write_long(TEST_SP + 4, dialog_out_ptr);
            bus.write_long(TEST_SP + 8, event_ptr);
            disp.dispatch_dialog(true, 0x180, cpu, bus)
                .unwrap()
                .unwrap();
        };

        dispatch_dialog_select_event(&mut disp, &mut cpu, &mut bus, 1, 100);
        assert_eq!(bus.read_word(TEST_SP + 12), 0xFFFF);
        assert_eq!(bus.read_long(dialog_out_ptr), dialog_ptr);
        assert_eq!(bus.read_word(item_hit_ptr), 1);
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET), 1);
        assert_eq!(
            bus.read_long(te_ptr + TrapDispatcher::TE_CARET_TIME_OFFSET),
            100
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_CARET_STATE_OFFSET),
            0
        );
        assert!(
            screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
            "DialogSelect editText mouse-down should display the insertion caret"
        );

        dispatch_dialog_select_event(&mut disp, &mut cpu, &mut bus, 0, 131);
        assert_eq!(bus.read_word(TEST_SP + 12), 0);
        assert_eq!(bus.read_long(dialog_out_ptr), 0xDEAD_BEEF);
        assert_eq!(bus.read_word(item_hit_ptr), 0xCAFE);
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_CARET_STATE_OFFSET),
            0
        );
        assert!(
            screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
            "DialogSelect null event before 32 ticks should keep the caret visible"
        );

        dispatch_dialog_select_event(&mut disp, &mut cpu, &mut bus, 0, 132);
        assert_eq!(
            bus.read_long(te_ptr + TrapDispatcher::TE_CARET_TIME_OFFSET),
            132
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_CARET_STATE_OFFSET),
            1
        );
        assert!(
            !screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
            "DialogSelect null event at the 32-tick boundary should hide the caret"
        );

        dispatch_dialog_select_event(&mut disp, &mut cpu, &mut bus, 0, 164);
        assert_eq!(
            bus.read_long(te_ptr + TrapDispatcher::TE_CARET_TIME_OFFSET),
            164
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_CARET_STATE_OFFSET),
            0
        );
        assert!(
            screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
            "the next DialogSelect null-event blink interval should show the caret again"
        );
    }

    // ---- DrawDialog ($A981) ----

    #[test]
    fn draw_dialog_pops_4_bytes() {
        let (mut disp, mut cpu, mut bus) = setup();
        // SP+0: dialog ptr (4 bytes)
        bus.write_long(TEST_SP, 0x200000);

        let result = disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn draw_dialog_repaints_dialog_background_for_known_dialog() {
        // Inside Macintosh Volume I, I-417: DrawDialog draws the contents of
        // the given dialog box.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let (screen_base, row_bytes, _w, _h, pixel_size) = disp.screen_mode;

        bus.write_word(dialog_ptr + 8, 0);
        bus.write_word(dialog_ptr + 10, 0);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 30);
        bus.write_word(dialog_ptr + 22, 40);
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 4, // btnCtrl draws a deterministic border and label
                rect: (8, 8, 20, 32),
                text: "OK".to_string(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        let probe_x = 4u32;
        let probe_y = 4u32;
        let probe_addr = if pixel_size == 8 {
            screen_base + probe_y * row_bytes + probe_x
        } else {
            screen_base + probe_y * row_bytes + (probe_x / 8)
        };
        let probe_bit = 1 << (7 - (probe_x % 8));
        if pixel_size == 8 {
            bus.write_byte(probe_addr, 0xFF); // black in 8bpp CLUT
        } else {
            bus.write_byte(probe_addr, bus.read_byte(probe_addr) | probe_bit);
        }

        bus.write_long(TEST_SP, dialog_ptr);
        let result = disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        if pixel_size == 8 {
            assert_eq!(bus.read_byte(probe_addr), 0); // white in 8bpp CLUT
        } else {
            assert_eq!(bus.read_byte(probe_addr) & probe_bit, 0);
        }
    }

    #[test]
    fn draw_dialog_repaints_document_proc_title_chrome() {
        // DrawDialog is also valid for modeless documentProc dialogs. It must
        // redraw the WDEF title bar from the WindowRecord title instead of
        // falling back to modal-box border chrome (IM:I I-299/I-417).
        let screen_base = 0x300000u32;
        let row_bytes = 64u32;
        let (mut disp, mut cpu, mut bus) = setup();
        disp.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);

        let dialog_ptr = bus.alloc(170);
        bus.write_word(dialog_ptr + 6, 0);
        bus.write_word(dialog_ptr + 8, (-40i16) as u16);
        bus.write_word(dialog_ptr + 10, (-80i16) as u16);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 80);
        bus.write_word(dialog_ptr + 22, 160);
        bus.write_byte(dialog_ptr + 112, 0);
        let title_ptr = bus.alloc(16);
        bus.write_pstring(title_ptr, b"Modeless");
        let title_handle = bus.alloc(4);
        bus.write_long(title_handle, title_ptr);
        bus.write_long(dialog_ptr + 134, title_handle);
        disp.window_proc_ids.insert(dialog_ptr, 0);
        disp.dialog_items.insert(dialog_ptr, Vec::new());

        bus.write_long(TEST_SP, dialog_ptr);
        let result = disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        assert!(
            screen_pixel_is_set(&bus, screen_base, row_bytes, 85, 24),
            "documentProc DrawDialog should redraw active title-bar stripes"
        );
        assert!(
            screen_pixel_is_set(&bus, screen_base, row_bytes, 79, 21),
            "documentProc DrawDialog should redraw the title-bar border"
        );
    }

    #[test]
    fn draw_dialog_radio_items_use_control_value_state() {
        // SetCtlValue stores value 1 for a selected radio control (IM:I
        // I-317/I-328). DrawDialog must honor the dialog control value table
        // for radio items, just as it does for checkboxes.
        let screen_base = 0x300000u32;
        let row_bytes = 64u32;
        let bounds = (40, 40, 120, 220);
        let dialog_ptr = 0x2000;
        let items = vec![DialogItem {
            item_type: 6,
            rect: (20, 20, 40, 160),
            text: "Selected".to_string(),
            resource_id: 0,
            proc_ptr: 0,
            sel_start: 0,
            sel_end: 0,
        }];
        let (mut disp, _cpu, mut bus) = setup();
        disp.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
        disp.dialog_control_values.insert((dialog_ptr, 1), 1);

        disp.draw_dialog(&mut bus, bounds, 1, "", &items, 0, "", 0, false, dialog_ptr);

        assert!(
            screen_pixel_is_set(&bus, screen_base, row_bytes, 67, 69),
            "selected radio item should draw the central dot"
        );
    }

    #[test]
    fn draw_dialog_queues_user_item_draw_proc_for_known_dialog() {
        // MTE 1992 p. 6-142: DrawDialog calls application-defined item
        // draw procs whose userItem display rectangles are in the dialog.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);

        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 80);
        bus.write_word(dialog_ptr + 22, 120);
        disp.dialog_items.insert(
            dialog_ptr,
            vec![
                DialogItem {
                    item_type: 0,
                    rect: (8, 8, 24, 48),
                    proc_ptr: 0x500000,
                    ..Default::default()
                },
                DialogItem {
                    item_type: 0,
                    rect: (100, 8, 120, 48),
                    proc_ptr: 0x600000,
                    ..Default::default()
                },
                DialogItem {
                    item_type: 4,
                    rect: (40, 8, 60, 48),
                    text: "OK".to_string(),
                    ..Default::default()
                },
            ],
        );

        bus.write_long(TEST_SP, dialog_ptr);
        let result = disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(
            disp.modeless_dialog_draw_proc_queue
                .iter()
                .copied()
                .collect::<Vec<_>>(),
            vec![(dialog_ptr, 0x500000, 1)]
        );
    }

    #[test]
    fn draw_button_systemless_theme_routes_dialog_chrome_through_provider() {
        let screen_base = 0x300000u32;
        let row_bytes = 64u32;

        let (mut classic, _classic_cpu, mut classic_bus) = setup();
        classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
        classic.draw_button(&mut classic_bus, 20, 20, 40, 80, "", true);

        let (mut themed, _themed_cpu, mut themed_bus) = setup();
        themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
        themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
        themed.draw_button(&mut themed_bus, 20, 20, 40, 80, "", true);

        assert!(
            !screen_pixel_is_set(&classic_bus, screen_base, row_bytes, 16, 16),
            "classic default-button round rect should leave its outer corner as background"
        );
        assert!(
            screen_pixel_is_set(&themed_bus, screen_base, row_bytes, 16, 16),
            "systemless-default provider chrome should own the default-button outline corner"
        );
    }

    #[test]
    fn draw_checkbox_and_radio_systemless_theme_route_dialog_chrome_through_provider() {
        let screen_base = 0x300000u32;
        let row_bytes = 64u32;

        let (mut classic, _classic_cpu, mut classic_bus) = setup();
        classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
        classic.draw_checkbox(&mut classic_bus, 20, 20, 40, 120, "", true);
        classic.draw_radio(&mut classic_bus, 50, 20, 70, 120, "", true);

        let (mut themed, _themed_cpu, mut themed_bus) = setup();
        themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
        themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
        themed.draw_checkbox(&mut themed_bus, 20, 20, 40, 120, "", true);
        themed.draw_radio(&mut themed_bus, 50, 20, 70, 120, "", true);

        let classic_checkbox_pixels =
            count_set_pixels(&classic_bus, screen_base, row_bytes, 21, 20, 33, 32);
        let themed_checkbox_pixels =
            count_set_pixels(&themed_bus, screen_base, row_bytes, 21, 20, 33, 32);
        assert!(
            themed_checkbox_pixels > classic_checkbox_pixels,
            "systemless-default dialog checkbox provider should draw a denser selected mark ({themed_checkbox_pixels} <= {classic_checkbox_pixels})"
        );
        let classic_radio_pixels =
            count_set_pixels(&classic_bus, screen_base, row_bytes, 51, 20, 63, 32);
        let themed_radio_pixels =
            count_set_pixels(&themed_bus, screen_base, row_bytes, 51, 20, 63, 32);
        assert!(
            themed_radio_pixels > classic_radio_pixels,
            "systemless-default dialog radio provider should draw a denser selected mark ({themed_radio_pixels} <= {classic_radio_pixels})"
        );
    }

    #[test]
    fn draw_popup_control_systemless_theme_routes_dialog_chrome_through_provider() {
        let screen_base = 0x300000u32;
        let row_bytes = 64u32;

        let (mut classic, _classic_cpu, mut classic_bus) = setup();
        classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
        classic.draw_popup_control(&mut classic_bus, 20, 20, 40, 120, "");

        let (mut themed, _themed_cpu, mut themed_bus) = setup();
        themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
        themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
        themed.draw_popup_control(&mut themed_bus, 20, 20, 40, 120, "");

        assert!(
            screen_pixel_is_set(&classic_bus, screen_base, row_bytes, 120, 39),
            "classic popup CDEF should draw its one-pixel offset shadow"
        );
        assert!(
            !screen_pixel_is_set(&themed_bus, screen_base, row_bytes, 120, 39),
            "systemless-default popup provider should not draw the classic offset shadow"
        );
    }

    #[test]
    fn draw_edit_text_systemless_theme_routes_caret_through_provider() {
        let screen_base = 0x300000u32;
        let row_bytes = 64u32;

        let (mut classic, _classic_cpu, mut classic_bus) = setup();
        classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
        classic.draw_edit_text(&mut classic_bus, 20, 20, 40, 100, "", false);

        let (mut themed, _themed_cpu, mut themed_bus) = setup();
        themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
        themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
        themed.draw_edit_text(&mut themed_bus, 20, 20, 40, 100, "", false);

        assert!(
            !screen_pixel_is_set(&classic_bus, screen_base, row_bytes, 22, 22),
            "classic editText caret should remain a single vertical bar"
        );
        assert!(
            screen_pixel_is_set(&themed_bus, screen_base, row_bytes, 22, 22),
            "systemless-default provider should draw the caret cap"
        );
    }

    #[test]
    fn draw_edit_text_systemless_theme_routes_field_frame_through_provider() {
        let screen_base = 0x300000u32;
        let row_bytes = 64u32;

        let (mut classic, _classic_cpu, mut classic_bus) = setup();
        classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
        classic.draw_edit_text(&mut classic_bus, 20, 20, 40, 100, "", false);

        let (mut themed, _themed_cpu, mut themed_bus) = setup();
        themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
        themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
        themed.draw_edit_text(&mut themed_bus, 20, 20, 40, 100, "", false);

        assert!(
            !screen_pixel_is_set(&classic_bus, screen_base, row_bytes, 19, 22),
            "classic editText field should keep the upper interior clear"
        );
        assert!(
            screen_pixel_is_set(&themed_bus, screen_base, row_bytes, 19, 22),
            "systemless-default provider should draw focused editText field chrome"
        );
    }

    #[test]
    fn draw_dialog_systemless_theme_routes_disabled_edit_text_state_through_provider() {
        let screen_base = 0x300000u32;
        let row_bytes = 64u32;
        let bounds = (40, 40, 110, 190);
        let items = vec![DialogItem {
            item_type: 0x80 | 16,
            rect: (20, 20, 44, 130),
            text: "Disabled".to_string(),
            resource_id: 0,
            proc_ptr: 0,
            sel_start: 0,
            sel_end: 0,
        }];

        let (mut classic, _classic_cpu, mut classic_bus) = setup();
        classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
        classic.draw_dialog(
            &mut classic_bus,
            bounds,
            1,
            "",
            &items,
            0,
            "",
            0,
            false,
            0x2000,
        );

        let (mut themed, _themed_cpu, mut themed_bus) = setup();
        themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
        themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
        themed.draw_dialog(
            &mut themed_bus,
            bounds,
            1,
            "",
            &items,
            0,
            "",
            0,
            false,
            0x2000,
        );

        let disabled_inner_x =
            bounds.1 + items[0].rect.1 - TrapDispatcher::EDIT_TEXT_FRAME_OUTSET + 2;
        let disabled_inner_y = bounds.0 + items[0].rect.0 + 2;
        assert!(
            !screen_pixel_is_set(
                &classic_bus,
                screen_base,
                row_bytes,
                disabled_inner_x,
                disabled_inner_y
            ),
            "classic editText field should keep the disabled-state inner frame pixel clear"
        );
        assert!(
            screen_pixel_is_set(
                &themed_bus,
                screen_base,
                row_bytes,
                disabled_inner_x,
                disabled_inner_y
            ),
            "systemless-default provider should receive disabled editText field state"
        );
    }

    #[test]
    fn draw_dialog_systemless_theme_routes_disabled_standard_controls_through_provider() {
        let screen_base = 0x300000u32;
        let row_bytes = 64u32;
        let bounds = (40, 40, 140, 210);
        let items = vec![
            DialogItem {
                item_type: 0x80 | 4,
                rect: (20, 20, 40, 82),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            },
            DialogItem {
                item_type: 0x80 | 5,
                rect: (52, 20, 72, 120),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            },
            DialogItem {
                item_type: 0x80 | 6,
                rect: (84, 20, 104, 120),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            },
        ];

        let (mut classic, _classic_cpu, mut classic_bus) = setup();
        classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
        classic.draw_dialog(
            &mut classic_bus,
            bounds,
            1,
            "",
            &items,
            0,
            "",
            0,
            false,
            0x2000,
        );

        let (mut themed, _themed_cpu, mut themed_bus) = setup();
        themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
        themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
        themed.draw_dialog(
            &mut themed_bus,
            bounds,
            1,
            "",
            &items,
            0,
            "",
            0,
            false,
            0x2000,
        );

        // The unselected disabled checkbox is intentionally pixel-equivalent
        // after classic CDEF mark alignment; selected checkbox routing is
        // covered by draw_checkbox_and_radio_systemless_theme_route_dialog_chrome_through_provider.
        let probes = [
            (
                bounds.1 + items[0].rect.1 + 2,
                bounds.0 + items[0].rect.0 + 2,
            ),
            (
                bounds.1 + items[2].rect.1 + 2,
                bounds.0
                    + items[2].rect.0
                    + (items[2].rect.2
                        - items[2].rect.0
                        - TrapDispatcher::STANDARD_CONTROL_MARK_SIZE)
                        / 2,
            ),
        ];
        for (x, y) in probes {
            assert!(
                !screen_pixel_is_set(&classic_bus, screen_base, row_bytes, x, y),
                "classic disabled DITL controls should keep standard appearance at ({x},{y})"
            );
            assert!(
                screen_pixel_is_set(&themed_bus, screen_base, row_bytes, x, y),
                "systemless-default provider should receive disabled DITL control state at ({x},{y})"
            );
        }
    }

    #[test]
    fn draw_dialog_classic_keeps_disabled_checkbox_and_radio_titles_undimmed() {
        let screen_base = 0x300000u32;
        let row_bytes = 64u32;
        let bounds = (40, 40, 190, 230);
        let items = vec![
            DialogItem {
                item_type: 5,
                rect: (20, 20, 40, 150),
                text: "Sound".to_string(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            },
            DialogItem {
                item_type: 0x80 | 5,
                rect: (52, 20, 72, 150),
                text: "Sound".to_string(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            },
            DialogItem {
                item_type: 6,
                rect: (84, 20, 104, 150),
                text: "Music".to_string(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            },
            DialogItem {
                item_type: 0x80 | 6,
                rect: (116, 20, 136, 150),
                text: "Music".to_string(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            },
        ];

        let (mut dispatcher, _cpu, mut bus) = setup();
        dispatcher.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
        dispatcher.draw_dialog(&mut bus, bounds, 1, "", &items, 0, "", 0, false, 0x2000);

        let label_left = bounds.1
            + items[0].rect.1
            + TrapDispatcher::STANDARD_CONTROL_MARK_LEFT_INSET
            + TrapDispatcher::STANDARD_CONTROL_MARK_SIZE
            + TrapDispatcher::STANDARD_CONTROL_TITLE_GAP;
        let enabled_checkbox = count_set_pixels(
            &bus,
            screen_base,
            row_bytes,
            bounds.0 + items[0].rect.0,
            label_left,
            bounds.0 + items[0].rect.2,
            bounds.1 + items[0].rect.3,
        );
        let disabled_checkbox = count_set_pixels(
            &bus,
            screen_base,
            row_bytes,
            bounds.0 + items[1].rect.0,
            label_left,
            bounds.0 + items[1].rect.2,
            bounds.1 + items[1].rect.3,
        );
        let enabled_radio = count_set_pixels(
            &bus,
            screen_base,
            row_bytes,
            bounds.0 + items[2].rect.0,
            label_left,
            bounds.0 + items[2].rect.2,
            bounds.1 + items[2].rect.3,
        );
        let disabled_radio = count_set_pixels(
            &bus,
            screen_base,
            row_bytes,
            bounds.0 + items[3].rect.0,
            label_left,
            bounds.0 + items[3].rect.2,
            bounds.1 + items[3].rect.3,
        );

        assert!(
            enabled_checkbox > 0,
            "enabled checkbox title should be visible"
        );
        assert_eq!(
            enabled_checkbox, disabled_checkbox,
            "classic itemDisable checkbox title must not be visually dimmed"
        );
        assert!(enabled_radio > 0, "enabled radio title should be visible");
        assert_eq!(
            enabled_radio, disabled_radio,
            "classic itemDisable radio title must not be visually dimmed"
        );
    }

    #[test]
    fn draw_dialog_standard_control_hilite_255_titles_use_gray_device_index() {
        let screen_base = 0x300000u32;
        let row_bytes = 160u32;
        let bounds = (20, 20, 120, 220);
        let dialog_ptr = 0x2400u32;
        let items = vec![
            DialogItem {
                item_type: 5,
                rect: (20, 20, 40, 150),
                text: "Sound".to_string(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            },
            DialogItem {
                item_type: 6,
                rect: (52, 20, 72, 150),
                text: "Music".to_string(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            },
        ];

        let (mut dispatcher, _cpu, mut bus) = setup();
        dispatcher.set_screen_mode_for_test(screen_base, row_bytes, 160, 140, 8);
        dispatcher.device_clut = [[0x2020, 0x4040, 0x6060]; 256];
        dispatcher.device_clut[0] = [0xFFFF, 0xFFFF, 0xFFFF];
        dispatcher.device_clut[42] = [0xA1A1, 0xA1A1, 0xA1A1];
        dispatcher.device_clut[255] = [0, 0, 0];
        for offset in 0..(row_bytes * 140) {
            bus.write_byte(screen_base + offset, 0);
        }

        for (item_no, proc_id, item) in [(1i16, 1i16, &items[0]), (2, 2, &items[1])] {
            let ctrl_ptr = bus.alloc(296);
            let ctrl_handle = bus.alloc(4);
            bus.write_long(ctrl_handle, ctrl_ptr);
            dispatcher.initialize_control_record(
                &mut bus,
                ctrl_ptr,
                dialog_ptr,
                item.rect,
                item.text.as_bytes(),
                true,
                0,
                0,
                1,
                proc_id,
                0,
            );
            bus.write_byte(ctrl_ptr + 17, 255);
            dispatcher
                .dialog_control_handles
                .insert(ctrl_handle, (dialog_ptr, item_no));
            dispatcher
                .dialog_control_values
                .insert((dialog_ptr, item_no), 0);
        }

        dispatcher.draw_dialog(&mut bus, bounds, 1, "", &items, 0, "", 0, false, dialog_ptr);

        for (label, item) in [("checkbox", &items[0]), ("radio", &items[1])] {
            let label_left = bounds.1
                + item.rect.1
                + TrapDispatcher::STANDARD_CONTROL_MARK_LEFT_INSET
                + TrapDispatcher::STANDARD_CONTROL_MARK_SIZE
                + TrapDispatcher::STANDARD_CONTROL_TITLE_GAP;
            let top = bounds.0 + item.rect.0;
            let bottom = bounds.0 + item.rect.2;
            let right = bounds.1 + item.rect.3;
            let gray = count_pixel_index(
                &bus,
                screen_base,
                row_bytes,
                top,
                label_left,
                bottom,
                right,
                42,
            );
            let black = count_pixel_index(
                &bus,
                screen_base,
                row_bytes,
                top,
                label_left,
                bottom,
                right,
                255,
            );
            assert!(
                gray > 12,
                "inactive standard DITL {label} title should draw with the device gray index"
            );
            assert_eq!(
                black, 0,
                "inactive standard DITL {label} title must not leave black glyph pixels"
            );
        }
    }

    #[test]
    fn draw_dialog_systemless_theme_routes_frame_through_provider() {
        let screen_base = 0x300000u32;
        let row_bytes = 64u32;
        let bounds = (40, 40, 100, 140);

        let (mut classic, _classic_cpu, mut classic_bus) = setup();
        classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
        classic.draw_dialog(
            &mut classic_bus,
            bounds,
            1,
            "",
            &[],
            0,
            "",
            0,
            false,
            0x2000,
        );

        let (mut themed, _themed_cpu, mut themed_bus) = setup();
        themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
        themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
        themed.draw_dialog(&mut themed_bus, bounds, 1, "", &[], 0, "", 0, false, 0x2000);

        assert!(
            !screen_pixel_is_set(&classic_bus, screen_base, row_bytes, 50, 38),
            "classic dBoxProc leaves the gap between outer and inner borders clear"
        );
        assert!(
            screen_pixel_is_set(&themed_bus, screen_base, row_bytes, 50, 38),
            "systemless-default provider should own dialog frame accent pixels"
        );
    }

    #[test]
    fn draw_window_frame_systemless_theme_routes_border_through_provider() {
        let screen_base = 0x300000u32;
        let row_bytes = 64u32;

        let (mut classic, _classic_cpu, mut classic_bus) = setup();
        classic.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
        classic.window_bounds = (40, 40, 100, 140);
        classic.window_proc_id = 1;
        classic.draw_window_frame(&mut classic_bus);

        let (mut themed, _themed_cpu, mut themed_bus) = setup();
        themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
        themed.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
        themed.window_bounds = (40, 40, 100, 140);
        themed.window_proc_id = 1;
        themed.draw_window_frame(&mut themed_bus);

        assert!(
            !screen_pixel_is_set(&classic_bus, screen_base, row_bytes, 50, 34),
            "classic dBoxProc window frame keeps the structure gap clear"
        );
        assert!(
            screen_pixel_is_set(&themed_bus, screen_base, row_bytes, 50, 34),
            "systemless-default provider should own window frame accent pixels"
        );
    }

    #[test]
    fn draw_dialog_preserves_existing_user_item_pixels() {
        // UserItems are application-owned drawing areas. Games often draw
        // into them before calling DrawDialog; the Dialog Manager redraw
        // must not erase that content while refreshing the standard items.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let (screen_base, row_bytes, _w, _h, pixel_size) = disp.screen_mode;

        bus.write_word(dialog_ptr + 8, 0);
        bus.write_word(dialog_ptr + 10, 0);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 40);
        bus.write_word(dialog_ptr + 22, 80);
        disp.dialog_items.insert(
            dialog_ptr,
            vec![
                DialogItem {
                    item_type: 0x80, // disabled userItem
                    rect: (8, 8, 20, 32),
                    text: String::new(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
                DialogItem {
                    item_type: 4,
                    rect: (24, 8, 36, 40),
                    text: "OK".to_string(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
            ],
        );

        let user_x = 10u32;
        let user_y = 10u32;
        let background_x = 4u32;
        let background_y = 4u32;
        if pixel_size == 8 {
            bus.write_byte(screen_base + user_y * row_bytes + user_x, 0xFF);
            bus.write_byte(screen_base + background_y * row_bytes + background_x, 0xFF);
        } else {
            for (x, y) in [(user_x, user_y), (background_x, background_y)] {
                let addr = screen_base + y * row_bytes + (x / 8);
                let bit = 1 << (7 - (x % 8));
                bus.write_byte(addr, bus.read_byte(addr) | bit);
            }
        }

        bus.write_long(TEST_SP, dialog_ptr);
        let result = disp.dispatch_dialog(true, 0x181, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        if pixel_size == 8 {
            assert_eq!(
                bus.read_byte(screen_base + user_y * row_bytes + user_x),
                0xFF,
                "DrawDialog should preserve pre-drawn userItem pixels"
            );
            assert_eq!(
                bus.read_byte(screen_base + background_y * row_bytes + background_x),
                0,
                "DrawDialog should still repaint normal dialog background"
            );
        } else {
            let user_addr = screen_base + user_y * row_bytes + (user_x / 8);
            let user_bit = 1 << (7 - (user_x % 8));
            let background_addr = screen_base + background_y * row_bytes + (background_x / 8);
            let background_bit = 1 << (7 - (background_x % 8));
            assert_ne!(bus.read_byte(user_addr) & user_bit, 0);
            assert_eq!(bus.read_byte(background_addr) & background_bit, 0);
        }
    }

    #[test]
    fn modal_dialog_first_entry_does_not_preserve_invisible_user_item_background() {
        // A newly shown modal dialog has no meaningful userItem pixels yet.
        // Dialog Manager fills the dialog background; it must not treat the
        // previous screen contents as application-owned userItem drawing.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let item_hit_ptr = bus.alloc(2);
        let (screen_base, row_bytes, _w, _h, pixel_size) = disp.screen_mode;
        assert_eq!(pixel_size, 8);

        bus.write_word(dialog_ptr + 8, 0);
        bus.write_word(dialog_ptr + 10, 0);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 40);
        bus.write_word(dialog_ptr + 22, 80);
        bus.write_word(dialog_ptr + 108, 2);
        disp.front_window = dialog_ptr;
        disp.window_bounds = (0, 0, 40, 80);
        disp.window_proc_id = 2;
        disp.dialog_items.insert(
            dialog_ptr,
            vec![
                DialogItem {
                    item_type: 0x80, // disabled userItem placeholder
                    rect: (8, 8, 20, 32),
                    text: String::new(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
                DialogItem {
                    item_type: 4,
                    rect: (24, 8, 36, 40),
                    text: "OK".to_string(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
            ],
        );

        let user_x = 10u32;
        let user_y = 10u32;
        bus.write_byte(screen_base + user_y * row_bytes + user_x, 0xFF);

        bus.write_long(TEST_SP, item_hit_ptr);
        bus.write_long(TEST_SP + 4, 0);
        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        assert_eq!(
            bus.read_byte(screen_base + user_y * row_bytes + user_x),
            0,
            "ModalDialog should paint a clean dialog background through untouched userItems"
        );
        assert!(disp.dialog_tracking.is_some());
    }

    #[test]
    fn modal_dialog_first_entry_restores_visible_shell_before_user_item_preservation() {
        // Visible GetNewDialog draws a clean shell before the first ModalDialog
        // call. If later screen/chrome drawing leaves unrelated pixels in a
        // userItem rect, ModalDialog must restore that clean shell before
        // preserving application-owned userItem pixels. EVO's startup
        // shareware notice exposes this when title/menu art leaks through its
        // large userItem body.
        let (mut disp, mut cpu, mut bus) = setup();
        let screen_base = bus.alloc((800 * 600) as u32);
        bus.write_bytes(screen_base, &vec![0x77; 800 * 600]);
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 800, 800, 600, 8);

        let mut dlog = build_test_dlog((100, 100, 180, 260), 1711, 0);
        dlog[10] = 1; // visible
        let ditl = build_test_ditl_items(&[
            (0, (8, 8, 30, 80), b"".as_slice()),
            (4, (44, 20, 64, 90), b"OK".as_slice()),
        ]);
        disp.install_test_resource(&mut bus, *b"DLOG", 1710, &dlog);
        disp.install_test_resource(&mut bus, *b"DITL", 1711, &ditl);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, 0xFFFF_FFFF); // behind
        bus.write_long(TEST_SP + 4, 0); // dStorage
        bus.write_word(TEST_SP + 8, 1710); // dialogID
        disp.dispatch_dialog(true, 0x17C, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        let dlg_ptr = bus.read_long(TEST_SP + 10);
        assert_ne!(dlg_ptr, 0);
        assert!(disp.dialog_visible_snapshots.contains_key(&dlg_ptr));

        let probe_v = 120u32;
        let probe_h = 120u32;
        let probe_addr = screen_base + probe_v * 800 + probe_h;
        assert_eq!(
            bus.read_byte(probe_addr),
            0,
            "visible dialog creation should paint the userItem area as dialog background"
        );

        bus.write_byte(probe_addr, 0x55);
        let item_hit_ptr = bus.alloc(2);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, item_hit_ptr);
        bus.write_long(TEST_SP + 4, 0);
        disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        assert_eq!(
            bus.read_byte(probe_addr),
            0,
            "first ModalDialog entry should not preserve stale non-dialog pixels in userItems"
        );
        assert!(disp.dialog_tracking.is_some());
        assert!(
            !disp.dialog_visible_snapshots.contains_key(&dlg_ptr),
            "first ModalDialog entry consumes the clean visible shell snapshot"
        );
    }

    #[test]
    fn premodal_visible_dialog_snapshot_ignores_bulk_screen_blits() {
        // Background windows may bulk-blit menu/title art after a visible
        // dialog is already on screen. Until ModalDialog or a userItem draw
        // proc owns the dialog, those bulk blits are not dialog content and
        // must not replace the retained visible-dialog snapshot.
        let (mut disp, _cpu, mut bus) = setup();
        let screen_base = bus.alloc((64 * 64) as u32);
        bus.write_bytes(screen_base, &vec![0x00; 64 * 64]);
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 64, 64, 64, 8);

        let dialog_ptr = 0x200000u32;
        let bounds = (10, 10, 30, 30);
        let snapshot_width = (bounds.3 - bounds.1 + TrapDispatcher::DBOX_FRAME_MARGIN * 2) as usize;
        let snapshot_height =
            (bounds.2 - bounds.0 + TrapDispatcher::DBOX_FRAME_MARGIN * 2) as usize;
        disp.front_window = dialog_ptr;
        disp.dialog_items.insert(dialog_ptr, Vec::new());
        disp.dialog_visible_snapshots.insert(
            dialog_ptr,
            PersistentDialogSnapshot {
                bounds,
                pixels: vec![0x11; snapshot_width * snapshot_height],
            },
        );

        let probe = screen_base + 20 * 64 + 20;
        bus.write_byte(probe, 0x77);
        disp.refresh_visible_dialog_snapshot_after_bulk_port_draw(&bus, dialog_ptr);
        bus.write_byte(probe, 0x00);
        disp.restore_visible_dialog_snapshots(&mut bus);
        assert_eq!(
            bus.read_byte(probe),
            0x11,
            "pre-modal bulk blits must not replace a visible dialog snapshot"
        );

        disp.dialog_modal_entered.insert(dialog_ptr);
        bus.write_byte(probe, 0x88);
        disp.refresh_visible_dialog_snapshot_after_bulk_port_draw(&bus, dialog_ptr);
        bus.write_byte(probe, 0x00);
        disp.restore_visible_dialog_snapshots(&mut bus);
        assert_eq!(
            bus.read_byte(probe),
            0x88,
            "retained modal dialogs may refresh snapshots from later bulk drawing"
        );

        let game_dialog_ptr = 0x200100u32;
        let game_bounds = (34, 10, 54, 30);
        bus.write_word(game_dialog_ptr + 6, 0xC000);
        let pixmap_handle = bus.alloc(4);
        let pixmap_ptr = bus.alloc(50);
        bus.write_long(pixmap_handle, pixmap_ptr);
        bus.write_long(pixmap_ptr, screen_base);
        bus.write_word(pixmap_ptr + 4, 0x8000 | 64);
        bus.write_word(pixmap_ptr + 6, (-(game_bounds.0)) as u16);
        bus.write_word(pixmap_ptr + 8, (-(game_bounds.1)) as u16);
        bus.write_word(pixmap_ptr + 32, 8);
        bus.write_long(game_dialog_ptr + 2, pixmap_handle);
        bus.write_word(game_dialog_ptr + 16, 0);
        bus.write_word(game_dialog_ptr + 18, 0);
        bus.write_word(game_dialog_ptr + 20, 20);
        bus.write_word(game_dialog_ptr + 22, 20);
        bus.write_byte(game_dialog_ptr + 110, 0xFF);
        disp.dialog_items.insert(
            game_dialog_ptr,
            vec![DialogItem {
                item_type: 0,
                rect: (0, 0, 20, 20),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );
        disp.front_window = game_dialog_ptr;
        bus.write_byte(screen_base + 40 * 64 + 20, 0x99);
        disp.refresh_visible_dialog_snapshot_after_bulk_port_draw(&bus, game_dialog_ptr);
        bus.write_byte(screen_base + 40 * 64 + 20, 0x00);
        disp.restore_visible_dialog_snapshots(&mut bus);
        assert_eq!(
            bus.read_byte(screen_base + 40 * 64 + 20),
            0x99,
            "game-managed dialogs should create snapshots from app-owned bulk drawing"
        );
    }

    #[test]
    fn updtdialog_pops_eight_bytes() {
        // Inside Macintosh Volume I, I-415: UpdtDialog is a Pascal
        // procedure taking theDialog and updateRgn.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp = TEST_SP;
        bus.write_long(sp, 0x300100); // updateRgn
        bus.write_long(sp + 4, 0x200000); // theDialog

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

    #[test]
    fn updtdialog_nil_update_region_is_a_noop() {
        // Inside Macintosh Volume I, I-415 names updateRgn as the dialog's
        // update region. A NIL region should not trigger redraw work.
        let (mut disp, mut cpu, mut bus) = setup();
        let existing = 0x200040u32;
        disp.window_list = vec![existing];
        disp.front_window = existing;
        bus.write_byte(existing + 110, 0xFF);

        let screen_base = bus.alloc((800 * 600) as u32);
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 800, 800, 600, 8);

        let bounds_rect_ptr = 0x301200u32;
        bus.write_word(bounds_rect_ptr, 100);
        bus.write_word(bounds_rect_ptr + 2, 100);
        bus.write_word(bounds_rect_ptr + 4, 300);
        bus.write_word(bounds_rect_ptr + 6, 400);

        let sp = TEST_SP - 30;
        cpu.write_reg(Register::A7, sp);
        for i in 0..34u32 {
            bus.write_byte(sp + i, 0);
        }
        bus.write_long(sp + 22, bounds_rect_ptr);
        bus.write_word(sp + 16, 1); // visible
        bus.write_long(sp + 10, 0); // behind = NIL (backmost)

        let result = disp.dispatch_dialog(true, 0x17D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        let dlg_ptr = bus.read_long(sp + 30);
        assert_ne!(dlg_ptr, 0);

        let probe_x = 120u32;
        let probe_y = 120u32;
        let probe_addr = screen_base + probe_y * 800 + probe_x;
        bus.write_byte(probe_addr, 0xFF);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, 0); // NIL update region
        bus.write_long(TEST_SP + 4, dlg_ptr);

        let result = disp.dispatch_dialog(true, 0x178, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_byte(probe_addr), 0xFF);
    }

    #[test]
    fn updtdialog_queues_user_item_draw_proc_for_intersecting_update_region() {
        // MTE 1992, 6-142 to 6-143: UpdateDialog redraws only items in
        // the supplied update region and calls application-defined item
        // draw procedures.
        let (mut disp, mut cpu, mut bus) = setup();
        let screen_base = bus.alloc(100 * 100);
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 100, 100, 100, 8);

        let dialog_ptr = bus.alloc(256);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 100);
        bus.write_word(dialog_ptr + 22, 100);
        bus.write_word(dialog_ptr + 108, 2); // plain dialog box
        disp.dialog_items.insert(
            dialog_ptr,
            vec![
                DialogItem {
                    item_type: 0,
                    rect: (8, 8, 24, 32),
                    proc_ptr: 0x500000,
                    ..Default::default()
                },
                DialogItem {
                    item_type: 0,
                    rect: (60, 60, 80, 80),
                    proc_ptr: 0x600000,
                    ..Default::default()
                },
            ],
        );
        let update_rgn_ptr = bus.alloc(10);
        let update_rgn = bus.alloc(4);
        bus.write_long(update_rgn, update_rgn_ptr);
        bus.write_word(update_rgn_ptr, 10);
        bus.write_word(update_rgn_ptr + 2, 0);
        bus.write_word(update_rgn_ptr + 4, 0);
        bus.write_word(update_rgn_ptr + 6, 40);
        bus.write_word(update_rgn_ptr + 8, 40);

        bus.write_long(TEST_SP, update_rgn);
        bus.write_long(TEST_SP + 4, dialog_ptr);
        let result = disp.dispatch_dialog(true, 0x178, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(
            disp.modeless_dialog_draw_proc_queue
                .iter()
                .copied()
                .collect::<Vec<_>>(),
            vec![(dialog_ptr, 0x500000, 1)]
        );
    }

    #[test]
    fn te_text_box_erases_box_before_drawing_text() {
        // Inside Macintosh: Text 1993, p. 2-88: TETextBox erases the box
        // and then draws wrapped/aligned text into it.
        let (mut disp, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;
        let screen_base = bus.read_long(0x0824);

        // Fill the target area black so a missing EraseRect is visible.
        for y in 0..12u32 {
            for byte in 0..3u32 {
                bus.write_byte(screen_base + y * 64 + byte, 0xFF);
            }
        }

        // Make sure the text renderer has a usable size and port state.
        disp.current_port = port_ptr;
        disp.tx_size = 12;
        bus.write_word(port_ptr + 74, 12);

        let text_ptr = 0x200000u32;
        bus.write_byte(text_ptr, b'A');

        let box_ptr = 0x200100u32;
        bus.write_word(box_ptr, 0); // top
        bus.write_word(box_ptr + 2, 0); // left
        bus.write_word(box_ptr + 4, 12); // bottom
        bus.write_word(box_ptr + 6, 24); // right

        let sp = TEST_SP - 14;
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0); // teFlushLeft
        bus.write_long(sp + 2, box_ptr);
        bus.write_long(sp + 6, 1); // length
        bus.write_long(sp + 10, text_ptr);

        let result = disp.dispatch_dialog(true, 0x1CE, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);

        // Pixel (20, 10) lies inside the box but outside the glyph; it should
        // be white after the implicit EraseRect.
        let probe_addr = screen_base + 10 * 64 + 2;
        assert_eq!(bus.read_byte(probe_addr) & (1 << 3), 0);
    }

    #[test]
    fn te_text_box_erases_box_for_zero_length_text() {
        // Inside Macintosh: Text 1993, p. 2-88: the erase precedes text
        // drawing and is not conditional on the text length.
        let (mut disp, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;
        let screen_base = bus.read_long(0x0824);

        for y in 0..12u32 {
            for byte in 0..3u32 {
                bus.write_byte(screen_base + y * 64 + byte, 0xFF);
            }
        }

        disp.current_port = port_ptr;
        disp.tx_size = 12;
        bus.write_word(port_ptr + 74, 12);

        let text_ptr = 0x200000u32;
        let box_ptr = 0x200100u32;
        bus.write_word(box_ptr, 0);
        bus.write_word(box_ptr + 2, 0);
        bus.write_word(box_ptr + 4, 12);
        bus.write_word(box_ptr + 6, 24);

        let sp = TEST_SP - 14;
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0);
        bus.write_long(sp + 2, box_ptr);
        bus.write_long(sp + 6, 0);
        bus.write_long(sp + 10, text_ptr);

        let result = disp.dispatch_dialog(true, 0x1CE, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);

        let probe_addr = screen_base + 10 * 64 + 2;
        assert_eq!(bus.read_byte(probe_addr) & (1 << 3), 0);
    }

    #[test]
    fn te_text_box_consumes_align_box_length_text_arguments() {
        // Inside Macintosh: Text 1993, p. 2-88 declares TETextBox as a
        // procedure taking text/length/box/align arguments.
        let (mut disp, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;
        disp.current_port = port_ptr;
        disp.tx_size = 12;
        bus.write_word(port_ptr + 74, 12);

        let text_ptr = 0x200200u32;
        bus.write_byte(text_ptr, b'A');

        let box_ptr = 0x200240u32;
        bus.write_word(box_ptr, 0);
        bus.write_word(box_ptr + 2, 0);
        bus.write_word(box_ptr + 4, 20);
        bus.write_word(box_ptr + 6, 60);

        let sp = TEST_SP - 14;
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0);
        bus.write_long(sp + 2, box_ptr);
        bus.write_long(sp + 6, 1);
        bus.write_long(sp + 10, text_ptr);

        let result = disp.dispatch_dialog(true, 0x1CE, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
    }

    #[test]
    fn te_text_box_align_parameter_controls_rendered_line_origin() {
        // Inside Macintosh: Text 1993, p. 2-87 defines teJustLeft(0),
        // teJustCenter(1), and teJustRight(-1) alignment values.
        let (mut disp, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;
        disp.current_port = port_ptr;
        disp.tx_size = 12;
        bus.write_word(port_ptr + 74, 12);

        let text = b"ABC";
        let text_ptr = 0x200280u32;
        bus.write_bytes(text_ptr, text);

        let box_ptr = 0x2002C0u32;
        let box_top = 0i16;
        let box_left = 10i16;
        let box_bottom = 40i16;
        let box_right = 110i16;
        bus.write_word(box_ptr, box_top as u16);
        bus.write_word(box_ptr + 2, box_left as u16);
        bus.write_word(box_ptr + 4, box_bottom as u16);
        bus.write_word(box_ptr + 6, box_right as u16);

        let advance_extra = disp.advance_extra();
        let missing_advance = disp.missing_glyph_advance();
        let mut line_width = 0i16;
        for &byte in text {
            if let Some((glyph, _)) =
                crate::quickdraw::text::get_glyph(disp.tx_font, disp.tx_size, byte as char)
            {
                line_width += glyph.advance as i16 + advance_extra;
            } else {
                line_width += missing_advance;
            }
        }

        let mut run_align = |align: i16| -> i16 {
            let sp = TEST_SP - 14;
            cpu.write_reg(Register::A7, sp);
            bus.write_word(sp, align as u16);
            bus.write_long(sp + 2, box_ptr);
            bus.write_long(sp + 6, text.len() as u32);
            bus.write_long(sp + 10, text_ptr);
            let result = disp.dispatch_dialog(true, 0x1CE, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
            disp.pn_loc.1
        };

        let x_left = run_align(0);
        let x_center = run_align(1);
        let x_right = run_align(-1);

        let expected_left =
            TrapDispatcher::te_line_origin_x(0, box_left, box_right, line_width) + line_width;
        let expected_center =
            TrapDispatcher::te_line_origin_x(1, box_left, box_right, line_width) + line_width;
        let expected_right =
            TrapDispatcher::te_line_origin_x(-1, box_left, box_right, line_width) + line_width;

        assert_eq!(x_left, expected_left);
        assert_eq!(x_center, expected_center);
        assert_eq!(x_right, expected_right);
        assert!(x_left < x_center && x_center < x_right);
    }

    #[test]
    fn te_text_box_pascal_procedure_protocol_does_not_overwrite_past_arg_frame() {
        // Per Inside Macintosh: Text 1993, p. 2-88 TETextBox is a
        // Pascal PROCEDURE. The MPW Universal Headers C declaration
        // uses `const Rect *box`, so the caller pushes exactly 14
        // bytes (2-byte just + 4-byte Rect* + 4-byte long + 4-byte
        // text Ptr) and the trap must pop exactly 14 bytes with no
        // result slot written. Mirrors the strict bake's B4 predicate
        // in a9ce_tetextbox_strict.
        let (mut disp, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;
        disp.current_port = port_ptr;
        disp.tx_size = 12;
        bus.write_word(port_ptr + 74, 12);

        let text_ptr = 0x200400u32;
        bus.write_byte(text_ptr, b'X');

        let box_ptr = 0x200440u32;
        bus.write_word(box_ptr, 0);
        bus.write_word(box_ptr + 2, 0);
        bus.write_word(box_ptr + 4, 12);
        bus.write_word(box_ptr + 6, 64);

        // Pre-poison memory immediately past the 14-byte arg frame.
        // After the trap pops 14 bytes, A7 must equal TEST_SP and the
        // sentinel words at TEST_SP, TEST_SP+2 must survive untouched.
        let sp = TEST_SP - 14;
        bus.write_word(TEST_SP, 0xCAFE);
        bus.write_word(TEST_SP + 2, 0xBABE);
        cpu.write_reg(Register::A7, sp);
        bus.write_word(sp, 0); // align = teJustLeft
        bus.write_long(sp + 2, box_ptr);
        bus.write_long(sp + 6, 1); // length
        bus.write_long(sp + 10, text_ptr);

        let result = disp.dispatch_dialog(true, 0x1CE, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
        assert_eq!(bus.read_word(TEST_SP), 0xCAFE);
        assert_eq!(bus.read_word(TEST_SP + 2), 0xBABE);
    }

    #[test]
    fn te_text_box_wraps_when_text_exceeds_box_width() {
        // Inside Macintosh: Text 1993, p. 2-88: TETextBox word-wraps text
        // in the destination rectangle.
        let (mut disp, mut cpu, mut bus) = setup_with_port();
        let port_ptr = 0x181000u32;
        disp.current_port = port_ptr;
        disp.tx_size = 12;
        bus.write_word(port_ptr + 74, 12);

        let text = b"A A A A";
        let text_ptr = 0x200300u32;
        bus.write_bytes(text_ptr, text);

        let mut glyph_w = disp.missing_glyph_advance();
        if let Some((glyph, _)) = crate::quickdraw::text::get_glyph(disp.tx_font, disp.tx_size, 'A')
        {
            glyph_w = glyph.advance as i16 + disp.advance_extra();
        }

        let wide_box_ptr = 0x200340u32;
        bus.write_word(wide_box_ptr, 0);
        bus.write_word(wide_box_ptr + 2, 0);
        bus.write_word(wide_box_ptr + 4, 80);
        bus.write_word(wide_box_ptr + 6, 140);

        let narrow_box_ptr = 0x200380u32;
        bus.write_word(narrow_box_ptr, 0);
        bus.write_word(narrow_box_ptr + 2, 0);
        bus.write_word(narrow_box_ptr + 4, 80);
        bus.write_word(narrow_box_ptr + 6, glyph_w as u16);

        let mut run_box = |box_ptr: u32| -> i16 {
            let sp = TEST_SP - 14;
            cpu.write_reg(Register::A7, sp);
            bus.write_word(sp, 0);
            bus.write_long(sp + 2, box_ptr);
            bus.write_long(sp + 6, text.len() as u32);
            bus.write_long(sp + 10, text_ptr);
            let result = disp.dispatch_dialog(true, 0x1CE, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
            disp.pn_loc.0
        };

        let y_wide = run_box(wide_box_ptr);
        let y_narrow = run_box(narrow_box_ptr);
        let metrics = crate::quickdraw::text::get_font_metrics(disp.tx_font, disp.tx_size);
        let line_height = metrics.ascent + metrics.descent + metrics.leading.max(2);

        assert!(
            y_narrow >= y_wide + line_height,
            "narrow box should force at least one additional wrapped line"
        );
    }

    // ---- CloseDialog ($A982) ----

    fn alloc_region_handle(bus: &mut MacMemoryBus, rect: Option<(i16, i16, i16, i16)>) -> u32 {
        let rgn_ptr = bus.alloc(10);
        bus.write_word(rgn_ptr, 10);
        if let Some((top, left, bottom, right)) = rect.filter(|r| r.2 > r.0 && r.3 > r.1) {
            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);
        } else {
            bus.write_long(rgn_ptr + 2, 0);
            bus.write_long(rgn_ptr + 6, 0);
        }
        let handle = bus.alloc(4);
        bus.write_long(handle, rgn_ptr);
        handle
    }

    fn seed_window_regions(
        bus: &mut MacMemoryBus,
        window_ptr: u32,
        content_rect: (i16, i16, i16, i16),
    ) {
        // Minimal WindowRecord region setup for invalidate_window_rect:
        // contRgn @ +118 and updateRgn @ +122.
        bus.write_word(window_ptr + 16, content_rect.0 as u16);
        bus.write_word(window_ptr + 18, content_rect.1 as u16);
        bus.write_word(window_ptr + 20, content_rect.2 as u16);
        bus.write_word(window_ptr + 22, content_rect.3 as u16);
        let cont_rgn = alloc_region_handle(bus, Some(content_rect));
        let update_rgn = alloc_region_handle(bus, None);
        bus.write_long(window_ptr + 118, cont_rgn);
        bus.write_long(window_ptr + 122, update_rgn);
        bus.write_byte(window_ptr + 110, 0xFF);
    }

    fn seed_app_owned_modal_dialog(
        disp: &mut TrapDispatcher,
        bus: &mut MacMemoryBus,
        dialog_ptr: u32,
        prev_window: u32,
        proc_id: i16,
    ) {
        seed_window_regions(bus, dialog_ptr, (0, 0, 100, 220));
        seed_window_regions(bus, prev_window, (0, 0, 342, 512));
        bus.write_word(dialog_ptr + 8, 0);
        bus.write_word(dialog_ptr + 10, 0);
        bus.write_word(dialog_ptr + 108, 2);
        disp.window_proc_ids.insert(dialog_ptr, proc_id);
        disp.front_window = dialog_ptr;
        disp.current_port = dialog_ptr;
        disp.window_bounds = (0, 0, 100, 220);
        disp.window_proc_id = proc_id;
        disp.window_list = vec![dialog_ptr, prev_window];
        disp.window_stack
            .push((prev_window, (0, 0, 342, 512), 0, "Game".to_string()));
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 4,
                rect: (60, 130, 82, 200),
                text: "OK".to_string(),
                ..DialogItem::default()
            }],
        );
        disp.sent_open_app_event = true;
    }

    fn seed_retained_modal_dialog(
        disp: &mut TrapDispatcher,
        bus: &mut MacMemoryBus,
        dialog_ptr: u32,
        prev_window: u32,
        proc_id: i16,
    ) {
        seed_app_owned_modal_dialog(disp, bus, dialog_ptr, prev_window, proc_id);
        disp.dialog_modal_entered.insert(dialog_ptr);
    }

    #[test]
    fn close_dialog_pops_4_bytes() {
        // Inside Macintosh Volume I, I-413: CloseDialog takes one
        // DialogPtr argument.
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_long(TEST_SP, 0x200000);

        let result = disp.dispatch_dialog(true, 0x182, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn close_dialog_front_dialog_restores_previous_port_state() {
        // IM:I I-413 says CloseDialog behaves like CloseWindow; when the
        // front dialog closes, the window behind it becomes frontmost.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x200000u32;
        let prev_window = 0x181000u32;
        seed_window_regions(&mut bus, prev_window, (0, 0, 342, 512));

        disp.front_window = dialog_ptr;
        disp.current_port = dialog_ptr;
        disp.window_bounds = (100, 120, 220, 320);
        bus.write_long(crate::memory::globals::addr::THE_PORT, dialog_ptr);
        let a5 = cpu.read_reg(Register::A5);
        let global_ptr = bus.read_long(a5);
        bus.write_long(global_ptr, dialog_ptr);
        disp.window_stack
            .push((prev_window, (0, 0, 342, 512), 2, "Prev".to_string()));

        bus.write_long(TEST_SP, dialog_ptr);

        let result = disp.dispatch_dialog(true, 0x182, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(disp.front_window, prev_window);
        assert_eq!(disp.current_port, prev_window);
        assert_eq!(
            bus.read_long(crate::memory::globals::addr::THE_PORT),
            prev_window
        );
        assert_eq!(bus.read_long(global_ptr), prev_window);
        assert_eq!(
            TrapDispatcher::region_handle_rect(&bus, bus.read_long(prev_window + 122)),
            Some((100, 120, 220, 320)),
            "CloseDialog should invalidate the dialog-exposed area on the promoted window"
        );
        assert!(
            disp.event_queue.iter().any(|event| {
                event.what == 8 && event.message == prev_window && (event.modifiers & 1) != 0
            }),
            "CloseDialog should queue activateEvt for the promoted front window"
        );
        assert!(
            disp.event_queue
                .iter()
                .any(|event| event.what == 6 && event.message == prev_window),
            "CloseDialog must queue updateEvt for the newly exposed front window"
        );
    }

    #[test]
    fn close_dialog_restores_saved_background_pixels() {
        let (mut disp, mut cpu, mut bus) = setup();
        let screen_base = 0x300000u32;
        let row_bytes: u32 = 640;
        disp.set_screen_mode_for_test(screen_base, row_bytes, 640, 480, 8);

        let bounds = (100i16, 100i16, 150i16, 200i16);
        let dialog_ptr = 0x200000u32;
        disp.front_window = dialog_ptr;
        disp.window_bounds = bounds;

        for y in 92u32..158 {
            for x in 92u32..208 {
                bus.write_byte(screen_base + y * row_bytes + x, 0xCC);
            }
        }

        disp.dialog_saved_pixels
            .insert(dialog_ptr, vec![0x33; 66 * 116]);

        bus.write_long(TEST_SP, dialog_ptr);
        let result = disp.dispatch_dialog(true, 0x182, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        for y in 92u32..158 {
            for x in 92u32..208 {
                assert_eq!(
                    bus.read_byte(screen_base + y * row_bytes + x),
                    0x33,
                    "CloseDialog should restore saved background at ({x},{y})"
                );
            }
        }
        assert!(
            !disp.dialog_saved_pixels.contains_key(&dialog_ptr),
            "saved pixels must be consumed after CloseDialog"
        );
    }

    #[test]
    fn close_dialog_non_front_dialog_leaves_front_window_and_port_unchanged() {
        // CloseWindow front-promotion only applies when the closed window
        // was frontmost (IM:I I-283); CloseDialog inherits that behavior.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x200000u32;
        let other_front = 0x181000u32;

        disp.front_window = other_front;
        disp.current_port = other_front;
        bus.write_long(crate::memory::globals::addr::THE_PORT, other_front);
        let a5 = cpu.read_reg(Register::A5);
        let global_ptr = bus.read_long(a5);
        bus.write_long(global_ptr, other_front);
        disp.window_stack
            .push((0x170000, (1, 2, 3, 4), 3, "Prev".to_string()));

        bus.write_long(TEST_SP, dialog_ptr);
        let result = disp.dispatch_dialog(true, 0x182, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        assert_eq!(disp.front_window, other_front);
        assert_eq!(disp.current_port, other_front);
        assert_eq!(
            bus.read_long(crate::memory::globals::addr::THE_PORT),
            other_front
        );
        assert_eq!(bus.read_long(global_ptr), other_front);
        assert_eq!(
            disp.window_stack.len(),
            1,
            "non-front CloseDialog should not consume the saved front-window stack entry"
        );
    }

    #[test]
    fn close_dialog_removes_window_but_preserves_caller_storage() {
        // MTE 1992 pp. 6-119..6-120: CloseDialog releases dialog-owned
        // items but does not dispose caller-supplied DialogRecord or item-list
        // storage.
        let (mut disp, mut cpu, mut bus) = setup();
        install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
        let storage_ptr = bus.alloc(170);
        let ditl = build_test_ditl_item(8, (10, 12, 24, 140), b"Static");
        let (items_handle, items_ptr) = install_ditl_handle_for_test(&mut bus, &ditl);

        let (_sp, dialog_ptr) = call_new_dialog_for_test(
            &mut disp,
            &mut cpu,
            &mut bus,
            storage_ptr,
            (80, 90, 150, 260),
            false,
            4,
            0xFFFF_FFFF,
            items_handle,
        );

        let text_handle = ditl_item_handle_field(&bus, items_ptr, 1);
        let text_ptr = bus.read_long(text_handle);
        let te_handle = bus.read_long(dialog_ptr + 160);
        let te_ptr = bus.read_long(te_handle);
        assert_ne!(text_handle, 0);
        assert_ne!(text_ptr, 0);
        assert_ne!(te_handle, 0);
        assert_ne!(te_ptr, 0);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, dialog_ptr);
        disp.dispatch_dialog(true, 0x182, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert!(!disp.window_list.contains(&dialog_ptr));
        assert_eq!(bus.get_alloc_size(storage_ptr), Some(170));
        assert_eq!(bus.get_alloc_size(items_handle), Some(4));
        assert_eq!(bus.get_alloc_size(items_ptr), Some(ditl.len() as u32));
        assert_eq!(bus.get_alloc_size(text_ptr), None);
        assert_eq!(bus.get_alloc_size(text_handle), None);
        assert_eq!(bus.get_alloc_size(te_ptr), None);
        assert_eq!(bus.get_alloc_size(te_handle), None);
        assert!(!disp.dialog_items.contains_key(&dialog_ptr));
        assert!(!disp.dialog_item_handles.contains_key(&text_handle));
    }

    // ---- DisposDialog ($A983) ----

    #[test]
    fn dispos_dialog_pops_4_bytes() {
        let (mut disp, mut cpu, mut bus) = setup();
        // SP+0: dialog ptr (4 bytes)
        bus.write_long(TEST_SP, 0x200000);

        let result = disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn dispos_dialog_restores_previous_port_state() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x200000u32;
        let prev_window = 0x181000u32;
        seed_window_regions(&mut bus, prev_window, (0, 0, 342, 512));

        disp.front_window = dialog_ptr;
        disp.current_port = dialog_ptr;
        disp.window_bounds = (100, 120, 220, 320);
        bus.write_long(crate::memory::globals::addr::THE_PORT, dialog_ptr);
        let a5 = cpu.read_reg(Register::A5);
        let global_ptr = bus.read_long(a5);
        bus.write_long(global_ptr, dialog_ptr);
        disp.window_stack
            .push((prev_window, (0, 0, 342, 512), 2, "Prev".to_string()));

        bus.write_long(TEST_SP, dialog_ptr);

        let result = disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(disp.front_window, prev_window);
        assert_eq!(disp.current_port, prev_window);
        assert_eq!(
            bus.read_long(crate::memory::globals::addr::THE_PORT),
            prev_window
        );
        assert_eq!(bus.read_long(global_ptr), prev_window);
        assert_eq!(
            TrapDispatcher::region_handle_rect(&bus, bus.read_long(prev_window + 122)),
            Some((100, 120, 220, 320)),
            "DisposDialog should invalidate the dialog-exposed area on the promoted window"
        );
        assert!(
            disp.event_queue.iter().any(|event| {
                event.what == 8 && event.message == prev_window && (event.modifiers & 1) != 0
            }),
            "DisposDialog should queue activateEvt for the promoted front window"
        );
        assert!(
            disp.event_queue
                .iter()
                .any(|event| event.what == 6 && event.message == prev_window),
            "DisposDialog must queue updateEvt for the newly exposed front window"
        );
    }

    #[test]
    fn dispos_dialog_removes_dialog_from_window_list() {
        // IM:I I-425: DisposDialog closes/disposes the dialog and
        // IM:I I-283 CloseWindow semantics remove it from window list.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x200000u32;
        let prev_window = 0x181000u32;

        disp.window_list = vec![dialog_ptr, prev_window];
        disp.front_window = dialog_ptr;
        disp.current_port = dialog_ptr;
        bus.write_byte(dialog_ptr + 110, 0xFF);
        bus.write_byte(prev_window + 110, 0xFF);
        disp.window_stack
            .push((prev_window, (0, 0, 342, 512), 2, "Prev".to_string()));

        bus.write_long(TEST_SP, dialog_ptr);
        let result = disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(disp.window_list, vec![prev_window]);
        assert_eq!(disp.front_window, prev_window);
    }

    #[test]
    fn dispos_dialog_with_user_item_proc_argument_does_not_close_dialog() {
        // SetDItem stores a ProcPtr for userItem entries. A ProcPtr is not a
        // DialogPtr, so DisposDialog must not translate it into the current
        // front dialog. Doing so suppresses legitimate visible prompts whose
        // app code is installing or calling userItem procedures.
        // Inside Macintosh Volume I, I-421, I-425.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x200000u32;
        let prev_window = 0x181000u32;
        let user_item_proc = 0x00016178u32;

        seed_window_regions(&mut bus, prev_window, (0, 0, 342, 512));
        disp.window_list = vec![dialog_ptr, prev_window];
        disp.front_window = dialog_ptr;
        disp.current_port = dialog_ptr;
        disp.window_bounds = (110, 155, 380, 645);
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 0x80,
                proc_ptr: user_item_proc,
                ..DialogItem::default()
            }],
        );
        disp.dialog_saved_pixels
            .insert(dialog_ptr, vec![0x33; 66 * 116]);
        disp.window_stack
            .push((prev_window, (0, 0, 342, 512), 2, "Prev".to_string()));

        bus.write_long(TEST_SP, user_item_proc);
        let result = disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(disp.window_list, vec![dialog_ptr, prev_window]);
        assert_eq!(disp.front_window, dialog_ptr);
        assert!(disp.dialog_items.contains_key(&dialog_ptr));
        assert!(disp.dialog_saved_pixels.contains_key(&dialog_ptr));
    }

    #[test]
    fn dispose_dialog_disposes_allocated_storage_and_items() {
        // MTE 1992 p. 6-120: DisposeDialog calls CloseDialog, then releases
        // the copied item list and DialogRecord allocated for a NIL dStorage.
        let (mut disp, mut cpu, mut bus) = setup();
        install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
        let ditl = build_test_ditl_item(8, (10, 12, 24, 140), b"Static");
        let (items_handle, items_ptr) = install_ditl_handle_for_test(&mut bus, &ditl);

        let (_sp, dialog_ptr) = call_new_dialog_for_test(
            &mut disp,
            &mut cpu,
            &mut bus,
            0,
            (80, 90, 150, 260),
            false,
            4,
            0xFFFF_FFFF,
            items_handle,
        );

        let text_handle = ditl_item_handle_field(&bus, items_ptr, 1);
        let text_ptr = bus.read_long(text_handle);
        let te_handle = bus.read_long(dialog_ptr + 160);
        let te_ptr = bus.read_long(te_handle);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, dialog_ptr);
        disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.get_alloc_size(text_ptr), None);
        assert_eq!(bus.get_alloc_size(text_handle), None);
        assert_eq!(bus.get_alloc_size(te_ptr), None);
        assert_eq!(bus.get_alloc_size(te_handle), None);
        assert_eq!(bus.get_alloc_size(items_ptr), None);
        assert_eq!(bus.get_alloc_size(items_handle), None);
        assert_eq!(bus.get_alloc_size(dialog_ptr), None);
        assert!(!disp.dialog_items.contains_key(&dialog_ptr));
    }

    #[test]
    fn dispose_dialog_releases_text_handles() {
        // IM:I I-413 and I-425: CloseDialog releases standard dialog items;
        // DisposeDialog inherits that item cleanup before freeing the record.
        let (mut disp, mut cpu, mut bus) = setup();
        install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
        let ditl = build_test_ditl_item(16, (10, 12, 24, 140), b"Edit");
        let (items_handle, items_ptr) = install_ditl_handle_for_test(&mut bus, &ditl);

        let (_sp, dialog_ptr) = call_new_dialog_for_test(
            &mut disp,
            &mut cpu,
            &mut bus,
            0,
            (80, 90, 150, 260),
            false,
            4,
            0xFFFF_FFFF,
            items_handle,
        );
        let item_text_handle = ditl_item_handle_field(&bus, items_ptr, 1);
        let item_text_ptr = bus.read_long(item_text_handle);
        let dialog_te_handle = bus.read_long(dialog_ptr + 160);
        let dialog_te_ptr = bus.read_long(dialog_te_handle);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, dialog_ptr);
        disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        assert_eq!(bus.get_alloc_size(item_text_ptr), None);
        assert_eq!(bus.get_alloc_size(item_text_handle), None);
        assert_eq!(bus.get_alloc_size(dialog_te_ptr), None);
        assert_eq!(bus.get_alloc_size(dialog_te_handle), None);
        assert!(!disp.dialog_item_handles.contains_key(&item_text_handle));
    }

    #[test]
    fn dispose_dialog_releases_control_mappings() {
        // IM:I I-413 and I-425: dialog-owned controls are released with the
        // dialog, including dispatcher-side control mappings.
        let (mut disp, mut cpu, mut bus) = setup();
        install_new_dialog_test_screen(&mut disp, &mut bus, 0x77);
        let ditl = build_test_ditl_item(4, (36, 40, 58, 120), b"OK");
        let (items_handle, _items_ptr) = install_ditl_handle_for_test(&mut bus, &ditl);

        let (_sp, dialog_ptr) = call_new_dialog_for_test(
            &mut disp,
            &mut cpu,
            &mut bus,
            0,
            (80, 90, 150, 260),
            false,
            4,
            0xFFFF_FFFF,
            items_handle,
        );
        let box_ptr = bus.alloc(8);
        let item_ptr = bus.alloc(4);
        let type_ptr = bus.alloc(2);
        let snapshot = get_ditem_snapshot_for_test(
            &mut disp, &mut cpu, &mut bus, dialog_ptr, 1, box_ptr, item_ptr, type_ptr,
        );
        let control_handle = snapshot.item;
        let control_ptr = bus.read_long(control_handle);
        assert_ne!(control_handle, 0);
        assert_ne!(control_ptr, 0);
        assert_eq!(
            disp.dialog_control_handles.get(&control_handle),
            Some(&(dialog_ptr, 1))
        );
        assert!(disp.control_proc_ids.contains_key(&control_ptr));
        let aux_handle = disp.ensure_control_aux_record(&mut bus, control_handle);
        let aux_ptr = bus.read_long(aux_handle);
        assert_ne!(aux_handle, 0);
        assert_ne!(aux_ptr, 0);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, dialog_ptr);
        disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        assert_eq!(bus.get_alloc_size(control_ptr), None);
        assert_eq!(bus.get_alloc_size(control_handle), None);
        assert_eq!(bus.get_alloc_size(aux_ptr), None);
        assert_eq!(bus.get_alloc_size(aux_handle), None);
        assert!(disp.control_aux_state(control_handle).is_none());
        assert!(!disp.dialog_control_handles.contains_key(&control_handle));
        assert!(!disp.control_proc_ids.contains_key(&control_ptr));
    }

    #[test]
    fn dispose_dialog_clears_visible_snapshot() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        disp.window_list = vec![dialog_ptr];
        disp.dialog_visible_snapshots.insert(
            dialog_ptr,
            PersistentDialogSnapshot {
                bounds: (10, 20, 30, 40),
                pixels: vec![0x55; 400],
            },
        );

        bus.write_long(TEST_SP, dialog_ptr);
        disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        assert!(!disp.dialog_visible_snapshots.contains_key(&dialog_ptr));
    }

    #[test]
    fn dispose_dialog_clears_retained_click_state() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        disp.window_list = vec![dialog_ptr];
        disp.retained_modal_dialog_click = Some(RetainedModalDialogClickState {
            dialog_ptr,
            item_no: 1,
            rect: (10, 20, 30, 40),
            title: "OK".to_string(),
            is_default: true,
            highlighted: true,
            delivered_to_app: false,
        });

        bus.write_long(TEST_SP, dialog_ptr);
        disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        assert!(disp.retained_modal_dialog_click.is_none());
    }

    #[test]
    fn dispose_dialog_with_active_tracking_cancels_tracking() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x200000u32;
        disp.dialog_tracking = Some(DialogTrackingState {
            dialog_ptr,
            ..Default::default()
        });

        bus.write_long(TEST_SP, dialog_ptr);
        disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        assert!(disp.dialog_tracking.is_none());
    }

    #[test]
    fn retained_modal_dialog_consumes_outside_mouse_down() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let prev_window = bus.alloc(170);
        seed_retained_modal_dialog(&mut disp, &mut bus, dialog_ptr, prev_window, 1);

        disp.push_mouse_down(140, 260);
        let (what, _message, _where_v, _where_h, _modifiers, has_event) =
            disp.dequeue_toolbox_event(&mut cpu, &mut bus, 0xFFFF);

        assert!(!has_event);
        assert_eq!(what, 0);
        assert!(disp.event_queue.is_empty());
        assert_eq!(disp.front_window, dialog_ptr);
        assert!(
            disp.retained_modal_dialog_click.is_some(),
            "outside click must be captured until mouseUp so it cannot leak behind the dialog"
        );

        disp.push_mouse_up(140, 260);
        let (_what, _message, _where_v, _where_h, _modifiers, has_event) =
            disp.dequeue_toolbox_event(&mut cpu, &mut bus, 0xFFFF);

        assert!(!has_event);
        assert_eq!(disp.front_window, dialog_ptr);
        assert!(disp.retained_modal_dialog_click.is_none());
        assert!(
            disp.dialog_items.contains_key(&dialog_ptr),
            "outside clicks do not dismiss modal dialogs"
        );
    }

    #[test]
    fn app_owned_modal_dialog_mouse_down_is_delivered_to_event_loop() {
        // A DLOG can be created and shown with GetNewDialog while the
        // application owns the WaitNextEvent/DialogSelect loop. The Dialog
        // Manager reports enabled item clicks to that application code; it
        // does not dismiss the dialog from the Event Manager dequeue path.
        // Macintosh Toolbox Essentials 1992, pp. 6-138..6-141.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let prev_window = bus.alloc(170);
        let screen_base = 0x300000u32;
        let row_bytes = 320u32;
        disp.set_screen_mode_for_test(screen_base, row_bytes, 320, 200, 8);
        let probe = screen_base + 70 * row_bytes + 150;
        bus.write_byte(probe, 17);

        seed_app_owned_modal_dialog(&mut disp, &mut bus, dialog_ptr, prev_window, 1);

        disp.push_mouse_down(70, 150);
        let (what, _message, where_v, where_h, _modifiers, has_event) =
            disp.dequeue_toolbox_event(&mut cpu, &mut bus, 0xFFFF);

        assert!(has_event);
        assert_eq!(what, 1);
        assert_eq!((where_v, where_h), (70, 150));
        assert_eq!(
            disp.retained_modal_dialog_click
                .as_ref()
                .map(|click| (click.item_no, click.delivered_to_app)),
            Some((1, true))
        );
        assert_eq!(
            bus.read_byte(probe),
            238,
            "app-owned modal button should still show the pressed state"
        );
        assert_eq!(disp.front_window, dialog_ptr);
        assert!(
            disp.dialog_items.contains_key(&dialog_ptr),
            "application-owned dialog should not be closed by event dequeue"
        );
    }

    #[test]
    fn app_owned_modal_dialog_button_mouse_up_dismisses_after_delivery() {
        // Some apps drive modal DLOGs with their own WaitNextEvent loop and
        // only ask the Window Manager which window was clicked. Keep the
        // mouseDown deliverable, but finish the standard modal button press
        // on mouseUp if app code has not called DialogSelect.
        // Macintosh Toolbox Essentials 1992, pp. 6-136, 6-138..6-141.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let prev_window = bus.alloc(170);
        let screen_base = 0x300000u32;
        let row_bytes = 320u32;
        disp.set_screen_mode_for_test(screen_base, row_bytes, 320, 200, 8);
        let probe = screen_base + 70 * row_bytes + 150;
        bus.write_byte(probe, 17);

        seed_app_owned_modal_dialog(&mut disp, &mut bus, dialog_ptr, prev_window, 1);

        disp.push_mouse_down(70, 150);
        let (what, _message, where_v, where_h, _modifiers, has_event) =
            disp.dequeue_toolbox_event(&mut cpu, &mut bus, 0xFFFF);
        assert!(has_event);
        assert_eq!(what, 1);
        assert_eq!((where_v, where_h), (70, 150));
        assert_eq!(disp.front_window, dialog_ptr);

        disp.push_mouse_up(70, 150);
        let (what, _message, where_v, where_h, _modifiers, has_event) =
            disp.dequeue_toolbox_event(&mut cpu, &mut bus, 0xFFFF);

        assert!(
            has_event,
            "app-owned modal mouseUp remains deliverable after compatibility close"
        );
        assert_eq!(what, 2);
        assert_eq!((where_v, where_h), (70, 150));
        assert_eq!(bus.read_byte(probe), 17);
        assert_eq!(disp.front_window, prev_window);
        assert!(!disp.dialog_items.contains_key(&dialog_ptr));
        assert!(disp.retained_modal_dialog_click.is_none());
        assert!(
            disp.event_queue
                .iter()
                .any(|event| event.what == 6 && event.message == prev_window),
            "closing the dialog must invalidate/update the exposed window"
        );
    }

    #[test]
    fn retained_modal_dialog_button_click_highlights_then_dismisses() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let prev_window = bus.alloc(170);
        let screen_base = 0x300000u32;
        let row_bytes = 320u32;
        disp.set_screen_mode_for_test(screen_base, row_bytes, 320, 200, 8);
        seed_retained_modal_dialog(&mut disp, &mut bus, dialog_ptr, prev_window, 1);

        let probe = screen_base + 70 * row_bytes + 150;
        bus.write_byte(probe, 17);
        let bounds = TrapDispatcher::dialog_screen_bounds(&bus, dialog_ptr);
        assert_eq!(bounds, (0, 0, 100, 220));
        let base_pixels = disp.save_dialog_pixels(&bus, bounds);
        disp.dialog_visible_snapshots.insert(
            dialog_ptr,
            PersistentDialogSnapshot {
                bounds,
                pixels: base_pixels,
            },
        );
        let items = disp.dialog_items.get(&dialog_ptr).cloned().unwrap();
        assert_eq!(
            disp.dialog_item_hit_test(
                &bus,
                &items,
                bounds,
                70,
                150,
                &disp.dialog_popup_original_rects,
                dialog_ptr,
            ),
            1
        );

        disp.push_mouse_down(70, 150);
        let (_what, _message, _where_v, _where_h, _modifiers, has_event) =
            disp.dequeue_toolbox_event(&mut cpu, &mut bus, 0xFFFF);

        assert!(!has_event);
        assert_eq!(
            disp.retained_modal_dialog_click
                .as_ref()
                .map(|click| click.item_no),
            Some(1)
        );
        assert_eq!(
            bus.read_byte(probe),
            238,
            "button interior should invert on mouseDown"
        );
        disp.restore_visible_dialog_snapshots(&mut bus);
        disp.redraw_retained_modal_dialog_click(&mut bus);
        assert_eq!(
            bus.read_byte(probe),
            238,
            "pressed retained-modal button must survive visible-dialog snapshot redraw"
        );
        assert_eq!(disp.front_window, dialog_ptr);

        disp.push_mouse_up(70, 150);
        let (_what, _message, _where_v, _where_h, _modifiers, has_event) =
            disp.dequeue_toolbox_event(&mut cpu, &mut bus, 0xFFFF);

        assert!(!has_event);
        assert_eq!(
            bus.read_byte(probe),
            17,
            "button should be unhighlighted before close"
        );
        assert_eq!(disp.front_window, prev_window);
        assert!(!disp.dialog_items.contains_key(&dialog_ptr));
        assert!(disp.retained_modal_dialog_click.is_none());
        assert!(
            !disp
                .event_queue
                .iter()
                .any(|event| matches!(event.what, 1 | 2)),
            "consumed dialog click must not leave mouse events for the game loop"
        );
        assert!(
            disp.event_queue
                .iter()
                .any(|event| event.what == 6 && event.message == prev_window),
            "closing the dialog must invalidate/update the exposed window"
        );
    }

    #[test]
    fn modal_dialog_button_tracking_systemless_theme_routes_pressed_state_through_provider() {
        let (mut disp, _cpu, mut bus) = setup();
        let screen_base = 0x300000u32;
        let row_bytes = 64u32;
        let bounds = (0, 0, 100, 220);
        let item_rect = (60, 120, 80, 180);
        let screen_rect = TrapDispatcher::dialog_item_screen_rect(bounds, item_rect);

        disp.set_ui_theme_id(UiThemeId::SystemlessDefault);
        disp.set_screen_mode_for_test(screen_base, row_bytes, 512, 342, 1);
        disp.draw_button(
            &mut bus,
            screen_rect.0,
            screen_rect.1,
            screen_rect.2,
            screen_rect.3,
            "",
            true,
        );

        let probe_x = 150;
        let probe_y = 70;
        assert!(
            !screen_pixel_is_set(&bus, screen_base, row_bytes, probe_x, probe_y),
            "unpressed provider button interior should start clear"
        );

        let dialog_ptr = bus.alloc(170);
        disp.dialog_tracking = Some(DialogTrackingState {
            dialog_ptr,
            bounds,
            items: vec![DialogItem {
                item_type: 4,
                rect: item_rect,
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
            default_item: 1,
            active_button: Some(DialogButtonTrackingState {
                item_no: 1,
                rect: item_rect,
                title: String::new(),
                is_default: true,
                highlighted: false,
            }),
            ..Default::default()
        });

        disp.mouse_button = true;
        disp.mouse_pos = (probe_y, probe_x);
        disp.handle_dialog_button_tracking(&mut bus);

        assert!(
            screen_pixel_is_set(&bus, screen_base, row_bytes, probe_x, probe_y),
            "inside tracking should paint the provider pressed fill"
        );
        assert_eq!(
            disp.dialog_tracking
                .as_ref()
                .and_then(|tracking| tracking.active_button.as_ref())
                .map(|button| button.highlighted),
            Some(true)
        );

        disp.mouse_pos = (40, 50);
        disp.handle_dialog_button_tracking(&mut bus);

        assert!(
            !screen_pixel_is_set(&bus, screen_base, row_bytes, probe_x, probe_y),
            "outside tracking should redraw unpressed provider chrome"
        );
        assert_eq!(
            disp.dialog_tracking
                .as_ref()
                .and_then(|tracking| tracking.active_button.as_ref())
                .map(|button| button.highlighted),
            Some(false)
        );
    }

    #[test]
    fn retained_modal_dialog_capture_does_not_apply_to_modeless_dialogs() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let prev_window = bus.alloc(170);
        seed_retained_modal_dialog(&mut disp, &mut bus, dialog_ptr, prev_window, 4);

        disp.push_mouse_down(70, 150);
        let (what, _message, where_v, where_h, _modifiers, has_event) =
            disp.dequeue_toolbox_event(&mut cpu, &mut bus, 0xFFFF);

        assert!(has_event);
        assert_eq!(what, 1);
        assert_eq!((where_v, where_h), (70, 150));
        assert!(disp.retained_modal_dialog_click.is_none());
        assert_eq!(disp.front_window, dialog_ptr);
    }

    #[test]
    fn dispos_dialog_clears_tracking_for_disposed_dialog() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x200000u32;
        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
            dialog_ptr,
            bounds: (0, 0, 32, 32),
            title: String::new(),
            proc_id: 1,
            items: Vec::new(),
            default_item: 0,
            cancel_item: 0,
            edit_text: String::new(),
            edit_item: 0,
            saved_pixels: Vec::new(),
            stack_ptr: 0,
            item_hit_ptr: 0,
            rendered_pixels: Vec::new(),
            flash_remaining: 0,
            flash_delay: 0,
            flash_item: 0,
            edit_text_modified: false,
            draw_proc_queue: VecDeque::new(),
            draw_procs_done: true,
            rendered_pixels_final: true,
            filter_proc: 0,
            game_managed: false,
            last_filter_event: None,
            popup_draws: Vec::new(),
            active_popup: None,
            active_button: None,
            active_user_item: None,
        });

        bus.write_long(TEST_SP, dialog_ptr);
        let result = disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert!(disp.dialog_tracking.is_none());
    }

    #[test]
    fn dispos_dialog_clears_dialog_scoped_side_maps_for_disposed_dialog() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x200000u32;
        let other_dialog_ptr = 0x210000u32;
        let text_handle = 0x300000u32;
        let other_text_handle = 0x300010u32;
        let ctrl_handle = 0x300020u32;
        let other_ctrl_handle = 0x300030u32;

        disp.dialog_items
            .insert(dialog_ptr, vec![DialogItem::default()]);
        disp.dialog_items
            .insert(other_dialog_ptr, vec![DialogItem::default()]);
        disp.dialog_item_handles
            .insert(text_handle, (dialog_ptr, 0));
        disp.dialog_item_handles
            .insert(other_text_handle, (other_dialog_ptr, 0));
        disp.dialog_control_handles
            .insert(ctrl_handle, (dialog_ptr, 1));
        disp.dialog_control_handles
            .insert(other_ctrl_handle, (other_dialog_ptr, 1));
        disp.dialog_control_values.insert((dialog_ptr, 1), 1);
        disp.dialog_control_values.insert((other_dialog_ptr, 1), 1);
        disp.hidden_dialog_item_rects
            .insert((dialog_ptr, 1), (10, 20, 30, 40));
        disp.hidden_dialog_item_rects
            .insert((other_dialog_ptr, 1), (50, 60, 70, 80));
        disp.dialog_item_popup_menus.insert((dialog_ptr, 1), 900);
        disp.dialog_item_popup_menus
            .insert((other_dialog_ptr, 1), 901);
        disp.dialog_popup_original_rects
            .insert((dialog_ptr, 1), (10, 20, 30, 130));
        disp.dialog_popup_original_rects
            .insert((other_dialog_ptr, 1), (50, 60, 70, 180));
        disp.dialog_popup_candidate_items.insert((dialog_ptr, 1));
        disp.dialog_popup_candidate_items
            .insert((other_dialog_ptr, 1));
        disp.dialog_cancel_items.insert(dialog_ptr, 2);
        disp.dialog_cancel_items.insert(other_dialog_ptr, 3);
        disp.pending_dialog_popup_menu = Some(PendingDialogPopupMenu {
            dialog_ptr,
            item_no: 1,
            menu_id: 900,
            rect: (10, 20, 30, 130),
        });

        bus.write_long(TEST_SP, dialog_ptr);
        disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        assert!(!disp.dialog_items.contains_key(&dialog_ptr));
        assert!(disp.dialog_items.contains_key(&other_dialog_ptr));
        assert!(!disp.dialog_item_handles.contains_key(&text_handle));
        assert!(disp.dialog_item_handles.contains_key(&other_text_handle));
        assert!(!disp.dialog_control_handles.contains_key(&ctrl_handle));
        assert!(disp.dialog_control_handles.contains_key(&other_ctrl_handle));
        assert!(!disp.dialog_control_values.contains_key(&(dialog_ptr, 1)));
        assert!(disp
            .dialog_control_values
            .contains_key(&(other_dialog_ptr, 1)));
        assert!(!disp.hidden_dialog_item_rects.contains_key(&(dialog_ptr, 1)));
        assert!(disp
            .hidden_dialog_item_rects
            .contains_key(&(other_dialog_ptr, 1)));
        assert!(!disp.dialog_item_popup_menus.contains_key(&(dialog_ptr, 1)));
        assert!(disp
            .dialog_item_popup_menus
            .contains_key(&(other_dialog_ptr, 1)));
        assert!(!disp
            .dialog_popup_original_rects
            .contains_key(&(dialog_ptr, 1)));
        assert!(disp
            .dialog_popup_original_rects
            .contains_key(&(other_dialog_ptr, 1)));
        assert!(!disp.dialog_popup_candidate_items.contains(&(dialog_ptr, 1)));
        assert!(disp
            .dialog_popup_candidate_items
            .contains(&(other_dialog_ptr, 1)));
        assert!(!disp.dialog_cancel_items.contains_key(&dialog_ptr));
        assert!(disp.dialog_cancel_items.contains_key(&other_dialog_ptr));
        assert!(disp.pending_dialog_popup_menu.is_none());
    }

    // Regression: games that run their own event loop (e.g. Escape
    // Velocity's "enter pilot/ship name" text dialogs) call
    // GetNewDialog → custom event loop → DisposDialog without ever
    // invoking ModalDialog. Before the fix, DisposDialog discarded
    // the saved-background pixels without blitting them back to the
    // screen, leaving a dialog-shaped hole over the window behind.
    // IM:I I-425 says DisposDialog internally calls CloseWindow,
    // whose PaintBehind/CalcVisBehind is supposed to restore the
    // underlying content. These three tests pin that contract.
    //
    // Save/restore geometry note: save_dialog_pixels adds the dBoxProc
    // structure margin around the bounds. The tests use bounds
    // (100,100,150,200) → save area (92,92)..(158,208) =
    // 66 rows × 116 cols = 7656 bytes.

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

        // 8bpp test screen. Must be inside the 4MB test bus.
        let screen_base = 0x300000u32;
        let row_bytes: u32 = 640;
        disp.set_screen_mode_for_test(screen_base, row_bytes, 640, 480, 8);

        let bounds = (100i16, 100i16, 150i16, 200i16);
        let dialog_ptr = 0x200000u32;
        disp.front_window = dialog_ptr;
        disp.window_bounds = bounds;

        // Paint the save area with 0xCC — the "dialog pixels" that
        // should be overwritten on dispose.
        for y in 92u32..158 {
            for x in 92u32..208 {
                bus.write_byte(screen_base + y * row_bytes + x, 0xCC);
            }
        }

        // Install a saved background snapshot filled with 0x33 (the
        // "what was behind the dialog" pattern). 66*116=7656 bytes.
        disp.dialog_saved_pixels
            .insert(dialog_ptr, vec![0x33; 66 * 116]);

        bus.write_long(TEST_SP, dialog_ptr);
        let result = disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        // Every byte in the save area should now be 0x33, not 0xCC.
        for y in 92u32..158 {
            for x in 92u32..208 {
                let addr = screen_base + y * row_bytes + x;
                let got = bus.read_byte(addr);
                assert_eq!(
                    got, 0x33,
                    "byte at ({},{}) must be restored background 0x33, got 0x{:02X}",
                    x, y, got
                );
            }
        }
        assert!(
            !disp.dialog_saved_pixels.contains_key(&dialog_ptr),
            "saved pixels must be consumed after DisposDialog"
        );
    }

    #[test]
    fn disposdialog_restore_is_bounded_by_save_margin() {
        // Pin that the restore writes exactly the dBoxProc structure-margin
        // rectangle and does not stomp adjacent bytes. This catches
        // off-by-one errors in the save/restore geometry.
        let (mut disp, mut cpu, mut bus) = setup();
        let screen_base = 0x300000u32;
        let row_bytes: u32 = 640;
        disp.set_screen_mode_for_test(screen_base, row_bytes, 640, 480, 8);

        let bounds = (100i16, 100i16, 150i16, 200i16);
        let dialog_ptr = 0x200000u32;
        disp.front_window = dialog_ptr;
        disp.window_bounds = bounds;

        // Paint the entire screen area of interest (including a
        // generous border around the save rect) with 0xAA.
        for y in 85u32..165 {
            for x in 85u32..215 {
                bus.write_byte(screen_base + y * row_bytes + x, 0xAA);
            }
        }

        disp.dialog_saved_pixels
            .insert(dialog_ptr, vec![0x33; 66 * 116]);

        bus.write_long(TEST_SP, dialog_ptr);
        disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        // Bytes OUTSIDE the save area must still be 0xAA.
        // Above the save rect:
        for x in 85u32..215 {
            assert_eq!(bus.read_byte(screen_base + 91 * row_bytes + x), 0xAA);
        }
        // Below the save rect:
        for x in 85u32..215 {
            assert_eq!(bus.read_byte(screen_base + 158 * row_bytes + x), 0xAA);
        }
        // Left of the save rect:
        for y in 85u32..165 {
            assert_eq!(bus.read_byte(screen_base + y * row_bytes + 91), 0xAA);
        }
        // Right of the save rect:
        for y in 85u32..165 {
            assert_eq!(bus.read_byte(screen_base + y * row_bytes + 208), 0xAA);
        }
        // Bytes INSIDE the save area must now be 0x33.
        for y in 92u32..158 {
            for x in 92u32..208 {
                assert_eq!(bus.read_byte(screen_base + y * row_bytes + x), 0x33);
            }
        }
    }

    #[test]
    fn disposdialog_without_saved_pixels_leaves_screen_untouched() {
        // If no saved pixels exist for this dialog (e.g. ModalDialog
        // already consumed them on flash completion), DisposDialog
        // must be a no-op on the screen — no panic, no accidental
        // fill.
        let (mut disp, mut cpu, mut bus) = setup();
        let screen_base = 0x300000u32;
        let row_bytes: u32 = 640;
        disp.set_screen_mode_for_test(screen_base, row_bytes, 640, 480, 8);

        let bounds = (100i16, 100i16, 150i16, 200i16);
        let dialog_ptr = 0x200000u32;
        disp.front_window = dialog_ptr;
        disp.window_bounds = bounds;

        for y in 95u32..155 {
            for x in 95u32..205 {
                bus.write_byte(screen_base + y * row_bytes + x, 0x77);
            }
        }
        assert!(!disp.dialog_saved_pixels.contains_key(&dialog_ptr));

        bus.write_long(TEST_SP, dialog_ptr);
        disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        for y in 95u32..155 {
            for x in 95u32..205 {
                let addr = screen_base + y * row_bytes + x;
                assert_eq!(
                    bus.read_byte(addr),
                    0x77,
                    "DisposDialog without saved pixels must not write to screen"
                );
            }
        }
    }

    #[test]
    fn disposdialog_non_front_does_not_restore() {
        // If the dialog being disposed is NOT the front window, we
        // don't know its correct bounds (self.window_bounds belongs
        // to whatever is currently front). Restoring at the wrong
        // coords would corrupt the screen over the actual front
        // window. Expected behavior: discard saved pixels silently.
        let (mut disp, mut cpu, mut bus) = setup();
        let screen_base = 0x300000u32;
        let row_bytes: u32 = 640;
        disp.set_screen_mode_for_test(screen_base, row_bytes, 640, 480, 8);

        let dialog_ptr = 0x200000u32;
        let other_front = 0x280000u32;
        disp.front_window = other_front; // dialog_ptr is NOT front
        disp.window_bounds = (200, 200, 300, 400); // matches other_front

        for y in 95u32..155 {
            for x in 95u32..205 {
                bus.write_byte(screen_base + y * row_bytes + x, 0x55);
            }
        }
        disp.dialog_saved_pixels
            .insert(dialog_ptr, vec![0x99; 66 * 116]);

        bus.write_long(TEST_SP, dialog_ptr);
        disp.dispatch_dialog(true, 0x183, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        for y in 95u32..155 {
            for x in 95u32..205 {
                let addr = screen_base + y * row_bytes + x;
                assert_eq!(
                    bus.read_byte(addr),
                    0x55,
                    "non-front DisposDialog must not restore over current front window"
                );
            }
        }
        assert!(
            !disp.dialog_saved_pixels.contains_key(&dialog_ptr),
            "saved pixels must still be removed for non-front dispose"
        );
    }

    // ---- ParamText ($A98B) ----

    fn write_pascal_str(bus: &mut crate::memory::MacMemoryBus, addr: u32, s: &[u8]) {
        bus.write_byte(addr, s.len() as u8);
        for (i, b) in s.iter().enumerate() {
            bus.write_byte(addr + 1 + i as u32, *b);
        }
    }

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

        let p0 = 0x300000u32;
        let p1 = 0x300100u32;
        let p2 = 0x300200u32;
        let p3 = 0x300300u32;
        write_pascal_str(&mut bus, p0, b"alpha");
        write_pascal_str(&mut bus, p1, b"bravo");
        write_pascal_str(&mut bus, p2, b"chi");
        write_pascal_str(&mut bus, p3, b"d");

        bus.write_long(TEST_SP, p3);
        bus.write_long(TEST_SP + 4, p2);
        bus.write_long(TEST_SP + 8, p1);
        bus.write_long(TEST_SP + 12, p0);

        disp.dispatch_dialog(true, 0x18B, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 16);
        assert_eq!(disp.param_text[0], b"alpha");
        assert_eq!(disp.param_text[1], b"bravo");
        assert_eq!(disp.param_text[2], b"chi");
        assert_eq!(disp.param_text[3], b"d");
    }

    #[test]
    fn param_text_empty_pascal_string_clears_slot() {
        // Per Inside Macintosh Volume I, I-422, an empty Pascal string
        // (length-byte = 0) IS a valid value — the caret placeholder
        // gets replaced by nothing. This is the explicit "clear this
        // slot" idiom. Distinguishes from NIL (preserves prior).
        let (mut disp, mut cpu, mut bus) = setup();
        disp.param_text[0] = b"stale".to_vec();
        disp.param_text[1] = b"stale".to_vec();
        disp.param_text[2] = b"stale".to_vec();
        disp.param_text[3] = b"stale".to_vec();

        let empty_ptr = 0x300000u32;
        write_pascal_str(&mut bus, empty_ptr, b"");

        for off in [0u32, 4, 8, 12] {
            bus.write_long(TEST_SP + off, empty_ptr);
        }

        disp.dispatch_dialog(true, 0x18B, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        for i in 0..4 {
            assert_eq!(
                disp.param_text[i],
                Vec::<u8>::new(),
                "empty Pascal string must clear slot {}",
                i
            );
        }
    }

    #[test]
    fn apply_param_text_substitutes_caret_placeholders() {
        let (mut disp, _cpu, _bus) = setup();
        disp.param_text[0] = b"MS UserKey".to_vec();
        disp.param_text[1] = b"42".to_vec();

        assert_eq!(
            disp.apply_param_text("Unable to open the \"^0\" file."),
            "Unable to open the \"MS UserKey\" file."
        );
        assert_eq!(disp.apply_param_text("count: ^1"), "count: 42");
        assert_eq!(
            disp.apply_param_text("plain text without placeholders"),
            "plain text without placeholders"
        );
        assert_eq!(disp.apply_param_text("^0 ^1 ^2 ^3"), "MS UserKey 42  ");
        assert_eq!(
            disp.apply_param_text("^A literal caret"),
            "^A literal caret"
        );

        // Edge cases: lone trailing caret, double caret, caret at boundary,
        // out-of-range digit (^9 has no slot 9 → kept literal).
        assert_eq!(disp.apply_param_text("trailing^"), "trailing^");
        assert_eq!(disp.apply_param_text("^^0"), "^MS UserKey");
        assert_eq!(disp.apply_param_text("^9 unknown slot"), "^9 unknown slot");
        assert_eq!(disp.apply_param_text(""), "");
    }

    #[test]
    fn param_text_nil_pointer_preserves_previous_slot_value() {
        // Per Inside Macintosh Volume I, I-422, passing NIL for any
        // ParamText slot must leave the prior value unchanged — apps
        // commonly stage one parameter at a time before opening an
        // alert, expecting the others to retain whatever they were
        // last set to.
        let (mut disp, mut cpu, mut bus) = setup();
        disp.param_text[0] = b"old0".to_vec();
        disp.param_text[1] = b"old1".to_vec();
        disp.param_text[2] = b"old2".to_vec();
        disp.param_text[3] = b"old3".to_vec();

        let new0 = 0x300000u32;
        write_pascal_str(&mut bus, new0, b"new0");

        bus.write_long(TEST_SP, 0); // param3 = NIL
        bus.write_long(TEST_SP + 4, 0); // param2 = NIL
        bus.write_long(TEST_SP + 8, 0); // param1 = NIL
        bus.write_long(TEST_SP + 12, new0);

        disp.dispatch_dialog(true, 0x18B, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        assert_eq!(disp.param_text[0], b"new0", "param0 should be replaced");
        assert_eq!(disp.param_text[1], b"old1", "NIL must preserve param1");
        assert_eq!(disp.param_text[2], b"old2", "NIL must preserve param2");
        assert_eq!(disp.param_text[3], b"old3", "NIL must preserve param3");
    }

    #[test]
    fn apply_param_text_returns_borrowed_when_no_placeholders() {
        // Pin the no-allocation contract: the common case (DITL text
        // without any `^N` placeholders) must return Cow::Borrowed so
        // draw_static_text doesn't allocate per item.
        use std::borrow::Cow;
        let (mut disp, _cpu, _bus) = setup();
        disp.param_text[0] = b"value".to_vec();

        let plain = disp.apply_param_text("hello world");
        assert!(
            matches!(plain, Cow::Borrowed(_)),
            "no-placeholder input must skip the allocation path"
        );

        let substituted = disp.apply_param_text("hello ^0");
        assert!(
            matches!(substituted, Cow::Owned(_)),
            "with-placeholder input takes the allocation path"
        );
        assert_eq!(substituted, "hello value");
    }

    // ---- GetDItem ($A98D) ----

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

        // Set up output pointers
        let box_ptr = 0x300000u32;
        let item_ptr = 0x300100u32;
        let type_ptr = 0x300200u32;

        // Pre-fill output locations with non-zero to verify they get cleared
        bus.write_long(box_ptr, 0xDEADBEEF);
        bus.write_long(box_ptr + 4, 0xDEADBEEF);
        bus.write_long(item_ptr, 0xDEADBEEF);
        bus.write_word(type_ptr, 0xBEEF);

        // Stack layout: SP+0: box(4), SP+4: item(4), SP+8: type(4), SP+12: itemNo(2), SP+14: dialog(4)
        bus.write_long(TEST_SP, box_ptr);
        bus.write_long(TEST_SP + 4, item_ptr);
        bus.write_long(TEST_SP + 8, type_ptr);
        bus.write_word(TEST_SP + 12, 1); // item number
        bus.write_long(TEST_SP + 14, 0x200000); // dialog ptr

        let result = disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 18);

        // Verify outputs were cleared to 0
        assert_eq!(bus.read_word(type_ptr), 0);
        assert_eq!(bus.read_long(item_ptr), 0);
        assert_eq!(bus.read_long(box_ptr), 0);
        assert_eq!(bus.read_long(box_ptr + 4), 0);
    }

    #[test]
    fn get_ditem_text_handle_returns_existing_raw_text_handle() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let box_ptr = 0x300000u32;
        let item_ptr = 0x300100u32;
        let type_ptr = 0x300200u32;
        let text_handle = bus.alloc(4);
        let text_ptr = bus.alloc(5);
        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(22);

        bus.write_bytes(text_ptr, b"Hello");
        bus.write_long(text_handle, text_ptr);
        bus.write_long(items_handle, ditl_ptr);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_word(ditl_ptr, 0);
        bus.write_long(ditl_ptr + 2, text_handle);
        bus.write_word(ditl_ptr + 6, 10);
        bus.write_word(ditl_ptr + 8, 20);
        bus.write_word(ditl_ptr + 10, 30);
        bus.write_word(ditl_ptr + 12, 40);
        bus.write_byte(ditl_ptr + 14, 8);
        bus.write_byte(ditl_ptr + 15, 5);
        bus.write_bytes(ditl_ptr + 16, b"Hello");

        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 8,
                rect: (10, 20, 30, 40),
                text: "Hello".to_string(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        bus.write_long(TEST_SP, box_ptr);
        bus.write_long(TEST_SP + 4, item_ptr);
        bus.write_long(TEST_SP + 8, type_ptr);
        bus.write_word(TEST_SP + 12, 1);
        bus.write_long(TEST_SP + 14, dialog_ptr);

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

        assert_eq!(bus.read_long(item_ptr), text_handle);
        assert_eq!(bus.read_long(ditl_ptr + 2), text_handle);
        assert_eq!(bus.get_alloc_size(text_ptr), Some(5));
        assert_eq!(bus.read_bytes(text_ptr, 5), b"Hello".to_vec());
    }

    #[test]
    fn get_ditem_records_pending_popup_candidate_without_associating() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let box_ptr = bus.alloc(8);
        let item_ptr = bus.alloc(4);
        let type_ptr = bus.alloc(2);

        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 0,
                rect: (10, 20, 30, 40),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );
        disp.last_inserted_menu_id = Some(1234);

        bus.write_long(TEST_SP, box_ptr);
        bus.write_long(TEST_SP + 4, item_ptr);
        bus.write_long(TEST_SP + 8, type_ptr);
        bus.write_word(TEST_SP + 12, 1);
        bus.write_long(TEST_SP + 14, dialog_ptr);

        disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        assert!(!disp.dialog_item_popup_menus.contains_key(&(dialog_ptr, 1)));
        assert!(!disp
            .dialog_popup_original_rects
            .contains_key(&(dialog_ptr, 1)));
        let pending = disp
            .pending_dialog_popup_menu
            .expect("enabled userItem should leave a pending popup candidate");
        assert_eq!(pending.dialog_ptr, dialog_ptr);
        assert_eq!(pending.item_no, 1);
        assert_eq!(pending.menu_id, 1234);
        assert_eq!(pending.rect, (10, 20, 30, 40));
        assert_eq!(disp.last_inserted_menu_id, None);
    }

    #[test]
    fn get_ditem_ignores_recent_inserted_menu_for_disabled_user_item() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let box_ptr = bus.alloc(8);
        let item_ptr = bus.alloc(4);
        let type_ptr = bus.alloc(2);

        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 0x80,
                rect: (10, 20, 30, 40),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );
        disp.last_inserted_menu_id = Some(1234);

        bus.write_long(TEST_SP, box_ptr);
        bus.write_long(TEST_SP + 4, item_ptr);
        bus.write_long(TEST_SP + 8, type_ptr);
        bus.write_word(TEST_SP + 12, 1);
        bus.write_long(TEST_SP + 14, dialog_ptr);

        disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        assert!(!disp.dialog_item_popup_menus.contains_key(&(dialog_ptr, 1)));
        assert!(!disp
            .dialog_popup_original_rects
            .contains_key(&(dialog_ptr, 1)));
        assert!(disp.pending_dialog_popup_menu.is_none());
        assert_eq!(disp.last_inserted_menu_id, None);
    }

    #[test]
    fn set_ditem_confirms_pending_popup_user_item_when_proc_installed() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let get_box_ptr = bus.alloc(8);
        let get_item_ptr = bus.alloc(4);
        let get_type_ptr = bus.alloc(2);
        let set_box_ptr = bus.alloc(8);
        let proc_ptr = 0x00AB_CDEF;

        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 0,
                rect: (10, 20, 30, 40),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );
        disp.last_inserted_menu_id = Some(1234);

        bus.write_long(TEST_SP, get_box_ptr);
        bus.write_long(TEST_SP + 4, get_item_ptr);
        bus.write_long(TEST_SP + 8, get_type_ptr);
        bus.write_word(TEST_SP + 12, 1);
        bus.write_long(TEST_SP + 14, dialog_ptr);
        disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(set_box_ptr, 12);
        bus.write_word(set_box_ptr + 2, 22);
        bus.write_word(set_box_ptr + 4, 32);
        bus.write_word(set_box_ptr + 6, 42);
        bus.write_long(TEST_SP, set_box_ptr);
        bus.write_long(TEST_SP + 4, proc_ptr);
        bus.write_word(TEST_SP + 8, 0);
        bus.write_word(TEST_SP + 10, 1);
        bus.write_long(TEST_SP + 12, dialog_ptr);
        disp.dispatch_dialog(true, 0x18E, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        assert_eq!(
            disp.dialog_item_popup_menus.get(&(dialog_ptr, 1)),
            Some(&1234)
        );
        assert_eq!(
            disp.dialog_popup_original_rects.get(&(dialog_ptr, 1)),
            Some(&(10, 20, 30, 40))
        );
        assert!(disp.pending_dialog_popup_menu.is_none());
    }

    #[test]
    fn set_ditem_narrowing_user_item_records_popup_candidate_without_proc() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let set_box_ptr = bus.alloc(8);

        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 0,
                rect: (10, 20, 30, 150),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        bus.write_word(set_box_ptr, 10);
        bus.write_word(set_box_ptr + 2, 20);
        bus.write_word(set_box_ptr + 4, 30);
        bus.write_word(set_box_ptr + 6, 35);
        bus.write_long(TEST_SP, set_box_ptr);
        bus.write_long(TEST_SP + 4, 0);
        bus.write_word(TEST_SP + 8, 0);
        bus.write_word(TEST_SP + 10, 1);
        bus.write_long(TEST_SP + 12, dialog_ptr);
        disp.dispatch_dialog(true, 0x18E, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        assert!(!disp.dialog_item_popup_menus.contains_key(&(dialog_ptr, 1)));
        assert_eq!(
            disp.dialog_popup_original_rects.get(&(dialog_ptr, 1)),
            Some(&(10, 20, 30, 150))
        );
        assert!(disp.dialog_popup_candidate_items.contains(&(dialog_ptr, 1)));
        assert_eq!(
            disp.dialog_items
                .get(&dialog_ptr)
                .and_then(|items| items.first())
                .map(|item| item.rect),
            Some((10, 20, 30, 35))
        );
    }

    // ---- SetDItem ($A98E) ----

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

        let result = disp.dispatch_dialog(true, 0x18E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 16);
    }

    #[test]
    fn set_ditem_updates_item_fields_visible_through_get_ditem() {
        // Inside Macintosh Volume I, I-421: SetDItem changes itemType, item,
        // and box for the specified item; GetDItem must then report those
        // updated fields.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(22);
        let old_handle = bus.alloc(4);
        let old_ptr = bus.alloc(4);
        let new_handle = bus.alloc(4);
        let new_ptr = bus.alloc(3);
        let set_box_ptr = bus.alloc(8);
        let get_box_ptr = bus.alloc(8);
        let get_item_ptr = bus.alloc(4);
        let get_type_ptr = bus.alloc(2);

        bus.write_bytes(old_ptr, b"Old!");
        bus.write_bytes(new_ptr, b"New");
        bus.write_long(old_handle, old_ptr);
        bus.write_long(new_handle, new_ptr);
        bus.write_long(items_handle, ditl_ptr);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_word(ditl_ptr, 0); // one item
        bus.write_long(ditl_ptr + 2, old_handle);
        bus.write_word(ditl_ptr + 6, 10);
        bus.write_word(ditl_ptr + 8, 20);
        bus.write_word(ditl_ptr + 10, 30);
        bus.write_word(ditl_ptr + 12, 40);
        bus.write_byte(ditl_ptr + 14, 8);
        bus.write_byte(ditl_ptr + 15, 4);
        bus.write_bytes(ditl_ptr + 16, b"Old!");

        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 8,
                rect: (10, 20, 30, 40),
                text: "Old!".to_string(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );
        disp.dialog_item_handles.insert(old_handle, (dialog_ptr, 0));

        bus.write_word(set_box_ptr, 111);
        bus.write_word(set_box_ptr + 2, 222);
        bus.write_word(set_box_ptr + 4, 333);
        bus.write_word(set_box_ptr + 6, 444);
        bus.write_long(TEST_SP, set_box_ptr);
        bus.write_long(TEST_SP + 4, new_handle);
        bus.write_word(TEST_SP + 8, 16);
        bus.write_word(TEST_SP + 10, 1);
        bus.write_long(TEST_SP + 12, dialog_ptr);

        disp.dispatch_dialog(true, 0x18E, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, get_box_ptr);
        bus.write_long(TEST_SP + 4, get_item_ptr);
        bus.write_long(TEST_SP + 8, get_type_ptr);
        bus.write_word(TEST_SP + 12, 1);
        bus.write_long(TEST_SP + 14, dialog_ptr);
        disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        assert_eq!(bus.read_word(get_type_ptr), 16);
        assert_eq!(bus.read_long(get_item_ptr), new_handle);
        assert_eq!(bus.read_word(get_box_ptr), 111);
        assert_eq!(bus.read_word(get_box_ptr + 2), 222);
        assert_eq!(bus.read_word(get_box_ptr + 4), 333);
        assert_eq!(bus.read_word(get_box_ptr + 6), 444);
    }

    #[test]
    fn set_ditem_user_item_treats_item_parameter_as_proc_ptr() {
        // Inside Macintosh Volume I, I-421: for userItem, SetDItem's `item`
        // parameter is a draw procedure pointer (ProcPtr), not a text/control
        // handle. GetDItem should report the same ProcPtr value.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(16);
        let proc_ptr = 0x00AB_CDEFu32;
        let set_box_ptr = bus.alloc(8);
        let get_box_ptr = bus.alloc(8);
        let get_item_ptr = bus.alloc(4);
        let get_type_ptr = bus.alloc(2);

        bus.write_long(items_handle, ditl_ptr);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_word(ditl_ptr, 0); // one item
        bus.write_long(ditl_ptr + 2, 0);
        bus.write_word(ditl_ptr + 6, 10);
        bus.write_word(ditl_ptr + 8, 20);
        bus.write_word(ditl_ptr + 10, 30);
        bus.write_word(ditl_ptr + 12, 40);
        bus.write_byte(ditl_ptr + 14, 0);
        bus.write_byte(ditl_ptr + 15, 0);

        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 0,
                rect: (10, 20, 30, 40),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        bus.write_word(set_box_ptr, 50);
        bus.write_word(set_box_ptr + 2, 60);
        bus.write_word(set_box_ptr + 4, 70);
        bus.write_word(set_box_ptr + 6, 80);
        bus.write_long(TEST_SP, set_box_ptr);
        bus.write_long(TEST_SP + 4, proc_ptr);
        bus.write_word(TEST_SP + 8, 0);
        bus.write_word(TEST_SP + 10, 1);
        bus.write_long(TEST_SP + 12, dialog_ptr);
        disp.dispatch_dialog(true, 0x18E, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, get_box_ptr);
        bus.write_long(TEST_SP + 4, get_item_ptr);
        bus.write_long(TEST_SP + 8, get_type_ptr);
        bus.write_word(TEST_SP + 12, 1);
        bus.write_long(TEST_SP + 14, dialog_ptr);
        disp.dispatch_dialog(true, 0x18D, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        assert_eq!(bus.read_word(get_type_ptr), 0);
        assert_eq!(bus.read_long(get_item_ptr), proc_ptr);
        assert_eq!(bus.read_word(get_box_ptr), 50);
        assert_eq!(bus.read_word(get_box_ptr + 2), 60);
        assert_eq!(bus.read_word(get_box_ptr + 4), 70);
        assert_eq!(bus.read_word(get_box_ptr + 6), 80);
    }

    #[test]
    fn set_dialog_item_text_resizes_existing_text_handle() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x200000u32;
        let text_handle = bus.alloc(4);
        let text_ptr = bus.alloc(2);
        let new_text_ptr = 0x300000u32;

        bus.write_long(text_handle, text_ptr);
        bus.write_byte(text_ptr, 1);
        bus.write_byte(text_ptr + 1, b'A');
        bus.write_byte(new_text_ptr, 5);
        bus.write_bytes(new_text_ptr + 1, b"Hello");

        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 8,
                rect: (0, 0, 10, 10),
                text: "A".to_string(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );
        disp.dialog_item_handles
            .insert(text_handle, (dialog_ptr, 0));

        bus.write_long(TEST_SP, new_text_ptr);
        bus.write_long(TEST_SP + 4, text_handle);

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

        let resized_ptr = bus.read_long(text_handle);
        assert_eq!(bus.get_alloc_size(resized_ptr), Some(5));
        assert_eq!(bus.read_bytes(resized_ptr, 5), b"Hello".to_vec());
    }

    #[test]
    fn set_dialog_item_text_redraws_visible_text_item() {
        let (mut disp, mut cpu, mut bus) = setup();
        let screen_base = bus.alloc((200 * 100) as u32);
        let row_bytes = 200u32;
        disp.set_screen_mode_for_test(screen_base, row_bytes, 200, 100, 8);
        bus.write_long(0x0824, screen_base);
        TrapDispatcher::fb_fill_rect(
            &mut bus,
            screen_base,
            row_bytes,
            8,
            200,
            100,
            0,
            0,
            100,
            200,
            false,
        );
        let white = bus.read_byte(screen_base);

        let bounds = (10, 20, 70, 180);
        let dialog_ptr = bus.alloc(170);
        bus.write_word(dialog_ptr + 6, 0); // GrafPort, not CGrafPort
        bus.write_word(dialog_ptr + 8, (-bounds.0) as u16);
        bus.write_word(dialog_ptr + 10, (-bounds.1) as u16);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, (bounds.2 - bounds.0) as u16);
        bus.write_word(dialog_ptr + 22, (bounds.3 - bounds.1) as u16);
        bus.write_word(dialog_ptr + 108, 2);
        bus.write_byte(dialog_ptr + 110, 0xFF);
        bus.write_word(dialog_ptr + 164, 0xFFFF);
        bus.write_word(dialog_ptr + 168, 1);

        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(16);
        let text_handle = bus.alloc(4);
        let text_ptr = bus.alloc(1);
        bus.write_bytes(text_ptr, b"A");
        bus.write_long(text_handle, text_ptr);
        bus.write_long(items_handle, ditl_ptr);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_word(ditl_ptr, 0);
        bus.write_long(ditl_ptr + 2, text_handle);
        bus.write_word(ditl_ptr + 6, 10);
        bus.write_word(ditl_ptr + 8, 10);
        bus.write_word(ditl_ptr + 10, 26);
        bus.write_word(ditl_ptr + 12, 140);
        bus.write_byte(ditl_ptr + 14, 8);
        bus.write_byte(ditl_ptr + 15, 0);

        let items = vec![DialogItem {
            item_type: 8,
            rect: (10, 10, 26, 140),
            text: "A".to_string(),
            resource_id: 0,
            proc_ptr: 0,
            sel_start: 0,
            sel_end: 0,
        }];
        disp.dialog_items.insert(dialog_ptr, items.clone());
        disp.dialog_item_handles
            .insert(text_handle, (dialog_ptr, 0));
        disp.draw_dialog(&mut bus, bounds, 2, "", &items, 1, "", 0, false, dialog_ptr);

        let count_nonwhite = |bus: &MacMemoryBus| -> usize {
            let mut count = 0;
            for y in 20..36u32 {
                for x in 30..160u32 {
                    if bus.read_byte(screen_base + y * row_bytes + x) != white {
                        count += 1;
                    }
                }
            }
            count
        };
        let before = count_nonwhite(&bus);

        let pstr = bus.alloc(9);
        bus.write_byte(pstr, 8);
        bus.write_bytes(pstr + 1, b"WWWWWWWW");
        bus.write_long(TEST_SP, pstr);
        bus.write_long(TEST_SP + 4, text_handle);

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

        let after = count_nonwhite(&bus);
        assert!(
            after > before + 20,
            "SetDialogItemText must draw the updated text item; before={} after={}",
            before,
            after
        );
    }

    #[test]
    fn set_dialog_item_text_redraw_clips_repaint_to_dialog_bounds() {
        let (mut disp, mut cpu, mut bus) = setup();
        let screen_base = bus.alloc((200 * 100) as u32);
        let row_bytes = 200u32;
        disp.set_screen_mode_for_test(screen_base, row_bytes, 200, 100, 8);
        bus.write_long(0x0824, screen_base);
        TrapDispatcher::fb_fill_rect(
            &mut bus,
            screen_base,
            row_bytes,
            8,
            200,
            100,
            0,
            0,
            100,
            200,
            true,
        );

        let bounds = (10, 20, 70, 120);
        let dialog_ptr = bus.alloc(170);
        bus.write_word(dialog_ptr + 6, 0); // GrafPort, not CGrafPort
        bus.write_word(dialog_ptr + 8, (-bounds.0) as u16);
        bus.write_word(dialog_ptr + 10, (-bounds.1) as u16);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, (bounds.2 - bounds.0) as u16);
        bus.write_word(dialog_ptr + 22, (bounds.3 - bounds.1) as u16);
        bus.write_word(dialog_ptr + 108, 2);
        bus.write_byte(dialog_ptr + 110, 0xFF);
        bus.write_word(dialog_ptr + 164, 1); // item 2 is the active edit field
        bus.write_word(dialog_ptr + 168, 1);

        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(30);
        let edit_handle = bus.alloc(4);
        let edit_ptr = bus.alloc(3);
        bus.write_bytes(edit_ptr, b"Old");
        bus.write_long(edit_handle, edit_ptr);
        bus.write_long(items_handle, ditl_ptr);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_word(ditl_ptr, 1); // two items
        bus.write_long(ditl_ptr + 2, 0);
        bus.write_word(ditl_ptr + 6, 5);
        bus.write_word(ditl_ptr + 8, 20);
        bus.write_word(ditl_ptr + 10, 20);
        bus.write_word(ditl_ptr + 12, 160); // extends beyond dialog right
        bus.write_byte(ditl_ptr + 14, 8);
        bus.write_byte(ditl_ptr + 15, 0);
        bus.write_long(ditl_ptr + 16, edit_handle);
        bus.write_word(ditl_ptr + 20, 25);
        bus.write_word(ditl_ptr + 22, 20);
        bus.write_word(ditl_ptr + 24, 41);
        bus.write_word(ditl_ptr + 26, 80);
        bus.write_byte(ditl_ptr + 28, 16);
        bus.write_byte(ditl_ptr + 29, 0);

        let items = vec![
            DialogItem {
                item_type: 8,
                rect: (5, 20, 20, 160),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            },
            DialogItem {
                item_type: 16,
                rect: (25, 20, 41, 80),
                text: "Old".to_string(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            },
        ];
        disp.dialog_items.insert(dialog_ptr, items.clone());
        disp.dialog_item_handles
            .insert(edit_handle, (dialog_ptr, 1));
        disp.draw_dialog(
            &mut bus, bounds, 2, "", &items, 1, "Old", 2, false, dialog_ptr,
        );

        let sample = screen_base + 17 * row_bytes + 150;
        let outside_before = bus.read_byte(sample);

        let pstr = bus.alloc(4);
        bus.write_byte(pstr, 3);
        bus.write_bytes(pstr + 1, b"New");
        bus.write_long(TEST_SP, pstr);
        bus.write_long(TEST_SP + 4, edit_handle);

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

        assert_eq!(
            bus.read_byte(sample),
            outside_before,
            "SetDialogItemText repaint must not clear outside the dialog port"
        );
    }

    #[test]
    fn get_dialog_item_text_returns_pascal_string_from_raw_handle() {
        let (mut disp, mut cpu, mut bus) = setup();
        let text_handle = bus.alloc(4);
        let text_ptr = bus.alloc(5);
        let out_ptr = 0x300000u32;

        bus.write_long(text_handle, text_ptr);
        bus.write_bytes(text_ptr, b"Hello");
        bus.write_long(TEST_SP, out_ptr);
        bus.write_long(TEST_SP + 4, text_handle);

        let result = disp.dispatch_dialog(true, 0x190, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_byte(out_ptr), 5);
        assert_eq!(bus.read_bytes(out_ptr + 1, 5), b"Hello".to_vec());
    }

    #[test]
    fn initialize_dialog_item_handles_rewrites_ditl_text_item_storage() {
        let (mut disp, _cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(22);

        bus.write_long(items_handle, ditl_ptr);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_word(ditl_ptr, 0);
        bus.write_long(ditl_ptr + 2, 0);
        bus.write_word(ditl_ptr + 6, 10);
        bus.write_word(ditl_ptr + 8, 20);
        bus.write_word(ditl_ptr + 10, 30);
        bus.write_word(ditl_ptr + 12, 40);
        bus.write_byte(ditl_ptr + 14, 8);
        bus.write_byte(ditl_ptr + 15, 5);
        bus.write_bytes(ditl_ptr + 16, b"Hello");

        let items = vec![DialogItem {
            item_type: 8,
            rect: (10, 20, 30, 40),
            text: "Hello".to_string(),
            resource_id: 0,
            proc_ptr: 0,
            sel_start: 0,
            sel_end: 0,
        }];

        disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);

        let created_handle = bus.read_long(ditl_ptr + 2);
        let created_ptr = bus.read_long(created_handle);
        assert_ne!(created_handle, 0);
        assert_eq!(bus.get_alloc_size(created_ptr), Some(5));
        assert_eq!(bus.read_bytes(created_ptr, 5), b"Hello".to_vec());
        assert_eq!(
            disp.dialog_item_handles.get(&created_handle),
            Some(&(dialog_ptr, 0))
        );
    }

    #[test]
    fn initialize_dialog_item_handles_materializes_icon_and_picture_handles() {
        let (mut disp, _cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(34);
        let icon_ptr = disp.install_test_resource(&mut bus, *b"ICON", 421, &[0xFF; 128]);
        let pict_ptr = disp.install_test_resource(&mut bus, *b"PICT", 422, &[0x11; 32]);

        bus.write_long(items_handle, ditl_ptr);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_word(ditl_ptr, 1);
        bus.write_long(ditl_ptr + 2, 0);
        bus.write_word(ditl_ptr + 6, 8);
        bus.write_word(ditl_ptr + 8, 8);
        bus.write_word(ditl_ptr + 10, 40);
        bus.write_word(ditl_ptr + 12, 40);
        bus.write_byte(ditl_ptr + 14, 32);
        bus.write_byte(ditl_ptr + 15, 2);
        bus.write_word(ditl_ptr + 16, 421);
        bus.write_long(ditl_ptr + 18, 0);
        bus.write_word(ditl_ptr + 22, 8);
        bus.write_word(ditl_ptr + 24, 48);
        bus.write_word(ditl_ptr + 26, 40);
        bus.write_word(ditl_ptr + 28, 112);
        bus.write_byte(ditl_ptr + 30, 64);
        bus.write_byte(ditl_ptr + 31, 2);
        bus.write_word(ditl_ptr + 32, 422);

        let items = vec![
            DialogItem {
                item_type: 32,
                rect: (8, 8, 40, 40),
                text: String::new(),
                resource_id: 421,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            },
            DialogItem {
                item_type: 64,
                rect: (8, 48, 40, 112),
                text: String::new(),
                resource_id: 422,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            },
        ];

        disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);

        let icon_handle = bus.read_long(ditl_ptr + 2);
        let pict_handle = bus.read_long(ditl_ptr + 18);
        assert_ne!(icon_handle, 0);
        assert_ne!(pict_handle, 0);
        assert_eq!(bus.read_long(icon_handle), icon_ptr);
        assert_eq!(bus.read_long(pict_handle), pict_ptr);
    }

    #[test]
    fn initialize_dialog_item_handles_clears_user_item_placeholder_proc_ptr() {
        let (mut disp, _cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(16);

        bus.write_long(items_handle, ditl_ptr);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_word(ditl_ptr, 0);
        bus.write_long(ditl_ptr + 2, 0x12345678);
        bus.write_word(ditl_ptr + 6, 10);
        bus.write_word(ditl_ptr + 8, 20);
        bus.write_word(ditl_ptr + 10, 30);
        bus.write_word(ditl_ptr + 12, 40);
        bus.write_byte(ditl_ptr + 14, 0);
        bus.write_byte(ditl_ptr + 15, 0);

        let items = vec![DialogItem {
            item_type: 0,
            rect: (10, 20, 30, 40),
            text: String::new(),
            resource_id: 0,
            proc_ptr: 0,
            sel_start: 0,
            sel_end: 0,
        }];

        disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);

        assert_eq!(bus.read_long(ditl_ptr + 2), 0);
    }

    #[test]
    fn initialize_dialog_item_handles_creates_standard_control_handle() {
        let (mut disp, _cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(22);

        bus.write_long(items_handle, ditl_ptr);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_word(ditl_ptr, 0);
        bus.write_long(ditl_ptr + 2, 0);
        bus.write_word(ditl_ptr + 6, 10);
        bus.write_word(ditl_ptr + 8, 20);
        bus.write_word(ditl_ptr + 10, 30);
        bus.write_word(ditl_ptr + 12, 120);
        bus.write_byte(ditl_ptr + 14, 5);
        bus.write_byte(ditl_ptr + 15, 5);
        bus.write_bytes(ditl_ptr + 16, b"Sound");

        let items = vec![DialogItem {
            item_type: 5,
            rect: (10, 20, 30, 120),
            text: "Sound".to_string(),
            resource_id: 0,
            proc_ptr: 0,
            sel_start: 0,
            sel_end: 0,
        }];

        disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);

        let control_handle = bus.read_long(ditl_ptr + 2);
        let control_ptr = bus.read_long(control_handle);
        assert_ne!(control_handle, 0);
        assert_ne!(control_ptr, 0);
        assert_eq!(bus.read_long(dialog_ptr + 140), control_handle);
        assert_eq!(bus.read_long(control_ptr + 4), dialog_ptr);
        assert_eq!(bus.read_word(control_ptr + 8) as i16, 10);
        assert_eq!(bus.read_word(control_ptr + 10) as i16, 20);
        assert_eq!(bus.read_byte(control_ptr + 17), 0);
        assert_eq!(
            disp.dialog_control_handles.get(&control_handle),
            Some(&(dialog_ptr, 1))
        );
        assert_eq!(disp.dialog_control_values.get(&(dialog_ptr, 1)), Some(&0));
        assert_eq!(disp.control_proc_ids.get(&control_ptr), Some(&1));
    }

    #[test]
    fn initialize_dialog_item_handles_creates_resctrl_control_handle() {
        let (mut disp, _cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(18);

        bus.write_long(items_handle, ditl_ptr);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_word(ditl_ptr, 0);
        bus.write_long(ditl_ptr + 2, 0);
        bus.write_word(ditl_ptr + 6, 10);
        bus.write_word(ditl_ptr + 8, 20);
        bus.write_word(ditl_ptr + 10, 30);
        bus.write_word(ditl_ptr + 12, 140);
        bus.write_byte(ditl_ptr + 14, 7);
        bus.write_byte(ditl_ptr + 15, 2);
        bus.write_word(ditl_ptr + 16, 128);

        let mut cntl = Vec::new();
        for word in [0i16, 0, 20, 110, 2, -1, 4, 1300, 1009] {
            cntl.extend_from_slice(&(word as u16).to_be_bytes());
        }
        cntl.extend_from_slice(&0x1234_5678u32.to_be_bytes());
        cntl.push(4);
        cntl.extend_from_slice(b"Team");
        disp.install_test_resource(&mut bus, *b"CNTL", 128, &cntl);

        let items = vec![DialogItem {
            item_type: 7,
            rect: (10, 20, 30, 140),
            text: String::new(),
            resource_id: 128,
            proc_ptr: 0,
            sel_start: 0,
            sel_end: 0,
        }];

        disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);

        let handle = bus.read_long(ditl_ptr + 2);
        let ctrl_ptr = bus.read_long(handle);
        assert_ne!(handle, 0);
        assert_ne!(ctrl_ptr, 0);
        assert_eq!(bus.read_long(dialog_ptr + 140), handle);
        assert_eq!(bus.read_long(ctrl_ptr + 4), dialog_ptr);
        assert_eq!(bus.read_word(ctrl_ptr + 8) as i16, 10);
        assert_eq!(bus.read_word(ctrl_ptr + 10) as i16, 20);
        assert_eq!(bus.read_word(ctrl_ptr + 12) as i16, 30);
        assert_eq!(bus.read_word(ctrl_ptr + 14) as i16, 140);
        assert_eq!(bus.read_word(ctrl_ptr + 18) as i16, 2);
        assert_eq!(bus.read_word(ctrl_ptr + 20) as i16, 1300);
        assert_eq!(disp.control_proc_ids.get(&ctrl_ptr), Some(&1009));
        assert_eq!(
            disp.dialog_control_handles.get(&handle),
            Some(&(dialog_ptr, 1))
        );
        assert_eq!(disp.dialog_control_values.get(&(dialog_ptr, 1)), Some(&2));
    }

    #[test]
    fn popup_resctrl_initializes_contrl_data_private_menu_record() {
        let (mut disp, _cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(18);

        bus.write_long(items_handle, ditl_ptr);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_word(ditl_ptr, 0);
        bus.write_long(ditl_ptr + 2, 0);
        bus.write_word(ditl_ptr + 6, 10);
        bus.write_word(ditl_ptr + 8, 20);
        bus.write_word(ditl_ptr + 10, 30);
        bus.write_word(ditl_ptr + 12, 140);
        bus.write_byte(ditl_ptr + 14, 7);
        bus.write_byte(ditl_ptr + 15, 2);
        bus.write_word(ditl_ptr + 16, 128);

        let mut cntl = Vec::new();
        for word in [0i16, 0, 20, 110, 1, -1, 0, 4000, 1009] {
            cntl.extend_from_slice(&(word as u16).to_be_bytes());
        }
        cntl.extend_from_slice(&0u32.to_be_bytes());
        cntl.push(0);
        disp.install_test_resource(&mut bus, *b"CNTL", 128, &cntl);

        let items = vec![DialogItem {
            item_type: 7,
            rect: (10, 20, 30, 140),
            text: String::new(),
            resource_id: 128,
            proc_ptr: 0,
            sel_start: 0,
            sel_end: 0,
        }];

        disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);

        let ctrl_handle = bus.read_long(ditl_ptr + 2);
        let ctrl_ptr = bus.read_long(ctrl_handle);
        let private_handle = bus.read_long(ctrl_ptr + 28);
        let private_ptr = bus.read_long(private_handle);
        let menu_handle = bus.read_long(private_ptr);
        let menu_ptr = bus.read_long(menu_handle);

        assert_ne!(private_handle, 0);
        assert_ne!(private_ptr, 0);
        assert_ne!(menu_handle, 0);
        assert_eq!(bus.read_word(private_ptr + 4) as i16, 4000);
        assert_eq!(bus.read_word(menu_ptr) as i16, 4000);
        assert_eq!(disp.popup_control_menu_id(&bus, ctrl_ptr, -1), 4000);
        assert!(disp
            .menus
            .iter()
            .any(|menu| menu.id == 4000 && menu.handle == menu_handle));
    }

    #[test]
    fn append_ditl_overlay_preserves_existing_handles_and_initializes_resctrl() {
        let (mut disp, _cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let items_handle = bus.alloc(4);
        let old_ditl_ptr = bus.alloc(18);
        let preserved_handle = 0x00A1_B2C3;

        bus.write_long(items_handle, old_ditl_ptr);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_word(old_ditl_ptr, 0);
        bus.write_long(old_ditl_ptr + 2, preserved_handle);
        bus.write_word(old_ditl_ptr + 6, 10);
        bus.write_word(old_ditl_ptr + 8, 20);
        bus.write_word(old_ditl_ptr + 10, 30);
        bus.write_word(old_ditl_ptr + 12, 60);
        bus.write_byte(old_ditl_ptr + 14, 4);
        bus.write_byte(old_ditl_ptr + 15, 2);
        bus.write_bytes(old_ditl_ptr + 16, b"OK");
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 4,
                rect: (10, 20, 30, 60),
                text: "OK".to_string(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        let mut cntl = Vec::new();
        for word in [0i16, 0, 20, 110, 3, -1, 1, 5, 1008] {
            cntl.extend_from_slice(&(word as u16).to_be_bytes());
        }
        cntl.extend_from_slice(&0x1234_5678u32.to_be_bytes());
        cntl.push(4);
        cntl.extend_from_slice(b"Pane");
        disp.install_test_resource(&mut bus, *b"CNTL", 131, &cntl);

        let append_handle = bus.alloc(4);
        let append_ditl_ptr = bus.alloc(18);
        bus.write_long(append_handle, append_ditl_ptr);
        bus.write_word(append_ditl_ptr, 0);
        bus.write_long(append_ditl_ptr + 2, 0);
        bus.write_word(append_ditl_ptr + 6, 40);
        bus.write_word(append_ditl_ptr + 8, 50);
        bus.write_word(append_ditl_ptr + 10, 60);
        bus.write_word(append_ditl_ptr + 12, 170);
        bus.write_byte(append_ditl_ptr + 14, 7);
        bus.write_byte(append_ditl_ptr + 15, 2);
        bus.write_word(append_ditl_ptr + 16, 131);

        let count = disp.append_ditl_to_dialog(&mut bus, dialog_ptr, append_handle, 0);

        assert_eq!(count, 2);
        let new_ditl_ptr = bus.read_long(items_handle);
        assert_ne!(new_ditl_ptr, old_ditl_ptr);
        assert_eq!(bus.read_word(new_ditl_ptr), 1);
        assert_eq!(
            bus.read_long(new_ditl_ptr + 2),
            preserved_handle,
            "AppendDITL must not recreate existing item handles"
        );
        let ctrl_handle = bus.read_long(new_ditl_ptr + 18);
        let ctrl_ptr = bus.read_long(ctrl_handle);
        assert_ne!(ctrl_handle, 0);
        assert_ne!(ctrl_ptr, 0);
        assert_eq!(bus.read_long(dialog_ptr + 140), ctrl_handle);
        assert_eq!(bus.read_word(ctrl_ptr + 8) as i16, 40);
        assert_eq!(bus.read_word(ctrl_ptr + 10) as i16, 50);
        assert_eq!(bus.read_word(ctrl_ptr + 12) as i16, 60);
        assert_eq!(bus.read_word(ctrl_ptr + 14) as i16, 170);
        assert_eq!(
            disp.dialog_control_handles.get(&ctrl_handle),
            Some(&(dialog_ptr, 2))
        );
        assert_eq!(disp.dialog_control_values.get(&(dialog_ptr, 2)), Some(&3));
    }

    #[test]
    fn shorten_ditl_erases_removed_retained_item_rects_without_wiping_header_pixels() {
        let (mut disp, _cpu, mut bus) = setup();
        let screen_base = bus.alloc((400 * 300) as u32);
        for i in 0..400u32 * 300 {
            bus.write_byte(screen_base + i, 0x00);
        }
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 400, 400, 300, 8);

        let dialog_ptr = bus.alloc(200);
        bus.write_word(dialog_ptr + 6, 0);
        bus.write_word(dialog_ptr + 8, (-100i16) as u16);
        bus.write_word(dialog_ptr + 10, (-100i16) as u16);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 120);
        bus.write_word(dialog_ptr + 22, 200);
        let bounds = TrapDispatcher::dialog_screen_bounds(&bus, dialog_ptr);

        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(38);
        bus.write_long(items_handle, ditl_ptr);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_word(ditl_ptr, 1);
        bus.write_long(ditl_ptr + 2, 0);
        bus.write_word(ditl_ptr + 6, 10);
        bus.write_word(ditl_ptr + 8, 10);
        bus.write_word(ditl_ptr + 10, 30);
        bus.write_word(ditl_ptr + 12, 80);
        bus.write_byte(ditl_ptr + 14, 8);
        bus.write_byte(ditl_ptr + 15, 2);
        bus.write_bytes(ditl_ptr + 16, b"OK");
        bus.write_long(ditl_ptr + 20, 0);
        bus.write_word(ditl_ptr + 24, 40);
        bus.write_word(ditl_ptr + 26, 40);
        bus.write_word(ditl_ptr + 28, 60);
        bus.write_word(ditl_ptr + 30, 160);
        bus.write_byte(ditl_ptr + 32, 8);
        bus.write_byte(ditl_ptr + 33, 3);
        bus.write_bytes(ditl_ptr + 34, b"Old");

        disp.dialog_items.insert(
            dialog_ptr,
            vec![
                DialogItem {
                    item_type: 8,
                    rect: (10, 10, 30, 80),
                    text: "OK".to_string(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
                DialogItem {
                    item_type: 8,
                    rect: (40, 40, 60, 160),
                    text: "Old".to_string(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
            ],
        );

        disp.fill_rect_clipped_to_dialog(&mut bus, bounds, (106, 106, 114, 170), true);
        disp.fill_rect_clipped_to_dialog(&mut bus, bounds, (140, 140, 160, 260), true);
        let pixels = disp.save_dialog_pixels(&bus, bounds);
        disp.dialog_visible_snapshots
            .insert(dialog_ptr, PersistentDialogSnapshot { bounds, pixels });

        let count = disp.shorten_ditl_in_dialog(&mut bus, dialog_ptr, 1);

        assert_eq!(count, 1);
        assert_eq!(bus.read_word(ditl_ptr), 0);
        assert_eq!(bus.read_byte(screen_base + 110 * 400 + 120), 0xFF);
        assert_eq!(bus.read_byte(screen_base + 150 * 400 + 150), 0x00);
        let retained = disp.dialog_visible_snapshots.get(&dialog_ptr).unwrap();
        assert!(
            retained.pixels == disp.save_dialog_pixels(&bus, bounds),
            "ShortenDITL retained snapshot must match the erased framebuffer"
        );
    }

    #[test]
    fn dialog_item_handle_addr_skips_zero_len_resctrl_resource_id() {
        let (mut disp, _cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(36);

        bus.write_long(items_handle, ditl_ptr);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_word(ditl_ptr, 1);

        bus.write_long(ditl_ptr + 2, 0);
        bus.write_word(ditl_ptr + 6, 10);
        bus.write_word(ditl_ptr + 8, 20);
        bus.write_word(ditl_ptr + 10, 30);
        bus.write_word(ditl_ptr + 12, 140);
        bus.write_byte(ditl_ptr + 14, 7);
        bus.write_byte(ditl_ptr + 15, 0);
        bus.write_word(ditl_ptr + 16, 128);

        bus.write_long(ditl_ptr + 18, 0);
        bus.write_word(ditl_ptr + 22, 40);
        bus.write_word(ditl_ptr + 24, 20);
        bus.write_word(ditl_ptr + 26, 52);
        bus.write_word(ditl_ptr + 28, 140);
        bus.write_byte(ditl_ptr + 30, 8);
        bus.write_byte(ditl_ptr + 31, 3);
        bus.write_bytes(ditl_ptr + 32, b"abc");
        bus.write_byte(ditl_ptr + 35, 0);

        let mut cntl = Vec::new();
        for word in [0i16, 0, 20, 110, 2, -1, 4, 1300, 1009] {
            cntl.extend_from_slice(&(word as u16).to_be_bytes());
        }
        cntl.extend_from_slice(&0x1234_5678u32.to_be_bytes());
        cntl.push(4);
        cntl.extend_from_slice(b"Team");
        disp.install_test_resource(&mut bus, *b"CNTL", 128, &cntl);

        let items = vec![
            DialogItem {
                item_type: 7,
                rect: (10, 20, 30, 140),
                text: String::new(),
                resource_id: 128,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            },
            DialogItem {
                item_type: 8,
                rect: (40, 20, 52, 140),
                text: "abc".to_string(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            },
        ];

        disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);

        assert_eq!(
            bus.read_word(ditl_ptr + 16),
            128,
            "resCtrl resource ID must not be overwritten by the next item handle"
        );
        assert_ne!(bus.read_long(ditl_ptr + 2), 0);
        assert_ne!(bus.read_long(ditl_ptr + 18), 0);
        assert_eq!(
            TrapDispatcher::dialog_item_handle(&bus, dialog_ptr, 2),
            bus.read_long(ditl_ptr + 18)
        );
    }

    #[test]
    fn draw_dialog_resctrl_uses_cntl_proc_id_not_always_popup() {
        let (mut disp, _cpu, mut bus) = setup();
        let screen_base = 0x300000u32;
        let row_bytes: u32 = 640;
        disp.set_screen_mode_for_test(screen_base, row_bytes, 640, 480, 8);

        let dialog_ptr = bus.alloc(170);
        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(18);
        bus.write_long(items_handle, ditl_ptr);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_word(ditl_ptr, 0);
        bus.write_long(ditl_ptr + 2, 0);
        bus.write_word(ditl_ptr + 6, 20);
        bus.write_word(ditl_ptr + 8, 20);
        bus.write_word(ditl_ptr + 10, 40);
        bus.write_word(ditl_ptr + 12, 120);
        bus.write_byte(ditl_ptr + 14, 7);
        bus.write_byte(ditl_ptr + 15, 2);
        bus.write_word(ditl_ptr + 16, 128);

        let mut cntl = Vec::new();
        for word in [0i16, 0, 20, 100, 0, -1, 0, 0, 0] {
            cntl.extend_from_slice(&(word as u16).to_be_bytes());
        }
        cntl.extend_from_slice(&0u32.to_be_bytes());
        cntl.push(2);
        cntl.extend_from_slice(b"OK");
        disp.install_test_resource(&mut bus, *b"CNTL", 128, &cntl);

        let items = vec![DialogItem {
            item_type: 7,
            rect: (20, 20, 40, 120),
            text: String::new(),
            resource_id: 128,
            proc_ptr: 0,
            sel_start: 0,
            sel_end: 0,
        }];
        disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);

        disp.draw_dialog(
            &mut bus,
            (100, 100, 180, 260),
            3,
            "",
            &items,
            0,
            "",
            0,
            false,
            dialog_ptr,
        );

        let old_popup_arrow_pixel = screen_base + 130 * row_bytes + 210;
        assert_ne!(
            bus.read_byte(old_popup_arrow_pixel),
            0xFF,
            "push-button resCtrl must not draw popup-menu arrow pixels"
        );
    }

    #[test]
    fn show_dialog_item_updates_live_resctrl_control_rect() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(18);
        let hidden_rect = (20, 20 + 16384, 40, 120 + 16384);
        let visible_rect = (20, 20, 40, 120);

        bus.write_long(items_handle, ditl_ptr);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_word(ditl_ptr, 0);
        bus.write_long(ditl_ptr + 2, 0);
        bus.write_word(ditl_ptr + 6, hidden_rect.0 as u16);
        bus.write_word(ditl_ptr + 8, hidden_rect.1 as u16);
        bus.write_word(ditl_ptr + 10, hidden_rect.2 as u16);
        bus.write_word(ditl_ptr + 12, hidden_rect.3 as u16);
        bus.write_byte(ditl_ptr + 14, 7);
        bus.write_byte(ditl_ptr + 15, 2);
        bus.write_word(ditl_ptr + 16, 128);

        let mut cntl = Vec::new();
        for word in [0i16, 0, 20, 100, 0, -1, 0, 0, 0] {
            cntl.extend_from_slice(&(word as u16).to_be_bytes());
        }
        cntl.extend_from_slice(&0u32.to_be_bytes());
        cntl.push(2);
        cntl.extend_from_slice(b"OK");
        disp.install_test_resource(&mut bus, *b"CNTL", 128, &cntl);

        let items = vec![DialogItem {
            item_type: 7,
            rect: hidden_rect,
            text: String::new(),
            resource_id: 128,
            proc_ptr: 0,
            sel_start: 0,
            sel_end: 0,
        }];
        disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);
        disp.dialog_items.insert(dialog_ptr, items);
        disp.hidden_dialog_item_rects
            .insert((dialog_ptr, 1), visible_rect);

        bus.write_word(TEST_SP, 1);
        bus.write_long(TEST_SP + 2, dialog_ptr);
        disp.dispatch_dialog(true, 0x028, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        let ctrl_handle = disp.dialog_control_handle_for_item(dialog_ptr, 1).unwrap();
        let ctrl_ptr = bus.read_long(ctrl_handle);
        assert_eq!(bus.read_word(ctrl_ptr + 8) as i16, visible_rect.0);
        assert_eq!(bus.read_word(ctrl_ptr + 10) as i16, visible_rect.1);
        assert_eq!(bus.read_word(ctrl_ptr + 12) as i16, visible_rect.2);
        assert_eq!(bus.read_word(ctrl_ptr + 14) as i16, visible_rect.3);
    }

    #[test]
    fn parse_ditl_preserves_user_item_proc_ptr() {
        let (_disp, _cpu, mut bus) = setup();
        let ditl_ptr = bus.alloc(16);

        bus.write_word(ditl_ptr, 0);
        bus.write_long(ditl_ptr + 2, 0x12345678);
        bus.write_word(ditl_ptr + 6, 10);
        bus.write_word(ditl_ptr + 8, 20);
        bus.write_word(ditl_ptr + 10, 30);
        bus.write_word(ditl_ptr + 12, 40);
        bus.write_byte(ditl_ptr + 14, 0);
        bus.write_byte(ditl_ptr + 15, 0);

        let items = TrapDispatcher::parse_ditl(&bus, ditl_ptr, 16);
        assert_eq!(items.len(), 1);
        assert_eq!(items[0].proc_ptr, 0x12345678);
    }

    #[test]
    fn initialize_dialog_item_handles_preserves_user_item_proc_ptr() {
        let (mut disp, _cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(16);

        bus.write_long(items_handle, ditl_ptr);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_word(ditl_ptr, 0);
        bus.write_long(ditl_ptr + 2, 0);
        bus.write_word(ditl_ptr + 6, 10);
        bus.write_word(ditl_ptr + 8, 20);
        bus.write_word(ditl_ptr + 10, 30);
        bus.write_word(ditl_ptr + 12, 40);
        bus.write_byte(ditl_ptr + 14, 0);
        bus.write_byte(ditl_ptr + 15, 0);

        let items = vec![DialogItem {
            item_type: 0,
            rect: (10, 20, 30, 40),
            text: String::new(),
            resource_id: 0,
            proc_ptr: 0x12345678,
            sel_start: 0,
            sel_end: 0,
        }];

        disp.initialize_dialog_item_handles(&mut bus, dialog_ptr, &items);

        assert_eq!(bus.read_long(ditl_ptr + 2), 0x12345678);
    }

    #[test]
    fn refresh_ditl_proc_ptrs_clears_stale_user_item_proc_ptr() {
        let (_disp, _cpu, mut bus) = setup();
        let dialog_ptr = bus.alloc(170);
        let items_handle = bus.alloc(4);
        let ditl_ptr = bus.alloc(16);

        bus.write_long(items_handle, ditl_ptr);
        bus.write_long(dialog_ptr + 156, items_handle);
        bus.write_word(ditl_ptr, 0);
        bus.write_long(ditl_ptr + 2, 0);
        bus.write_word(ditl_ptr + 6, 10);
        bus.write_word(ditl_ptr + 8, 20);
        bus.write_word(ditl_ptr + 10, 30);
        bus.write_word(ditl_ptr + 12, 40);
        bus.write_byte(ditl_ptr + 14, 0);
        bus.write_byte(ditl_ptr + 15, 0);

        let mut items = vec![DialogItem {
            item_type: 0,
            rect: (10, 20, 30, 40),
            text: String::new(),
            resource_id: 0,
            proc_ptr: 0x12345678,
            sel_start: 0,
            sel_end: 0,
        }];

        TrapDispatcher::refresh_ditl_proc_ptrs(&bus, dialog_ptr, &mut items);

        assert_eq!(items[0].proc_ptr, 0);
    }

    // ---- ModalDialog ($A991) ----

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

        let item_hit_addr = 0x300000u32;
        bus.write_word(item_hit_addr, 0); // pre-clear

        // SP+0: item_hit_ptr (4), SP+4: filterProc (4)
        bus.write_long(TEST_SP, item_hit_addr);
        bus.write_long(TEST_SP + 4, 0); // nil filterProc

        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(item_hit_addr), 1);
    }

    #[test]
    fn modal_dialog_retained_edit_text_appends_across_reentry_and_command_a_selects() {
        let (mut disp, mut cpu, mut bus) = setup_with_port();
        let screen_base = bus.alloc(320 * 240);
        for offset in 0..320u32 * 240 {
            bus.write_byte(screen_base + offset, 0xFF);
        }
        bus.write_long(0x0824, screen_base);
        disp.set_screen_mode_for_test(screen_base, 320, 320, 240, 8);

        let dialog_ptr = bus.alloc(256);
        let item_hit_addr = 0x300000u32;
        let bounds = (40, 40, 130, 280);
        disp.front_window = dialog_ptr;
        disp.current_port = dialog_ptr;
        disp.window_bounds = bounds;
        disp.window_proc_id = 2;
        disp.window_title.clear();
        disp.window_list = vec![dialog_ptr];
        bus.write_word(dialog_ptr + 108, 2);
        bus.write_word(dialog_ptr + 164, 0);
        bus.write_word(dialog_ptr + 168, 0);
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 16,
                rect: (24, 24, 42, 190),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        fn enter_modal(
            disp: &mut TrapDispatcher,
            cpu: &mut MockCpu,
            bus: &mut MacMemoryBus,
            item_hit_addr: u32,
        ) {
            bus.write_word(item_hit_addr, 0xCAFE);
            bus.write_long(TEST_SP, item_hit_addr);
            bus.write_long(TEST_SP + 4, 0);
            cpu.write_reg(Register::A7, TEST_SP);
            disp.dispatch_dialog(true, 0x191, cpu, bus)
                .unwrap()
                .unwrap();
            assert!(disp.dialog_tracking.is_some());
            assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
        }

        fn key_down(
            disp: &mut TrapDispatcher,
            cpu: &mut MockCpu,
            bus: &mut MacMemoryBus,
            item_hit_addr: u32,
            key_code: u8,
            char_code: u8,
            modifiers: u16,
        ) {
            disp.event_queue
                .push_back(crate::trap::dispatch::QueuedEvent {
                    what: 3,
                    message: (u32::from(key_code) << 8) | u32::from(char_code),
                    where_v: 0,
                    where_h: 0,
                    modifiers,
                });
            disp.dispatch_dialog(true, 0x191, cpu, bus)
                .unwrap()
                .unwrap();
            assert!(disp.dialog_tracking.is_none());
            assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
            assert_eq!(bus.read_word(item_hit_addr), 1);
        }

        for (key_code, char_code) in [(0x02, b'd'), (0x20, b'u'), (0x02, b'd')] {
            enter_modal(&mut disp, &mut cpu, &mut bus, item_hit_addr);
            key_down(
                &mut disp,
                &mut cpu,
                &mut bus,
                item_hit_addr,
                key_code,
                char_code,
                0,
            );
        }

        let item = &disp.dialog_items[&dialog_ptr][0];
        assert_eq!(item.text, "dud");
        assert_eq!((item.sel_start, item.sel_end), (3, 3));
        assert!(disp
            .dialog_edit_text_modified_items
            .contains(&(dialog_ptr, 1)));

        enter_modal(&mut disp, &mut cpu, &mut bus, item_hit_addr);
        key_down(
            &mut disp,
            &mut cpu,
            &mut bus,
            item_hit_addr,
            0x00,
            b'a',
            0x0100,
        );
        let item = &disp.dialog_items[&dialog_ptr][0];
        assert_eq!(item.text, "dud");
        assert_eq!((item.sel_start, item.sel_end), (0, 3));
        assert!(!disp
            .dialog_edit_text_modified_items
            .contains(&(dialog_ptr, 1)));

        enter_modal(&mut disp, &mut cpu, &mut bus, item_hit_addr);
        key_down(&mut disp, &mut cpu, &mut bus, item_hit_addr, 0x33, 0x08, 0);
        let item = &disp.dialog_items[&dialog_ptr][0];
        assert_eq!(item.text, "");
        assert_eq!((item.sel_start, item.sel_end), (0, 0));
    }

    #[test]
    fn modal_dialog_popup_resctrl_tracks_selection_and_returns_item() {
        let (mut disp, mut cpu, mut bus) = setup();
        let screen_base = bus.alloc((800 * 600) as u32);
        for i in 0..800u32 * 600 {
            bus.write_byte(screen_base + i, 0xFF);
        }
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 800, 800, 600, 8);

        let dialog_ptr = bus.alloc(256);
        bus.write_word(dialog_ptr + 6, 0);
        bus.write_word(dialog_ptr + 8, (-100i16) as u16);
        bus.write_word(dialog_ptr + 10, (-100i16) as u16);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 180);
        bus.write_word(dialog_ptr + 22, 220);
        let dialog_bounds = (100, 100, 280, 320);

        let ctrl_ptr = bus.alloc(296);
        let ctrl_handle = bus.alloc(4);
        bus.write_long(ctrl_handle, ctrl_ptr);
        disp.initialize_control_record(
            &mut bus,
            ctrl_ptr,
            dialog_ptr,
            (10, 20, 30, 130),
            b"",
            true,
            1,
            900,
            0,
            1009,
            0,
        );
        disp.dialog_control_handles
            .insert(ctrl_handle, (dialog_ptr, 1));
        disp.dialog_control_values.insert((dialog_ptr, 1), 1);
        disp.menus.push(Menu {
            id: 900,
            title: "Squadies".to_string(),
            items: vec![
                MenuItem {
                    text: "Duke".to_string(),
                    icon: 0,
                    key_equiv: 0,
                    mark: 0,
                    style: 0,
                    enabled: true,
                },
                MenuItem {
                    text: "Carnage".to_string(),
                    icon: 0,
                    key_equiv: 0,
                    mark: 0,
                    style: 0,
                    enabled: true,
                },
            ],
            enabled: true,
            handle: 0,
            in_menu_bar: false,
            hierarchical: false,
            visible_in_menu_bar: false,
        });

        let item_hit_addr = 0x300000u32;
        bus.write_word(item_hit_addr, 0);
        cpu.write_reg(Register::A7, TEST_SP);
        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
            dialog_ptr,
            bounds: dialog_bounds,
            title: String::new(),
            proc_id: 2,
            items: vec![DialogItem {
                item_type: 7,
                rect: (10, 20, 30, 130),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
            default_item: 0,
            cancel_item: 0,
            edit_text: String::new(),
            edit_item: 0,
            saved_pixels: Vec::new(),
            stack_ptr: TEST_SP,
            item_hit_ptr: item_hit_addr,
            rendered_pixels: Vec::new(),
            flash_remaining: 0,
            flash_delay: 0,
            flash_item: 0,
            edit_text_modified: false,
            draw_proc_queue: VecDeque::new(),
            draw_procs_done: true,
            rendered_pixels_final: true,
            filter_proc: 0,
            game_managed: false,
            last_filter_event: None,
            popup_draws: Vec::new(),
            active_popup: None,
            active_button: None,
            active_user_item: None,
        });
        disp.mouse_button = true;
        disp.mouse_pos = (115, 125);
        disp.event_queue
            .push_back(crate::trap::dispatch::QueuedEvent {
                what: 1,
                message: 0,
                where_v: 115,
                where_h: 125,
                modifiers: 0,
            });

        disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        assert!(disp
            .dialog_tracking
            .as_ref()
            .and_then(|tracking| tracking.active_popup.as_ref())
            .is_some());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
        let stale_open_popup_pixels = disp.save_dialog_pixels(&bus, dialog_bounds);
        let tracking = disp.dialog_tracking.as_mut().unwrap();
        tracking.rendered_pixels = stale_open_popup_pixels.clone();
        tracking.rendered_pixels_final = true;

        disp.mouse_pos = (148, 125);
        disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        assert_eq!(
            disp.dialog_tracking
                .as_ref()
                .and_then(|tracking| tracking.active_popup.as_ref())
                .map(|popup| popup.highlighted_item),
            Some(2)
        );

        disp.mouse_button = false;
        disp.event_queue
            .push_back(crate::trap::dispatch::QueuedEvent {
                what: 2,
                message: 0,
                where_v: 148,
                where_h: 125,
                modifiers: 0,
            });
        disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        assert!(disp.dialog_tracking.is_none());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(item_hit_addr), 1);
        assert_eq!(bus.read_word(ctrl_ptr + 18) as i16, 2);
        assert_eq!(disp.dialog_control_values.get(&(dialog_ptr, 1)), Some(&2));

        let retained = disp.dialog_visible_snapshots.get(&dialog_ptr).unwrap();
        let current_pixels = disp.save_dialog_pixels(&bus, dialog_bounds);
        assert!(
            retained.pixels == current_pixels,
            "retained snapshot must match the closed popup framebuffer"
        );
        assert!(
            retained.pixels != stale_open_popup_pixels,
            "retained snapshot kept stale open-popup pixels"
        );
    }

    #[test]
    fn modal_dialog_game_managed_dialog_still_queues_user_item_draw_procs() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x200000u32;
        let item_hit_addr = 0x300000u32;

        disp.front_window = dialog_ptr;
        disp.window_bounds = (92, 95, 415, 704);
        disp.window_proc_id = 2;
        disp.window_title.clear();
        disp.dialog_items.insert(
            dialog_ptr,
            vec![
                DialogItem {
                    item_type: 0,
                    rect: (8, 353, 148, 603),
                    text: String::new(),
                    resource_id: 0,
                    proc_ptr: 0x500000,
                    sel_start: 0,
                    sel_end: 0,
                },
                DialogItem {
                    item_type: 0x80,
                    rect: (159, 381, 184, 581),
                    text: String::new(),
                    resource_id: 0,
                    proc_ptr: 0x500100,
                    sel_start: 0,
                    sel_end: 0,
                },
                DialogItem {
                    item_type: 0x80,
                    rect: (324, 650, 349, 771),
                    text: String::new(),
                    resource_id: 0,
                    proc_ptr: 0x500200,
                    sel_start: 0,
                    sel_end: 0,
                },
            ],
        );

        bus.write_long(TEST_SP, item_hit_addr);
        bus.write_long(TEST_SP + 4, 0x149F0);

        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);

        let tracking = disp.dialog_tracking.as_ref().unwrap();
        assert!(tracking.game_managed);
        assert_eq!(tracking.filter_proc, 0x149F0);
        assert_eq!(tracking.draw_proc_queue.len(), 2);
        assert_eq!(
            tracking
                .draw_proc_queue
                .iter()
                .map(|(_, item_no)| *item_no)
                .collect::<Vec<_>>(),
            vec![1, 2]
        );
        assert!(!tracking.draw_procs_done);
        assert!(!tracking.rendered_pixels_final);
    }

    #[test]
    fn modal_dialog_retained_reentry_reuses_visible_snapshot_without_user_item_redraw() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x200000u32;
        let item_hit_addr = 0x300000u32;
        let screen_base = bus.alloc((64 * 64) as u32);
        let row_bytes = 64u32;
        for i in 0..row_bytes * 64 {
            bus.write_byte(screen_base + i, 0x11);
        }
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, row_bytes, 64, 64, 8);

        let bounds = (10, 12, 30, 34);
        let snapshot_width = (bounds.3 - bounds.1 + TrapDispatcher::DBOX_FRAME_MARGIN * 2) as usize;
        let snapshot_height =
            (bounds.2 - bounds.0 + TrapDispatcher::DBOX_FRAME_MARGIN * 2) as usize;
        let visible_pixels = vec![0x44; snapshot_width * snapshot_height];
        disp.dialog_visible_snapshots.insert(
            dialog_ptr,
            PersistentDialogSnapshot {
                bounds,
                pixels: visible_pixels,
            },
        );
        disp.dialog_saved_pixels
            .insert(dialog_ptr, vec![0x22; snapshot_width * snapshot_height]);
        disp.dialog_modal_entered.insert(dialog_ptr);

        disp.front_window = dialog_ptr;
        disp.window_bounds = bounds;
        disp.window_proc_id = 1;
        disp.window_title.clear();
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 0x80,
                rect: (0, 0, 20, 22),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0x500000,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        bus.write_long(TEST_SP, item_hit_addr);
        bus.write_long(TEST_SP + 4, 0);

        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);

        let tracking = disp.dialog_tracking.as_ref().unwrap();
        assert!(tracking.draw_proc_queue.is_empty());
        assert!(tracking.draw_procs_done);
        assert!(tracking.rendered_pixels_final);
        assert_eq!(
            bus.read_byte(screen_base + 20 * row_bytes + 20),
            0x44,
            "retained visible snapshot should be restored on re-entry"
        );
    }

    #[test]
    fn modal_dialog_first_entry_with_showwindow_snapshot_still_queues_user_item_draw_proc() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x200000u32;
        let item_hit_addr = 0x300000u32;
        let screen_base = bus.alloc((64 * 64) as u32);
        let row_bytes = 64u32;
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, row_bytes, 64, 64, 8);

        let bounds = (10, 12, 30, 34);
        let snapshot_width = (bounds.3 - bounds.1 + TrapDispatcher::DBOX_FRAME_MARGIN * 2) as usize;
        let snapshot_height =
            (bounds.2 - bounds.0 + TrapDispatcher::DBOX_FRAME_MARGIN * 2) as usize;
        disp.dialog_visible_snapshots.insert(
            dialog_ptr,
            PersistentDialogSnapshot {
                bounds,
                pixels: vec![0x44; snapshot_width * snapshot_height],
            },
        );

        disp.front_window = dialog_ptr;
        disp.window_bounds = bounds;
        disp.window_proc_id = 1;
        disp.window_title.clear();
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 0x80,
                rect: (0, 0, 20, 22),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0x500000,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        bus.write_long(TEST_SP, item_hit_addr);
        bus.write_long(TEST_SP + 4, 0);

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

        let tracking = disp.dialog_tracking.as_ref().unwrap();
        assert_eq!(tracking.draw_proc_queue.len(), 1);
        assert_eq!(tracking.draw_proc_queue[0], (0x500000, 1));
        assert!(!tracking.draw_procs_done);
        assert!(!tracking.rendered_pixels_final);
    }

    #[test]
    fn dialog_game_managed_ignores_offscreen_placeholder_items() {
        let bounds = (200, 146, 400, 510);
        let items = vec![
            DialogItem {
                item_type: 0,
                rect: (167, 279, 192, 357),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            },
            DialogItem {
                item_type: 0xC0, // disabled picture placeholder outside bounds
                rect: (217, 179, 247, 247),
                text: String::new(),
                resource_id: 1431,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            },
            DialogItem {
                item_type: 0x80, // disabled userItem inside bounds
                rect: (7, 8, 157, 358),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            },
        ];

        assert!(TrapDispatcher::dialog_is_game_managed(bounds, &items));

        let mut standard_items = items;
        standard_items.push(DialogItem {
            item_type: 8,
            rect: (20, 20, 40, 100),
            text: "Standard".to_string(),
            resource_id: 0,
            proc_ptr: 0,
            sel_start: 0,
            sel_end: 0,
        });
        assert!(!TrapDispatcher::dialog_is_game_managed(
            bounds,
            &standard_items
        ));
    }

    #[test]
    fn redraw_dialog_window_contents_does_not_snapshot_game_managed_shell() {
        let (mut disp, _cpu, mut bus) = setup();
        let dialog_ptr = 0x200000u32;
        let screen_base = bus.alloc(64 * 64);
        for offset in 0..64u32 * 64 {
            bus.write_byte(screen_base + offset, 0x11);
        }
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 64, 64, 64, 8);

        bus.write_long(dialog_ptr + 2, screen_base);
        bus.write_word(dialog_ptr + 6, 64);
        bus.write_word(dialog_ptr + 8, 0);
        bus.write_word(dialog_ptr + 10, 0);
        bus.write_word(dialog_ptr + 12, 20);
        bus.write_word(dialog_ptr + 14, 20);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 20);
        bus.write_word(dialog_ptr + 22, 20);
        bus.write_word(dialog_ptr + 108, 2);
        bus.write_byte(dialog_ptr + 110, 0xFF);
        disp.window_proc_ids.insert(dialog_ptr, 2);
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 0x80,
                rect: (2, 2, 10, 10),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );
        disp.dialog_visible_snapshots.insert(
            dialog_ptr,
            PersistentDialogSnapshot {
                bounds: (0, 0, 20, 20),
                pixels: vec![0x44; 30 * 30],
            },
        );

        disp.redraw_dialog_window_contents(&mut bus, dialog_ptr);

        assert!(
            !disp.dialog_visible_snapshots.contains_key(&dialog_ptr),
            "ShowWindow redraw must not retain a stale shell for app-drawn all-userItem dialogs"
        );
    }

    #[test]
    fn modal_dialog_resnapshot_redraws_popup_controls_after_user_item_draw_procs() {
        let (mut disp, mut cpu, mut bus) = setup();
        let screen_base = bus.alloc((800 * 600) as u32);
        for i in 0..800u32 * 600 {
            bus.write_byte(screen_base + i, 0xFF);
        }
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 800, 800, 600, 8);

        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
            dialog_ptr: 0x200000,
            bounds: (0, 0, 100, 220),
            title: String::new(),
            proc_id: 2,
            items: Vec::new(),
            default_item: 0,
            cancel_item: 0,
            edit_text: String::new(),
            edit_item: 0,
            saved_pixels: Vec::new(),
            stack_ptr: TEST_SP,
            item_hit_ptr: 0,
            rendered_pixels: Vec::new(),
            flash_remaining: 0,
            flash_delay: 0,
            flash_item: 0,
            edit_text_modified: false,
            draw_proc_queue: VecDeque::new(),
            draw_procs_done: true,
            rendered_pixels_final: false,
            filter_proc: 0,
            game_managed: false,
            last_filter_event: None,
            popup_draws: vec![DialogPopupDraw {
                rect: (20, 30, 42, 180),
                title: String::new(),
                enabled: true,
                pressed: false,
            }],
            active_popup: None,
            active_button: None,
            active_user_item: None,
        });

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

        let interior = screen_base + 30 * 800 + 80;
        assert_eq!(
            bus.read_byte(interior),
            0,
            "popup interior must be redrawn over the guest userItem pixels"
        );
        assert!(
            disp.dialog_tracking
                .as_ref()
                .is_some_and(|tracking| tracking.rendered_pixels_final),
            "ModalDialog should re-snapshot after popup redraw"
        );
    }

    #[test]
    fn modal_dialog_popup_resnapshot_preserves_theme_pressed_and_inactive_states() {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.set_ui_theme_id(UiThemeId::SystemlessDefault);

        let row_bytes = 64u32;
        let screen_base = bus.alloc(row_bytes * 160);
        for i in 0..row_bytes * 160 {
            bus.write_byte(screen_base + i, 0);
        }
        disp.set_screen_mode_for_test(screen_base, row_bytes, 512, 160, 1);
        cpu.write_reg(Register::A7, TEST_SP);

        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
            dialog_ptr: 0x200000,
            bounds: (0, 0, 120, 220),
            title: String::new(),
            proc_id: 2,
            items: Vec::new(),
            default_item: 0,
            cancel_item: 0,
            edit_text: String::new(),
            edit_item: 0,
            saved_pixels: Vec::new(),
            stack_ptr: TEST_SP,
            item_hit_ptr: 0,
            rendered_pixels: Vec::new(),
            flash_remaining: 0,
            flash_delay: 0,
            flash_item: 0,
            edit_text_modified: false,
            draw_proc_queue: VecDeque::new(),
            draw_procs_done: true,
            rendered_pixels_final: false,
            filter_proc: 0,
            game_managed: false,
            last_filter_event: None,
            popup_draws: vec![
                DialogPopupDraw {
                    rect: (20, 30, 42, 180),
                    title: String::new(),
                    enabled: true,
                    pressed: true,
                },
                DialogPopupDraw {
                    rect: (60, 30, 82, 180),
                    title: String::new(),
                    enabled: false,
                    pressed: false,
                },
            ],
            active_popup: None,
            active_button: None,
            active_user_item: None,
        });

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

        assert!(
            screen_pixel_is_set(&bus, screen_base, row_bytes, 35, 25),
            "pressed popup redraw should preserve the provider pressed fill"
        );
        assert!(
            screen_pixel_is_set(&bus, screen_base, row_bytes, 32, 62),
            "inactive popup redraw should preserve the provider inactive frame"
        );
        assert!(
            disp.dialog_tracking
                .as_ref()
                .is_some_and(|tracking| tracking.rendered_pixels_final),
            "ModalDialog should re-snapshot after stateful popup redraw"
        );
    }

    #[test]
    fn modal_dialog_resnapshot_blits_dialog_port_after_user_item_draw_procs() {
        let (mut disp, mut cpu, mut bus) = setup();
        let screen_base = bus.alloc((32 * 32) as u32);
        for i in 0..32u32 * 32 {
            bus.write_byte(screen_base + i, 0xFF);
        }
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 32, 32, 32, 8);

        let dialog_ptr = bus.alloc(64);
        let offscreen_base = bus.alloc(20 * 20);
        for i in 0..20u32 * 20 {
            bus.write_byte(offscreen_base + i, 0x00);
        }
        let pixmap_handle = bus.alloc(4);
        let pixmap_ptr = bus.alloc(50);
        bus.write_long(pixmap_handle, pixmap_ptr);
        bus.write_long(pixmap_ptr, offscreen_base);
        bus.write_word(pixmap_ptr + 4, 0x8000 | 20);
        bus.write_word(pixmap_ptr + 32, 8);
        bus.write_long(dialog_ptr + 2, pixmap_handle);
        bus.write_word(dialog_ptr + 6, 0xC000);
        bus.write_word(dialog_ptr + 16, 0);
        bus.write_word(dialog_ptr + 18, 0);
        bus.write_word(dialog_ptr + 20, 20);
        bus.write_word(dialog_ptr + 22, 20);

        bus.write_byte(offscreen_base + 7 * 20 + 9, 0x44);
        disp.front_window = dialog_ptr;
        disp.window_bounds = (0, 0, 20, 20);
        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
            dialog_ptr,
            bounds: (0, 0, 20, 20),
            title: String::new(),
            proc_id: 2,
            items: Vec::new(),
            default_item: 0,
            cancel_item: 0,
            edit_text: String::new(),
            edit_item: 0,
            saved_pixels: Vec::new(),
            stack_ptr: TEST_SP,
            item_hit_ptr: 0,
            rendered_pixels: Vec::new(),
            flash_remaining: 0,
            flash_delay: 0,
            flash_item: 0,
            edit_text_modified: false,
            draw_proc_queue: VecDeque::new(),
            draw_procs_done: true,
            rendered_pixels_final: false,
            filter_proc: 0,
            game_managed: false,
            last_filter_event: None,
            popup_draws: Vec::new(),
            active_popup: None,
            active_button: None,
            active_user_item: None,
        });

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

        let screen_pixel = screen_base + 7 * 32 + 9;
        assert_eq!(
            bus.read_byte(screen_pixel),
            0x44,
            "ModalDialog should composite dialog-port userItem pixels before snapshot"
        );
        let tracking = disp.dialog_tracking.as_ref().unwrap();
        assert!(tracking.rendered_pixels_final);
        let snapshot_width = 20 + (TrapDispatcher::DBOX_FRAME_MARGIN as usize * 2);
        let snapshot_height = 20 + (TrapDispatcher::DBOX_FRAME_MARGIN as usize * 2);
        assert_eq!(
            tracking.rendered_pixels.len(),
            snapshot_width * snapshot_height
        );
        let snapshot_index = (7 + TrapDispatcher::DBOX_FRAME_MARGIN as usize) * snapshot_width
            + (9 + TrapDispatcher::DBOX_FRAME_MARGIN as usize);
        assert_eq!(tracking.rendered_pixels[snapshot_index], 0x44);
    }

    #[test]
    fn modal_dialog_update_event_restores_existing_snapshot_before_resnapshot() {
        let (mut disp, mut cpu, mut bus) = setup();
        let screen_base = bus.alloc((32 * 32) as u32);
        for i in 0..32u32 * 32 {
            bus.write_byte(screen_base + i, 0x00);
        }
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 32, 32, 32, 8);

        let dialog_ptr = bus.alloc(64);
        disp.front_window = dialog_ptr;
        disp.window_bounds = (0, 0, 20, 20);

        let snapshot_width = 20 + (TrapDispatcher::DBOX_FRAME_MARGIN as usize * 2);
        let snapshot_height = 20 + (TrapDispatcher::DBOX_FRAME_MARGIN as usize * 2);
        let mut rendered_pixels = vec![0x00; snapshot_width * snapshot_height];
        let snapshot_index = (7 + TrapDispatcher::DBOX_FRAME_MARGIN as usize) * snapshot_width
            + (9 + TrapDispatcher::DBOX_FRAME_MARGIN as usize);
        rendered_pixels[snapshot_index] = 0x44;

        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
            dialog_ptr,
            bounds: (0, 0, 20, 20),
            title: String::new(),
            proc_id: 2,
            items: Vec::new(),
            default_item: 0,
            cancel_item: 0,
            edit_text: String::new(),
            edit_item: 0,
            saved_pixels: Vec::new(),
            stack_ptr: TEST_SP,
            item_hit_ptr: 0,
            rendered_pixels,
            flash_remaining: 0,
            flash_delay: 0,
            flash_item: 0,
            edit_text_modified: false,
            draw_proc_queue: VecDeque::new(),
            draw_procs_done: true,
            rendered_pixels_final: true,
            filter_proc: 0,
            game_managed: false,
            last_filter_event: None,
            popup_draws: Vec::new(),
            active_popup: None,
            active_button: None,
            active_user_item: None,
        });
        disp.event_queue
            .push_back(crate::trap::dispatch::QueuedEvent {
                what: 6,
                message: dialog_ptr,
                where_v: 0,
                where_h: 0,
                modifiers: 0,
            });

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

        let screen_pixel = screen_base + 7 * 32 + 9;
        assert_eq!(
            bus.read_byte(screen_pixel),
            0x44,
            "ModalDialog update events must preserve userItem-rendered dialog pixels"
        );
        let tracking = disp.dialog_tracking.as_ref().unwrap();
        assert_eq!(tracking.rendered_pixels[snapshot_index], 0x44);
    }

    #[test]
    fn finalize_dialog_draw_procs_snapshots_completed_user_item_pixels() {
        let (mut disp, _cpu, mut bus) = setup();
        let screen_base = bus.alloc((32 * 32) as u32);
        for i in 0..32u32 * 32 {
            bus.write_byte(screen_base + i, 0x00);
        }
        bus.write_long(0x0824, screen_base);
        disp.screen_mode = (screen_base, 32, 32, 32, 8);

        let dialog_ptr = bus.alloc(64);
        let screen_pixel = screen_base + 7 * 32 + 9;
        bus.write_byte(screen_pixel, 0x44);
        disp.front_window = dialog_ptr;
        disp.window_bounds = (0, 0, 20, 20);
        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
            dialog_ptr,
            bounds: (0, 0, 20, 20),
            title: String::new(),
            proc_id: 2,
            items: Vec::new(),
            default_item: 0,
            cancel_item: 0,
            edit_text: String::new(),
            edit_item: 0,
            saved_pixels: Vec::new(),
            stack_ptr: TEST_SP,
            item_hit_ptr: 0,
            rendered_pixels: Vec::new(),
            flash_remaining: 0,
            flash_delay: 0,
            flash_item: 0,
            edit_text_modified: false,
            draw_proc_queue: VecDeque::new(),
            draw_procs_done: false,
            rendered_pixels_final: false,
            filter_proc: 0,
            game_managed: false,
            last_filter_event: None,
            popup_draws: Vec::new(),
            active_popup: None,
            active_button: None,
            active_user_item: None,
        });

        disp.finalize_dialog_draw_procs_if_idle(&mut bus);

        let tracking = disp.dialog_tracking.as_ref().unwrap();
        assert!(tracking.draw_procs_done);
        assert!(tracking.rendered_pixels_final);
        let snapshot_width = 20 + (TrapDispatcher::DBOX_FRAME_MARGIN as usize * 2);
        let snapshot_index = (7 + TrapDispatcher::DBOX_FRAME_MARGIN as usize) * snapshot_width
            + (9 + TrapDispatcher::DBOX_FRAME_MARGIN as usize);
        assert_eq!(tracking.rendered_pixels[snapshot_index], 0x44);
    }

    #[test]
    fn modal_dialog_mouse_down_return_consumes_queued_mouse_up() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x200000u32;
        let item_hit_ptr = 0x300000u32;

        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
            dialog_ptr,
            bounds: (100, 200, 200, 360),
            title: String::new(),
            proc_id: 2,
            items: vec![DialogItem {
                item_type: 0,
                rect: (20, 30, 60, 110),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
            default_item: 1,
            cancel_item: 2,
            edit_text: String::new(),
            edit_item: 0,
            saved_pixels: Vec::new(),
            stack_ptr: TEST_SP,
            item_hit_ptr,
            rendered_pixels: Vec::new(),
            flash_remaining: 0,
            flash_delay: 0,
            flash_item: 0,
            edit_text_modified: false,
            draw_proc_queue: VecDeque::new(),
            draw_procs_done: true,
            rendered_pixels_final: true,
            filter_proc: 0,
            game_managed: true,
            last_filter_event: None,
            popup_draws: Vec::new(),
            active_popup: None,
            active_button: None,
            active_user_item: None,
        });
        disp.event_queue
            .push_back(crate::trap::dispatch::QueuedEvent {
                what: 1,
                message: 0,
                where_v: 130,
                where_h: 240,
                modifiers: 0,
            });
        disp.event_queue
            .push_back(crate::trap::dispatch::QueuedEvent {
                what: 2,
                message: 0,
                where_v: 130,
                where_h: 240,
                modifiers: 0x0080,
            });

        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(item_hit_ptr), 1);
        assert!(disp.event_queue.iter().all(|event| event.what != 2));
    }

    #[test]
    fn modal_dialog_plain_user_item_waits_for_mouse_up_before_returning() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x200000u32;
        let item_hit_ptr = 0x300000u32;

        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
            dialog_ptr,
            bounds: (100, 200, 200, 360),
            title: String::new(),
            proc_id: 2,
            items: vec![DialogItem {
                item_type: 0,
                rect: (20, 30, 60, 110),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
            default_item: 1,
            cancel_item: 2,
            edit_text: String::new(),
            edit_item: 0,
            saved_pixels: Vec::new(),
            stack_ptr: TEST_SP,
            item_hit_ptr,
            rendered_pixels: Vec::new(),
            flash_remaining: 0,
            flash_delay: 0,
            flash_item: 0,
            edit_text_modified: false,
            draw_proc_queue: VecDeque::new(),
            draw_procs_done: true,
            rendered_pixels_final: true,
            filter_proc: 0,
            game_managed: false,
            last_filter_event: None,
            popup_draws: Vec::new(),
            active_popup: None,
            active_button: None,
            active_user_item: None,
        });
        disp.mouse_button = true;
        disp.mouse_pos = (130, 240);
        disp.event_queue
            .push_back(crate::trap::dispatch::QueuedEvent {
                what: 1,
                message: 0,
                where_v: 130,
                where_h: 240,
                modifiers: 0,
            });

        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
        assert_eq!(bus.read_word(item_hit_ptr), 0);
        assert!(disp
            .dialog_tracking
            .as_ref()
            .and_then(|tracking| tracking.active_user_item.as_ref())
            .is_some());

        disp.mouse_button = false;
        disp.event_queue
            .push_back(crate::trap::dispatch::QueuedEvent {
                what: 2,
                message: 0,
                where_v: 130,
                where_h: 240,
                modifiers: 0x0080,
            });
        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(item_hit_ptr), 1);
        assert!(disp.event_queue.iter().all(|event| event.what != 2));
    }

    #[test]
    fn modal_dialog_popup_candidate_user_item_returns_on_mouse_down_in_original_rect() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x200000u32;
        let item_hit_ptr = 0x300000u32;

        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
            dialog_ptr,
            bounds: (100, 200, 200, 420),
            title: String::new(),
            proc_id: 2,
            items: vec![DialogItem {
                item_type: 0,
                rect: (20, 90, 40, 110),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
            default_item: 1,
            cancel_item: 2,
            edit_text: String::new(),
            edit_item: 0,
            saved_pixels: Vec::new(),
            stack_ptr: TEST_SP,
            item_hit_ptr,
            rendered_pixels: Vec::new(),
            flash_remaining: 0,
            flash_delay: 0,
            flash_item: 0,
            edit_text_modified: false,
            draw_proc_queue: VecDeque::new(),
            draw_procs_done: true,
            rendered_pixels_final: true,
            filter_proc: 0,
            game_managed: false,
            last_filter_event: None,
            popup_draws: Vec::new(),
            active_popup: None,
            active_button: None,
            active_user_item: None,
        });
        disp.dialog_popup_original_rects
            .insert((dialog_ptr, 1), (20, 30, 40, 150));
        disp.dialog_popup_candidate_items.insert((dialog_ptr, 1));
        disp.mouse_button = true;
        disp.mouse_pos = (130, 260);
        disp.event_queue
            .push_back(crate::trap::dispatch::QueuedEvent {
                what: 1,
                message: 0,
                where_v: 130,
                where_h: 260,
                modifiers: 0,
            });

        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(item_hit_ptr), 1);
        assert_eq!(
            disp.dialog_tracking
                .as_ref()
                .and_then(|tracking| tracking.active_user_item.as_ref())
                .map(|active| active.item_no),
            None,
            "popup-candidate userItems must return on mouse-down so the app can run PopUpMenuSelect"
        );
    }

    #[test]
    fn modal_dialog_button_waits_for_late_mouse_up_before_returning() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x200000u32;
        let item_hit_ptr = 0x300000u32;

        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
            dialog_ptr,
            bounds: (100, 200, 200, 360),
            title: String::new(),
            proc_id: 2,
            items: vec![DialogItem {
                item_type: 4,
                rect: (20, 30, 60, 110),
                text: String::from("OK"),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
            default_item: 1,
            cancel_item: 0,
            edit_text: String::new(),
            edit_item: 0,
            saved_pixels: Vec::new(),
            stack_ptr: TEST_SP,
            item_hit_ptr,
            rendered_pixels: Vec::new(),
            flash_remaining: 0,
            flash_delay: 0,
            flash_item: 0,
            edit_text_modified: false,
            draw_proc_queue: VecDeque::new(),
            draw_procs_done: true,
            rendered_pixels_final: true,
            filter_proc: 0,
            game_managed: true,
            last_filter_event: None,
            popup_draws: Vec::new(),
            active_popup: None,
            active_button: None,
            active_user_item: None,
        });
        disp.mouse_button = true;
        disp.mouse_pos = (130, 240);
        disp.event_queue
            .push_back(crate::trap::dispatch::QueuedEvent {
                what: 1,
                message: 0,
                where_v: 130,
                where_h: 240,
                modifiers: 0,
            });

        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
        assert_eq!(bus.read_word(item_hit_ptr), 0);
        assert!(disp
            .dialog_tracking
            .as_ref()
            .and_then(|tracking| tracking.active_button.as_ref())
            .is_some());

        disp.mouse_button = false;
        disp.event_queue
            .push_back(crate::trap::dispatch::QueuedEvent {
                what: 2,
                message: 0,
                where_v: 130,
                where_h: 240,
                modifiers: 0x0080,
            });
        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert!(disp.event_queue.iter().all(|event| event.what != 2));
        assert!(disp
            .dialog_tracking
            .as_ref()
            .and_then(|tracking| tracking.active_button.as_ref())
            .is_none());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);

        {
            let tracking = disp.dialog_tracking.as_mut().unwrap();
            tracking.flash_remaining = 1;
            tracking.flash_delay = 0;
        }
        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(item_hit_ptr), 1);
    }

    #[test]
    fn modal_dialog_filter_handled_mouse_down_consumes_queued_mouse_up() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x200000u32;
        let item_hit_ptr = 0x300000u32;
        let result_addr = 0x300100u32;

        bus.write_word(result_addr, 0xFFFF);
        bus.write_word(item_hit_ptr, 12);
        disp.dialog_filter_result_addr = result_addr;
        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
            dialog_ptr,
            bounds: (100, 200, 200, 360),
            title: String::new(),
            proc_id: 2,
            items: vec![DialogItem {
                item_type: 0,
                rect: (20, 30, 60, 110),
                text: String::new(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
            default_item: 1,
            cancel_item: 2,
            edit_text: String::new(),
            edit_item: 0,
            saved_pixels: Vec::new(),
            stack_ptr: TEST_SP,
            item_hit_ptr,
            rendered_pixels: Vec::new(),
            flash_remaining: 0,
            flash_delay: 0,
            flash_item: 0,
            edit_text_modified: false,
            draw_proc_queue: VecDeque::new(),
            draw_procs_done: true,
            rendered_pixels_final: true,
            filter_proc: 0x149F0,
            game_managed: true,
            last_filter_event: Some(crate::trap::dispatch::QueuedEvent {
                what: 1,
                message: 0,
                where_v: 130,
                where_h: 240,
                modifiers: 0,
            }),
            popup_draws: Vec::new(),
            active_popup: None,
            active_button: None,
            active_user_item: None,
        });
        disp.event_queue
            .push_back(crate::trap::dispatch::QueuedEvent {
                what: 2,
                message: 0,
                where_v: 130,
                where_h: 240,
                modifiers: 0x0080,
            });

        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert!(disp.event_queue.iter().all(|event| event.what != 2));
    }

    #[test]
    fn modal_dialog_return_key_activates_default_button_after_filter_false() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x200000u32;
        let item_hit_ptr = 0x300000u32;
        let result_addr = 0x300100u32;

        bus.write_word(result_addr, 0);
        bus.write_word(item_hit_ptr, 0);
        disp.dialog_filter_result_addr = result_addr;
        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
            dialog_ptr,
            bounds: (100, 200, 200, 360),
            title: String::new(),
            proc_id: 2,
            items: vec![DialogItem {
                item_type: 4,
                rect: (20, 30, 60, 110),
                text: String::from("OK"),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
            default_item: 1,
            cancel_item: 0,
            edit_text: String::new(),
            edit_item: 0,
            saved_pixels: Vec::new(),
            stack_ptr: TEST_SP,
            item_hit_ptr,
            rendered_pixels: Vec::new(),
            flash_remaining: 0,
            flash_delay: 0,
            flash_item: 0,
            edit_text_modified: false,
            draw_proc_queue: VecDeque::new(),
            draw_procs_done: true,
            rendered_pixels_final: true,
            filter_proc: 0x149F0,
            game_managed: true,
            last_filter_event: Some(crate::trap::dispatch::QueuedEvent {
                what: 3,
                message: 0x0000_240D,
                where_v: 130,
                where_h: 240,
                modifiers: 0,
            }),
            popup_draws: Vec::new(),
            active_popup: None,
            active_button: None,
            active_user_item: None,
        });

        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
        assert_eq!(bus.read_word(item_hit_ptr), 0);
        {
            let tracking = disp.dialog_tracking.as_ref().unwrap();
            assert_eq!(tracking.flash_item, 1);
            assert!(tracking.flash_remaining > 0);
        }

        {
            let tracking = disp.dialog_tracking.as_mut().unwrap();
            tracking.flash_remaining = 1;
            tracking.flash_delay = 0;
        }
        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(item_hit_ptr), 1);
        assert!(disp.dialog_tracking.is_none());
    }

    #[test]
    fn modal_dialog_filter_true_zero_hit_returns_to_app() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x200000u32;
        let item_hit_ptr = 0x300000u32;
        let result_addr = 0x300100u32;

        bus.write_word(result_addr, 0xFFFF);
        bus.write_word(item_hit_ptr, 0);
        disp.dialog_filter_result_addr = result_addr;
        disp.dialog_tracking = Some(crate::trap::dispatch::DialogTrackingState {
            dialog_ptr,
            bounds: (100, 200, 200, 360),
            title: String::new(),
            proc_id: 2,
            items: vec![DialogItem {
                item_type: 4,
                rect: (20, 30, 60, 110),
                text: String::from("OK"),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
            default_item: 1,
            cancel_item: 2,
            edit_text: String::new(),
            edit_item: 0,
            saved_pixels: Vec::new(),
            stack_ptr: TEST_SP,
            item_hit_ptr,
            rendered_pixels: Vec::new(),
            flash_remaining: 0,
            flash_delay: 0,
            flash_item: 0,
            edit_text_modified: false,
            draw_proc_queue: VecDeque::new(),
            draw_procs_done: true,
            rendered_pixels_final: true,
            filter_proc: 0x149F0,
            game_managed: false,
            last_filter_event: Some(crate::trap::dispatch::QueuedEvent {
                what: 3,
                message: 0x0000_240D,
                where_v: 130,
                where_h: 240,
                modifiers: 0,
            }),
            popup_draws: Vec::new(),
            active_popup: None,
            active_button: None,
            active_user_item: None,
        });

        let result = disp.dispatch_dialog(true, 0x191, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(item_hit_ptr), 0);
        assert!(disp.dialog_tracking.is_none());
    }

    // ---- TEInit ($A9CC) ----

    #[test]
    fn teinit_first_call_allocates_empty_scrap_handle_and_zeros_length() {
        // IM:I I-376 + I-389: TEInit creates an empty TextEdit scrap
        // handle and sets TEScrpLength to 0.
        let (mut disp, mut cpu, mut bus) = setup();
        let sp_before = cpu.read_reg(Register::A7);
        bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 0xFFFF);
        bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, 0);

        let result = disp.dispatch_dialog(true, 0x1CC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp_before);

        let scrap_handle = bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE);
        assert_ne!(scrap_handle, 0, "TEInit must allocate TEScrpHandle");
        assert_eq!(
            bus.read_long(scrap_handle),
            0,
            "TEInit scrap handle should initially reference empty data"
        );
        assert_eq!(
            bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH),
            0
        );
    }

    #[test]
    fn teinit_reuses_existing_scrap_handle_and_resets_length() {
        // IM:I I-376 + I-389: TEInit restores empty-scrap state; HLE must
        // keep an existing handle stable to avoid churn/leaks on repeat calls.
        let (mut disp, mut cpu, mut bus) = setup();
        let existing_handle = TrapDispatcher::allocate_handle_with_data(&mut bus, 3);
        let existing_ptr = bus.read_long(existing_handle);
        bus.write_bytes(existing_ptr, b"xyz");
        bus.write_long(
            crate::memory::globals::addr::TE_SCRP_HANDLE,
            existing_handle,
        );
        bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 3);

        let result = disp.dispatch_dialog(true, 0x1CC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
        assert_eq!(
            bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE),
            existing_handle
        );
        assert_eq!(
            bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH),
            0
        );
        assert_eq!(bus.read_long(existing_handle), existing_ptr);
        assert_eq!(bus.read_bytes(existing_ptr, 3), b"xyz".to_vec());
    }

    fn make_te_with_text(
        disp: &mut TrapDispatcher,
        bus: &mut crate::memory::MacMemoryBus,
        text: &[u8],
    ) -> u32 {
        let te_handle = TrapDispatcher::allocate_te_handle(bus);
        disp.current_port = 0x181000;
        disp.tx_font = 4;
        disp.tx_face = 0;
        disp.tx_mode = 0;
        disp.tx_size = 10;
        disp.initialize_te_record(bus, te_handle, (0, 0, 60, 160), (0, 0, 60, 160));
        disp.te_set_text_contents(bus, te_handle, text);
        te_handle
    }

    // ---- TENew ----

    #[test]
    fn tenew_pointer_arg_convention_initializes_destrect_viewrect_and_returns_non_nil_handle() {
        // IM:I I-373..I-374 + TextEdit.h ONEWORDINLINE(0xA9D2):
        // FUNCTION TENew(destRect, viewRect: Rect): TEHandle. Modern MPW
        // Universal Headers pass two `const Rect *` pointers (8 bytes of
        // args). After dispatch (**hTE).destRect and (**hTE).viewRect
        // must round-trip the caller's input rects exactly. Defeats stubs
        // that swap the args, zero the rects, or copy only a subset of
        // the four 2-byte fields.
        let (mut disp, mut cpu, mut bus) = setup();

        // Build two DISTINCT Rect inputs in guest memory.
        let dest_rect_ptr: u32 = 0x190000;
        bus.write_word(dest_rect_ptr, (-1000i16) as u16); // top
        bus.write_word(dest_rect_ptr + 2, (-1000i16) as u16); // left
        bus.write_word(dest_rect_ptr + 4, (-700i16) as u16); // bottom
        bus.write_word(dest_rect_ptr + 6, (-800i16) as u16); // right
        let view_rect_ptr: u32 = 0x190010;
        bus.write_word(view_rect_ptr, (-900i16) as u16); // top
        bus.write_word(view_rect_ptr + 2, (-1100i16) as u16); // left
        bus.write_word(view_rect_ptr + 4, (-600i16) as u16); // bottom
        bus.write_word(view_rect_ptr + 6, (-700i16) as u16); // right

        // Pascal FUNCTION stack frame (pointer-arg convention).
        // Pascal pushes args left-to-right, so the FIRST arg (destRect_ptr)
        // is DEEPEST on stack at trap entry. With 4-byte pointers:
        //   sp+0..3   viewRect_ptr  (last pushed, shallowest)
        //   sp+4..7   destRect_ptr  (first pushed, deepest)
        //   sp+8..11  TEHandle result slot (poisoned)
        bus.write_long(TEST_SP, view_rect_ptr);
        bus.write_long(TEST_SP + 4, dest_rect_ptr);
        bus.write_long(TEST_SP + 8, 0xDEAD_BEEF);

        let result = disp.dispatch_dialog(true, 0x1D2, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);

        let te_handle = bus.read_long(TEST_SP + 8);
        assert_ne!(te_handle, 0, "TENew returned NIL TEHandle");
        let te_ptr = bus.read_long(te_handle);
        assert_ne!(te_ptr, 0, "TEHandle master pointer is NIL");

        // destRect round-trip (top, left, bottom, right at offset 0..6).
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET) as i16,
            -1000
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2) as i16,
            -1000
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4) as i16,
            -700
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6) as i16,
            -800
        );

        // viewRect round-trip (top, left, bottom, right at offset 8..14).
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET) as i16,
            -900
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 2) as i16,
            -1100
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4) as i16,
            -600
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6) as i16,
            -700
        );
    }

    #[test]
    fn tenew_fresh_terec_has_zero_telength_and_empty_selection_per_im_i_373() {
        // IM:I I-373: a fresh TERec has no text and an empty selection
        // range. Witness teLength == 0, selStart == 0, selEnd == 0, and
        // hText is a non-NIL Handle.
        let (mut disp, mut cpu, mut bus) = setup();

        let rect_ptr: u32 = 0x190020;
        bus.write_word(rect_ptr, 10); // top
        bus.write_word(rect_ptr + 2, 20); // left
        bus.write_word(rect_ptr + 4, 110); // bottom
        bus.write_word(rect_ptr + 6, 220); // right

        bus.write_long(TEST_SP, rect_ptr);
        bus.write_long(TEST_SP + 4, rect_ptr);

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

        let te_handle = bus.read_long(TEST_SP + 8);
        let te_ptr = bus.read_long(te_handle);

        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET),
            0,
            "fresh TERec must have teLength == 0"
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            0,
            "fresh TERec must have selStart == 0"
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
            0,
            "fresh TERec must have selEnd == 0"
        );
        let h_text = bus.read_long(te_ptr + TrapDispatcher::TE_HTEXT_OFFSET);
        assert_ne!(h_text, 0, "fresh TERec must have a non-NIL hText handle");
    }

    #[test]
    fn tenew_function_protocol_consumes_two_pointer_args_and_writes_4_byte_result() {
        // Pascal FUNCTION calling convention: 2 pointer args (8 bytes)
        // are popped, the 4-byte TEHandle result is written into the
        // caller's pre-allocated slot at the former SP+8. Sentinels
        // around the result slot must survive the trap call.
        let (mut disp, mut cpu, mut bus) = setup();

        let rect_ptr: u32 = 0x190030;
        bus.write_word(rect_ptr, 0);
        bus.write_word(rect_ptr + 2, 0);
        bus.write_word(rect_ptr + 4, 100);
        bus.write_word(rect_ptr + 6, 200);

        bus.write_long(TEST_SP, rect_ptr);
        bus.write_long(TEST_SP + 4, rect_ptr);
        bus.write_long(TEST_SP + 8, 0xDEAD_BEEF);
        bus.write_long(TEST_SP + 12, 0xCAFE_BABE); // sentinel past result

        let result = disp.dispatch_dialog(true, 0x1D2, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);

        let te_handle = bus.read_long(TEST_SP + 8);
        assert_ne!(te_handle, 0xDEAD_BEEF, "result slot must be overwritten");
        assert_ne!(te_handle, 0);
        assert_eq!(
            bus.read_long(TEST_SP + 12),
            0xCAFE_BABE,
            "sentinel past 4-byte result slot must survive"
        );
    }

    // ---- TEDispose / TECalText / TESetSelect / TEDelete ----

    #[test]
    fn tedispose_releases_te_record_text_handle_and_pops_arg() {
        // IM:I I-383..I-384.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let te_ptr = bus.read_long(te_handle);
        let text_handle = bus.read_long(te_ptr + TrapDispatcher::TE_HTEXT_OFFSET);
        let text_ptr = bus.read_long(text_handle);
        assert_ne!(te_handle, 0);
        assert_ne!(te_ptr, 0);
        assert_ne!(text_handle, 0);
        assert_ne!(text_ptr, 0);

        bus.write_long(TEST_SP, te_handle);
        let result = disp.dispatch_dialog(true, 0x1CD, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        assert_eq!(bus.get_alloc_size(text_ptr), None);
        assert_eq!(bus.get_alloc_size(text_handle), None);
        assert_eq!(bus.get_alloc_size(te_ptr), None);
        assert_eq!(bus.get_alloc_size(te_handle), None);
    }

    #[test]
    fn tecaltext_recomputes_line_metadata_and_pops_arg() {
        // IM:I I-390: lineStarts are recomputed from current text layout.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
        let te_ptr = bus.read_long(te_handle);
        let text_len = TrapDispatcher::te_text_bytes(&bus, te_handle).len() as u16;
        assert_eq!(text_len, 32);

        // Force stale metadata and a narrow destination width so wrapped
        // layout produces multiple lines.
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 12);
        bus.write_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET, 0);
        bus.write_word(te_ptr + TrapDispatcher::TE_N_LINES_OFFSET, 0x7777);
        bus.write_word(te_ptr + TrapDispatcher::TE_LINE_STARTS_OFFSET, 0x7777);

        bus.write_long(TEST_SP, te_handle);
        let result = disp.dispatch_dialog(true, 0x1D0, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        let te_ptr = bus.read_long(te_handle);
        let n_lines = bus.read_word(te_ptr + TrapDispatcher::TE_N_LINES_OFFSET);
        assert_ne!(n_lines, 0x7777);
        assert!(n_lines > 1);
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET),
            text_len
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_LINE_STARTS_OFFSET),
            0
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_LINE_STARTS_OFFSET + u32::from(n_lines) * 2),
            text_len
        );
    }

    #[test]
    fn tegettext_returns_htext_handle_from_terec_and_pops_te_handle_arg() {
        // IM:I I-384: "TEGetText returns a handle to the text of the
        // specified edit record. The result is the same as the handle in
        // the hText field of the edit record."
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let te_ptr = bus.read_long(te_handle);
        let expected_h_text = bus.read_long(te_ptr + TrapDispatcher::TE_HTEXT_OFFSET);
        assert_ne!(expected_h_text, 0);

        bus.write_long(TEST_SP, te_handle);
        bus.write_long(TEST_SP + 4, 0xDEAD_BEEF); // poison result slot
        let result = disp.dispatch_dialog(true, 0x1CB, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        // The returned CharsHandle equals (**hTE).hText exactly.
        assert_eq!(bus.read_long(TEST_SP + 4), expected_h_text);

        // The handle dereferences to the 5-byte payload "HELLO".
        let text_ptr = bus.read_long(expected_h_text);
        assert_ne!(text_ptr, 0);
        assert_eq!(bus.read_bytes(text_ptr, 5), b"HELLO".to_vec());
    }

    #[test]
    fn tegettext_returns_nil_handle_when_te_handle_is_zero() {
        // Defensive: a NIL TEHandle has no TERec to read hText from, so
        // the result is NIL. Guards against any future change that would
        // crash or read past invalid memory when handed a NIL argument.
        let (mut disp, mut cpu, mut bus) = setup();

        bus.write_long(TEST_SP, 0); // NIL TEHandle
        bus.write_long(TEST_SP + 4, 0xCAFE_BABE); // poison result slot
        let result = disp.dispatch_dialog(true, 0x1CB, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_long(TEST_SP + 4), 0);
    }

    #[test]
    fn tesetselect_clamps_selend_to_text_length_and_pops_args() {
        // IM:I I-385: selEnd past end-of-text clamps to teLength.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let te_ptr = bus.read_long(te_handle);
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET), 5);

        bus.write_long(TEST_SP, te_handle); // hTE
        bus.write_long(TEST_SP + 4, 999); // selEnd
        bus.write_long(TEST_SP + 8, 2); // selStart
        let result = disp.dispatch_dialog(true, 0x1D1, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);

        let te_ptr = bus.read_long(te_handle);
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            2
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 5);
    }

    #[test]
    fn tedelete_removes_selection_without_touching_scrap_and_pops_arg() {
        // IM:I I-387: TEDelete deletes selection without writing TextEdit scrap.
        let (mut disp, mut cpu, mut bus) = setup();
        let existing_scrap = TrapDispatcher::allocate_handle_with_data(&mut bus, 2);
        let existing_ptr = bus.read_long(existing_scrap);
        bus.write_bytes(existing_ptr, b"QQ");
        bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, existing_scrap);
        bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 2);

        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);

        bus.write_long(TEST_SP, te_handle);
        let result = disp.dispatch_dialog(true, 0x1D7, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(
            TrapDispatcher::te_text_bytes(&bus, te_handle),
            b"HO".to_vec()
        );

        let te_ptr = bus.read_long(te_handle);
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            1
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 1);
        assert_eq!(
            bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE),
            existing_scrap
        );
        assert_eq!(
            bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH),
            2
        );
        assert_eq!(bus.read_bytes(existing_ptr, 2), b"QQ".to_vec());
    }

    #[test]
    fn tedelete_insertion_point_selection_is_noop_and_pops_arg() {
        // IM:I I-387: insertion-point selection means "nothing happens".
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 3);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 3);

        bus.write_long(TEST_SP, te_handle);
        let result = disp.dispatch_dialog(true, 0x1D7, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(
            TrapDispatcher::te_text_bytes(&bus, te_handle),
            b"HELLO".to_vec()
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            3
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 3);
    }

    #[test]
    fn teupdate_pascal_form_redraws_text_and_pops_inline_rect_and_tehandle() {
        // IM:I I-387; Inside Macintosh: Text 1993, 2-88.
        let (mut disp, mut cpu, mut bus) = setup_with_port();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let (screen_base, row_bytes, _screen_w, _screen_h, _pixel_size) = disp.screen_mode;
        for i in 0..(row_bytes * 80) {
            bus.write_byte(screen_base + i, 0);
        }

        bus.write_long(TEST_SP, te_handle);
        bus.write_word(TEST_SP + 4, 0); // rUpdate.top
        bus.write_word(TEST_SP + 6, 0); // rUpdate.left
        bus.write_word(TEST_SP + 8, 40); // rUpdate.bottom
        bus.write_word(TEST_SP + 10, 120); // rUpdate.right

        let result = disp.dispatch_dialog(true, 0x1D3, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
        assert_eq!(
            TrapDispatcher::te_text_bytes(&bus, te_handle),
            b"HELLO".to_vec()
        );
        let drew_any_pixel =
            (0..(row_bytes * 80)).any(|offset| bus.read_byte(screen_base + offset) != 0);
        assert!(
            drew_any_pixel,
            "TEUpdate should draw at least one non-white pixel for visible text"
        );
    }

    #[test]
    fn teupdate_systemless_theme_routes_active_caret_through_provider() {
        let (mut classic, mut classic_cpu, mut classic_bus) = setup_with_port();
        let classic_te_handle = make_te_with_text(&mut classic, &mut classic_bus, b"");
        let classic_te_ptr = classic_bus.read_long(classic_te_handle);
        classic_bus.write_word(classic_te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 1);
        classic_bus.write_word(classic_te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
        classic_bus.write_word(classic_te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 0);
        let (screen_base, row_bytes, _screen_w, _screen_h, _pixel_size) = classic.screen_mode;
        for i in 0..(row_bytes * 80) {
            classic_bus.write_byte(screen_base + i, 0);
        }
        classic_bus.write_long(TEST_SP, classic_te_handle);
        classic_bus.write_word(TEST_SP + 4, 0);
        classic_bus.write_word(TEST_SP + 6, 0);
        classic_bus.write_word(TEST_SP + 8, 40);
        classic_bus.write_word(TEST_SP + 10, 120);
        let result = classic.dispatch_dialog(true, 0x1D3, &mut classic_cpu, &mut classic_bus);
        assert!(result.unwrap().is_ok());

        let (mut themed, mut themed_cpu, mut themed_bus) = setup_with_port();
        themed.set_ui_theme_id(UiThemeId::SystemlessDefault);
        let themed_te_handle = make_te_with_text(&mut themed, &mut themed_bus, b"");
        let themed_te_ptr = themed_bus.read_long(themed_te_handle);
        themed_bus.write_word(themed_te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 1);
        themed_bus.write_word(themed_te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
        themed_bus.write_word(themed_te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 0);
        for i in 0..(row_bytes * 80) {
            themed_bus.write_byte(screen_base + i, 0);
        }
        themed_bus.write_long(TEST_SP, themed_te_handle);
        themed_bus.write_word(TEST_SP + 4, 0);
        themed_bus.write_word(TEST_SP + 6, 0);
        themed_bus.write_word(TEST_SP + 8, 40);
        themed_bus.write_word(TEST_SP + 10, 120);
        let result = themed.dispatch_dialog(true, 0x1D3, &mut themed_cpu, &mut themed_bus);
        assert!(result.unwrap().is_ok());

        assert!(
            screen_pixel_is_set(&classic_bus, screen_base, row_bytes, 1, 0),
            "classic TEUpdate should draw a one-pixel caret for an active empty insertion point"
        );
        assert!(
            screen_pixel_is_set(&themed_bus, screen_base, row_bytes, 0, 0),
            "systemless-default TEUpdate should draw provider-owned caret chrome"
        );
    }

    #[test]
    fn teactivate_and_tedeactivate_repaint_empty_insertion_caret() {
        // IM:I I-385 and Text 1993 p. 2-80: TEActivate displays the caret
        // when the active selection is an insertion point; TEDeactivate
        // removes it without changing the selection offsets.
        let (mut disp, mut cpu, mut bus) = setup_with_port();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"");
        let te_ptr = bus.read_long(te_handle);
        let (screen_base, row_bytes, _screen_w, _screen_h, _pixel_size) = disp.screen_mode;
        for i in 0..(row_bytes * 80) {
            bus.write_byte(screen_base + i, 0);
        }
        bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 0);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 0);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, te_handle);
        let result = disp.dispatch_dialog(true, 0x1D8, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET), 1);
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            0
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 0);
        assert!(
            screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
            "TEActivate should paint the visible insertion caret"
        );

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, te_handle);
        let result = disp.dispatch_dialog(true, 0x1D9, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET), 0);
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            0
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 0);
        assert!(
            !screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
            "TEDeactivate should erase the insertion caret"
        );
    }

    #[test]
    fn teupdate_classic_active_selection_inverts_selected_text_range() {
        fn render_classic_selection(selected: bool) -> u64 {
            let (mut disp, mut cpu, mut bus) = setup_with_port();
            let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
            let te_ptr = bus.read_long(te_handle);
            bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 1);
            bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
            bus.write_word(
                te_ptr + TrapDispatcher::TE_SEL_END_OFFSET,
                if selected { 3 } else { 0 },
            );
            let line_height = bus.read_word(te_ptr + TrapDispatcher::TE_LINE_HEIGHT_OFFSET) as i16;
            let port = bus.read_long(te_ptr + TrapDispatcher::TE_IN_PORT_OFFSET);
            let base = bus.read_long(port + 2);
            let row_bytes = u32::from(bus.read_word(port + 6) & 0x3FFF);
            for i in 0..(row_bytes * 80) {
                bus.write_byte(base + i, 0);
            }
            bus.write_long(TEST_SP, te_handle);
            bus.write_word(TEST_SP + 4, 0);
            bus.write_word(TEST_SP + 6, 0);
            bus.write_word(TEST_SP + 8, 40);
            bus.write_word(TEST_SP + 10, 120);
            let result = disp.dispatch_dialog(true, 0x1D3, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            sum_screen_bytes(&bus, base, row_bytes, 0, 0, line_height, 32)
        }

        // IM:I I-375/I-385 and Text 1993 p. 2-80: active non-empty TextEdit
        // selections are highlighted. Classic highlighting is inverse video
        // over the selected character range, so the selected render must
        // differ from the same active insertion-point redraw.
        let selected = render_classic_selection(true);
        let unselected = render_classic_selection(false);
        assert!(
            selected != unselected,
            "classic TEUpdate should invert active non-empty selection pixels; selected={selected}, unselected={unselected}"
        );
    }

    #[test]
    fn teupdate_systemless_theme_routes_multiline_selection_through_provider() {
        fn render_themed_multiline_selection(selected: bool) -> (u16, u16, i16, u64, u64) {
            let (mut disp, mut cpu, mut bus) = setup_with_port();
            disp.set_ui_theme_id(UiThemeId::SystemlessDefault);
            let te_handle = make_te_with_text(&mut disp, &mut bus, b"A\rB");
            let te_ptr = bus.read_long(te_handle);
            bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 1);
            bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
            bus.write_word(
                te_ptr + TrapDispatcher::TE_SEL_END_OFFSET,
                if selected { 3 } else { 0 },
            );
            let line_height = bus.read_word(te_ptr + TrapDispatcher::TE_LINE_HEIGHT_OFFSET) as i16;
            let (base, row_bytes, _screen_w, _screen_h, _pixel_size) = disp.screen_mode;
            for i in 0..(row_bytes * 80) {
                bus.write_byte(base + i, 0);
            }
            bus.write_long(TEST_SP, te_handle);
            bus.write_word(TEST_SP + 4, 0);
            bus.write_word(TEST_SP + 6, 0);
            bus.write_word(TEST_SP + 8, 40);
            bus.write_word(TEST_SP + 10, 120);
            let result = disp.dispatch_dialog(true, 0x1D3, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());

            let first_line = sum_screen_bytes(&bus, base, row_bytes, 0, 0, line_height, 24);
            let second_line =
                sum_screen_bytes(&bus, base, row_bytes, line_height, 0, line_height * 2, 24);
            (
                bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
                bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
                line_height,
                first_line,
                second_line,
            )
        }

        let selected = render_themed_multiline_selection(true);
        let unselected = render_themed_multiline_selection(false);

        // Text 1993, appendix C: TERec.active marks whether the selection
        // range is highlighted or the caret is displayed; selStart/selEnd are
        // byte offsets into the contiguous selection range.
        assert_eq!((selected.0, selected.1), (0, 3));
        assert_eq!((unselected.0, unselected.1), (0, 0));
        assert_eq!(selected.2, unselected.2);
        assert!(
            selected.4 != unselected.4,
            "systemless-default TEUpdate should draw provider selection chrome on the second selected line"
        );
    }

    #[test]
    fn teupdate_systemless_theme_renders_reversed_selection_without_rewriting_offsets() {
        fn render_themed_selection(sel_start: u16, sel_end: u16) -> (u16, u16, u16, i16, u32, u32) {
            let (mut disp, mut cpu, mut bus) = setup_with_port();
            disp.set_ui_theme_id(UiThemeId::SystemlessDefault);
            let te_handle = make_te_with_text(&mut disp, &mut bus, b"A\rB");
            let te_ptr = bus.read_long(te_handle);
            bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 1);
            bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, sel_start);
            bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, sel_end);
            let line_height = bus.read_word(te_ptr + TrapDispatcher::TE_LINE_HEIGHT_OFFSET) as i16;
            let (base, row_bytes, _screen_w, _screen_h, _pixel_size) = disp.screen_mode;
            for i in 0..(row_bytes * 80) {
                bus.write_byte(base + i, 0);
            }

            bus.write_long(TEST_SP, te_handle);
            bus.write_word(TEST_SP + 4, 0);
            bus.write_word(TEST_SP + 6, 0);
            bus.write_word(TEST_SP + 8, 40);
            bus.write_word(TEST_SP + 10, 120);
            let result = disp.dispatch_dialog(true, 0x1D3, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);

            let first_line_bottom_band =
                count_set_pixels(&bus, base, row_bytes, line_height - 2, 0, line_height, 24);
            let second_line_bottom_band = count_set_pixels(
                &bus,
                base,
                row_bytes,
                line_height * 2 - 2,
                0,
                line_height * 2,
                24,
            );

            (
                bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET),
                bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
                bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
                line_height,
                first_line_bottom_band,
                second_line_bottom_band,
            )
        }

        let forward = render_themed_selection(0, 3);
        let reversed = render_themed_selection(3, 0);

        // IM:I I-385 says TESetSelect owns selection-range changes, while
        // IM:I I-387 / Text 1993 p. 2-88 define TEUpdate as redraw. The theme
        // path can normalize reversed endpoints for drawing but must not
        // rewrite the guest TERec.
        assert_eq!(forward.0, 3);
        assert_eq!(reversed.0, 3);
        assert_eq!((forward.1, forward.2), (0, 3));
        assert_eq!((reversed.1, reversed.2), (3, 0));
        assert_eq!(forward.3, reversed.3);
        assert!(forward.4 > 0 && forward.5 > 0);
        assert_eq!(
            (reversed.4, reversed.5),
            (forward.4, forward.5),
            "systemless-default TEUpdate should render reversed selection ranges like their normalized range without rewriting selStart/selEnd"
        );
    }

    #[test]
    fn teupdate_systemless_theme_routes_inactive_outline_selection_through_provider() {
        let (mut inactive_plain, mut inactive_plain_cpu, mut inactive_plain_bus) =
            setup_with_port();
        inactive_plain.set_ui_theme_id(UiThemeId::SystemlessDefault);
        let inactive_plain_te_handle =
            make_te_with_text(&mut inactive_plain, &mut inactive_plain_bus, b"HELLO");
        let inactive_plain_te_ptr = inactive_plain_bus.read_long(inactive_plain_te_handle);
        inactive_plain_bus.write_word(inactive_plain_te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 0);
        inactive_plain_bus.write_word(
            inactive_plain_te_ptr + TrapDispatcher::TE_SEL_START_OFFSET,
            0,
        );
        inactive_plain_bus.write_word(inactive_plain_te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 3);
        let (plain_base, plain_row_bytes, _screen_w, _screen_h, _pixel_size) =
            inactive_plain.screen_mode;
        for i in 0..(plain_row_bytes * 80) {
            inactive_plain_bus.write_byte(plain_base + i, 0);
        }
        inactive_plain_bus.write_long(TEST_SP, inactive_plain_te_handle);
        inactive_plain_bus.write_word(TEST_SP + 4, 0);
        inactive_plain_bus.write_word(TEST_SP + 6, 0);
        inactive_plain_bus.write_word(TEST_SP + 8, 40);
        inactive_plain_bus.write_word(TEST_SP + 10, 120);
        let result = inactive_plain.dispatch_dialog(
            true,
            0x1D3,
            &mut inactive_plain_cpu,
            &mut inactive_plain_bus,
        );
        assert!(result.unwrap().is_ok());

        let (mut inactive_outline, mut inactive_outline_cpu, mut inactive_outline_bus) =
            setup_with_port();
        inactive_outline.set_ui_theme_id(UiThemeId::SystemlessDefault);
        let inactive_outline_te_handle =
            make_te_with_text(&mut inactive_outline, &mut inactive_outline_bus, b"HELLO");
        let inactive_outline_te_ptr = inactive_outline_bus.read_long(inactive_outline_te_handle);
        inactive_outline_bus.write_word(
            inactive_outline_te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET,
            0,
        );
        inactive_outline_bus.write_word(
            inactive_outline_te_ptr + TrapDispatcher::TE_SEL_START_OFFSET,
            0,
        );
        inactive_outline_bus.write_word(
            inactive_outline_te_ptr + TrapDispatcher::TE_SEL_END_OFFSET,
            3,
        );
        let inactive_line_height = inactive_outline_bus
            .read_word(inactive_outline_te_ptr + TrapDispatcher::TE_LINE_HEIGHT_OFFSET)
            as i16;
        let (outline_base, outline_row_bytes, _screen_w, _screen_h, _pixel_size) =
            inactive_outline.screen_mode;
        for i in 0..(outline_row_bytes * 80) {
            inactive_outline_bus.write_byte(outline_base + i, 0);
        }

        inactive_outline_cpu.write_reg(Register::A7, TEST_SP);
        inactive_outline_bus.write_word(TEST_SP, 0x000E);
        inactive_outline_bus.write_long(TEST_SP + 2, inactive_outline_te_handle);
        inactive_outline_bus.write_word(TEST_SP + 6, TrapDispatcher::TE_BIT_SET as u16);
        inactive_outline_bus.write_word(TEST_SP + 8, TrapDispatcher::TE_FEATURE_OUTLINE_HILITE);
        inactive_outline_bus.write_word(TEST_SP + 10, 0xBEEF);
        let result = inactive_outline.dispatch_dialog(
            true,
            0x03D,
            &mut inactive_outline_cpu,
            &mut inactive_outline_bus,
        );
        assert!(result.unwrap().is_ok());
        assert_eq!(inactive_outline_bus.read_word(TEST_SP + 10), 0);

        inactive_outline_cpu.write_reg(Register::A7, TEST_SP);
        inactive_outline_bus.write_long(TEST_SP, inactive_outline_te_handle);
        inactive_outline_bus.write_word(TEST_SP + 4, 0);
        inactive_outline_bus.write_word(TEST_SP + 6, 0);
        inactive_outline_bus.write_word(TEST_SP + 8, 40);
        inactive_outline_bus.write_word(TEST_SP + 10, 120);
        let result = inactive_outline.dispatch_dialog(
            true,
            0x1D3,
            &mut inactive_outline_cpu,
            &mut inactive_outline_bus,
        );
        assert!(result.unwrap().is_ok());

        let (mut active, mut active_cpu, mut active_bus) = setup_with_port();
        active.set_ui_theme_id(UiThemeId::SystemlessDefault);
        let active_te_handle = make_te_with_text(&mut active, &mut active_bus, b"HELLO");
        let active_te_ptr = active_bus.read_long(active_te_handle);
        active_bus.write_word(active_te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 1);
        active_bus.write_word(active_te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
        active_bus.write_word(active_te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 3);
        let active_line_height =
            active_bus.read_word(active_te_ptr + TrapDispatcher::TE_LINE_HEIGHT_OFFSET) as i16;
        let (active_base, active_row_bytes, _screen_w, _screen_h, _pixel_size) = active.screen_mode;
        for i in 0..(active_row_bytes * 80) {
            active_bus.write_byte(active_base + i, 0);
        }
        active_bus.write_long(TEST_SP, active_te_handle);
        active_bus.write_word(TEST_SP + 4, 0);
        active_bus.write_word(TEST_SP + 6, 0);
        active_bus.write_word(TEST_SP + 8, 40);
        active_bus.write_word(TEST_SP + 10, 120);
        let result = active.dispatch_dialog(true, 0x1D3, &mut active_cpu, &mut active_bus);
        assert!(result.unwrap().is_ok());

        // Text 1993 describes outline highlighting as framing an inactive
        // selection; TEActivate/TEDeactivate tie it to teFOutlineHilite.
        let inactive_plain_top_band = count_set_pixels(
            &inactive_plain_bus,
            plain_base,
            plain_row_bytes,
            0,
            0,
            1,
            24,
        );
        let inactive_outline_top_band = count_set_pixels(
            &inactive_outline_bus,
            outline_base,
            outline_row_bytes,
            0,
            0,
            1,
            24,
        );
        assert_eq!(
            inactive_plain_top_band, 0,
            "inactive TextEdit selection without teFOutlineHilite should not draw provider selection chrome"
        );
        assert!(
            inactive_outline_top_band > 0,
            "inactive TextEdit selection with teFOutlineHilite should draw provider outline chrome"
        );
        assert_eq!(
            inactive_outline_bus
                .read_word(inactive_outline_te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            0
        );
        assert_eq!(
            inactive_outline_bus
                .read_word(inactive_outline_te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
            3
        );
        assert_eq!(inactive_line_height, active_line_height);

        let inactive_bottom_band = count_set_pixels(
            &inactive_outline_bus,
            outline_base,
            outline_row_bytes,
            inactive_line_height - 2,
            0,
            inactive_line_height,
            24,
        );
        let active_bottom_band = count_set_pixels(
            &active_bus,
            active_base,
            active_row_bytes,
            active_line_height - 2,
            0,
            active_line_height,
            24,
        );
        assert!(
            active_bottom_band > inactive_bottom_band,
            "inactive outline selection should stay outline-only instead of drawing active bottom-stripe chrome"
        );
    }

    #[test]
    fn teupdate_c_rect_pointer_form_is_accepted_and_pops_eight_bytes() {
        // Inside Macintosh: Text 1993, 2-88 update semantics plus
        // MPW C call-shape compatibility (Rect* + TEHandle).
        let (mut disp, mut cpu, mut bus) = setup_with_port();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let rect_ptr = bus.alloc(8);
        bus.write_word(rect_ptr, 10);
        bus.write_word(rect_ptr + 2, 10);
        bus.write_word(rect_ptr + 4, 40);
        bus.write_word(rect_ptr + 6, 120);

        bus.write_long(TEST_SP, te_handle);
        bus.write_long(TEST_SP + 4, rect_ptr);

        let result = disp.dispatch_dialog(true, 0x1D3, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(
            TrapDispatcher::te_text_bytes(&bus, te_handle),
            b"HELLO".to_vec()
        );
    }

    #[test]
    fn teupdate_preserves_te_record_state_across_redraw() {
        // Pins the engines-agree contract from a9d3_teupdate_strict:
        // TEUpdate is a redraw operation and MUST NOT mutate hText,
        // teLength, selStart, or selEnd. Per IM:I I-387 + IM:Text 1993
        // p. 2-88. Mirrors band B3 of the strict bake (text bytes
        // "HELLO" preserved, teLength==5, selStart==1, selEnd==4 after
        // both C-form and Pascal-form TEUpdate calls).
        let (mut disp, mut cpu, mut bus) = setup_with_port();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);

        // C-form dispatch (8-byte pop).
        let rect_ptr = bus.alloc(8);
        bus.write_word(rect_ptr, 10);
        bus.write_word(rect_ptr + 2, 10);
        bus.write_word(rect_ptr + 4, 40);
        bus.write_word(rect_ptr + 6, 120);
        bus.write_long(TEST_SP, te_handle);
        bus.write_long(TEST_SP + 4, rect_ptr);
        let result = disp.dispatch_dialog(true, 0x1D3, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);

        // Pascal-form dispatch on the same TE record (12-byte pop):
        // hTE at sp+0, inlined rect at sp+4..sp+11.
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, te_handle);
        bus.write_word(TEST_SP + 4, 10);
        bus.write_word(TEST_SP + 6, 10);
        bus.write_word(TEST_SP + 8, 40);
        bus.write_word(TEST_SP + 10, 120);
        let result = disp.dispatch_dialog(true, 0x1D3, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);

        // State preservation contract.
        assert_eq!(
            TrapDispatcher::te_text_bytes(&bus, te_handle),
            b"HELLO".to_vec()
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET), 5);
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            1
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 4);
    }

    #[test]
    fn teactivate_sets_active_flag_preserves_selection_and_pops_arg() {
        // IM:I I-385; Inside Macintosh: Text 1993, 2-80.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 0);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);

        bus.write_long(TEST_SP, te_handle);
        let result = disp.dispatch_dialog(true, 0x1D8, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET), 1);
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            1
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 4);
    }

    #[test]
    fn tedeactivate_clears_active_flag_preserves_selection_and_pops_arg() {
        // IM:I I-385; Inside Macintosh: Text 1993, 2-80.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 1);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 2);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 5);

        bus.write_long(TEST_SP, te_handle);
        let result = disp.dispatch_dialog(true, 0x1D9, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET), 0);
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            2
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 5);
    }

    #[test]
    fn teclick_consumes_point_extend_and_tehandle_arguments() {
        // Inside Macintosh Volume I (1985), p. I-376 and
        // Inside Macintosh: Text (1993), p. 2-85.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");

        bus.write_long(TEST_SP, te_handle); // hTE
        bus.write_word(TEST_SP + 4, 0xFF00); // extend=TRUE in high byte
        bus.write_word(TEST_SP + 6, 18); // pt.v
        bus.write_word(TEST_SP + 8, 27); // pt.h

        let result = disp.dispatch_dialog(true, 0x1D4, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
    }

    #[test]
    fn teclick_empty_text_keeps_insertion_point_at_zero() {
        // Inside Macintosh Volume I (1985), p. I-376: TEClick places the
        // insertion point from mouse position; with empty text only offset 0
        // is valid.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 0);

        bus.write_long(TEST_SP, te_handle); // hTE
        bus.write_word(TEST_SP + 4, 0x0000); // extend=FALSE
        bus.write_word(TEST_SP + 6, 12); // pt.v
        bus.write_word(TEST_SP + 8, 40); // pt.h

        let result = disp.dispatch_dialog(true, 0x1D4, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            0
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 0);
    }

    #[test]
    fn teclick_preserves_terec_active_flag_and_telength() {
        // Mirrors band B2 of the a9d4_teclick_strict bake: both
        // BasiliskII System 7.5.3 ROM and Systemless HLE agree that
        // TEClick must not mutate the active flag (owned by
        // TEActivate/TEDeactivate per IM:I I-385) or teLength
        // (owned by TESetText/TEKey/TEDelete/TEInsert). Inside
        // Macintosh Volume I (1985), p. I-376; Inside Macintosh:
        // Text (1993), p. 2-85.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 0xFF);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 2);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
        let pre_active = bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET);
        let pre_telength = bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET);

        bus.write_long(TEST_SP, te_handle);
        bus.write_word(TEST_SP + 4, 0x0000); // extend=FALSE
        bus.write_word(TEST_SP + 6, 8); // pt.v
        bus.write_word(TEST_SP + 8, 32); // pt.h
        let result = disp.dispatch_dialog(true, 0x1D4, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET),
            pre_active
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET),
            pre_telength
        );
    }

    #[test]
    fn teclick_repeated_calls_balance_stack_no_drift() {
        // Mirrors band B3 of the a9d4_teclick_strict bake: an 8-call
        // composition of TEClick keeps the A7 advance balanced at
        // exactly 8 * 10 = 80 bytes; per-call pop errors that cancel
        // within a single call but drift over many invocations are
        // detected here. A7 is reset to TEST_SP between iterations so
        // each call exercises the SP discipline independently.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");

        for k in 0..8i16 {
            cpu.write_reg(Register::A7, TEST_SP);
            bus.write_long(TEST_SP, te_handle);
            bus.write_word(TEST_SP + 4, 0x0000); // extend=FALSE
            bus.write_word(TEST_SP + 6, (4 + k) as u16); // pt.v varied
            bus.write_word(TEST_SP + 8, (16 + k * 3) as u16); // pt.h varied
            let result = disp.dispatch_dialog(true, 0x1D4, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        }
    }

    fn textedit_selection_replacement_for_theme(
        theme_id: UiThemeId,
    ) -> (Vec<u8>, u16, u16, u16, u32) {
        let (mut disp, mut cpu, mut bus) = setup_with_port();
        disp.set_ui_theme_id(theme_id);
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 1);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);

        bus.write_long(TEST_SP, te_handle);
        bus.write_word(TEST_SP + 4, 0);
        bus.write_word(TEST_SP + 6, 0);
        bus.write_word(TEST_SP + 8, 40);
        bus.write_word(TEST_SP + 10, 120);
        let result = disp.dispatch_dialog(true, 0x1D3, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, te_handle);
        bus.write_word(TEST_SP + 4, u16::from(b'Y'));
        let result = disp.dispatch_dialog(true, 0x1DC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        (
            TrapDispatcher::te_text_bytes(&bus, te_handle),
            bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET),
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
            cpu.read_reg(Register::A7),
        )
    }

    fn textedit_private_scrap_editing_results_for_theme(
        theme_id: UiThemeId,
    ) -> TextEditPrivateScrapEditingSnapshot {
        let (mut disp, mut cpu, mut bus) = setup();
        disp.set_ui_theme_id(theme_id);

        let copy_te = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let copy_ptr = bus.read_long(copy_te);
        bus.write_word(copy_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
        bus.write_word(copy_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, copy_te);
        disp.dispatch_dialog(true, 0x1D5, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let copy = textedit_scrap_edit_case(&bus, copy_te, cpu.read_reg(Register::A7));

        let cut_te = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let cut_ptr = bus.read_long(cut_te);
        bus.write_word(cut_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
        bus.write_word(cut_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, cut_te);
        disp.dispatch_dialog(true, 0x1D6, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let cut = textedit_scrap_edit_case(&bus, cut_te, cpu.read_reg(Register::A7));

        let paste_te = make_te_with_text(&mut disp, &mut bus, b"HEXXO");
        let paste_ptr = bus.read_long(paste_te);
        bus.write_word(paste_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 2);
        bus.write_word(paste_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
        let paste_scrap = TrapDispatcher::allocate_handle_with_data(&mut bus, 2);
        let paste_scrap_ptr = bus.read_long(paste_scrap);
        bus.write_bytes(paste_scrap_ptr, b"LL");
        bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, paste_scrap);
        bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 2);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, paste_te);
        disp.dispatch_dialog(true, 0x1DB, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let paste = textedit_scrap_edit_case(&bus, paste_te, cpu.read_reg(Register::A7));

        let delete_te = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let delete_ptr = bus.read_long(delete_te);
        bus.write_word(delete_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
        bus.write_word(delete_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
        let delete_scrap = TrapDispatcher::allocate_handle_with_data(&mut bus, 2);
        let delete_scrap_ptr = bus.read_long(delete_scrap);
        bus.write_bytes(delete_scrap_ptr, b"QQ");
        bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, delete_scrap);
        bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 2);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, delete_te);
        disp.dispatch_dialog(true, 0x1D7, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();
        let delete = textedit_scrap_edit_case(&bus, delete_te, cpu.read_reg(Register::A7));

        TextEditPrivateScrapEditingSnapshot {
            copy,
            cut,
            paste,
            delete,
        }
    }

    #[test]
    fn systemless_theme_does_not_change_textedit_selection_offsets() {
        // Text 1993 appendix C defines selStart/selEnd as byte offsets into
        // the selection range; Text 1993 p. 2-81 says TEKey replaces that
        // range and positions the insertion point just past the inserted byte.
        // Theme selection chrome must not change those guest-visible fields.
        let classic = textedit_selection_replacement_for_theme(UiThemeId::ClassicSystem7);
        let themed = textedit_selection_replacement_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(classic.0, b"HYO".to_vec());
        assert_eq!(classic.1, 3);
        assert_eq!(classic.2, 2);
        assert_eq!(classic.3, 2);
        assert_eq!(classic.4, TEST_SP + 6);
        assert_eq!(
            themed, classic,
            "systemless-default must not change TextEdit replacement selection offsets"
        );
    }

    #[test]
    fn systemless_theme_does_not_change_textedit_private_scrap_editing() {
        // IM:I I-373 says TextEdit's scrap is private to TextEdit, not the
        // Scrap Manager desk scrap. IM:I I-385..I-387 define TECopy, TECut,
        // TEPaste, and TEDelete selection/scrap effects; MTE 1992 p.
        // 6-132..6-134 routes dialog edit commands through those TextEdit
        // operations. Theme chrome must not change the private-scrap globals,
        // TERec text/selection fields, or stack ABI for these edit actions.
        let classic = textedit_private_scrap_editing_results_for_theme(UiThemeId::ClassicSystem7);
        let themed = textedit_private_scrap_editing_results_for_theme(UiThemeId::SystemlessDefault);

        assert_eq!(classic.copy.text, b"HELLO".to_vec());
        assert_eq!(classic.copy.selection, (1, 4));
        assert_eq!(classic.copy.scrap_length, 3);
        assert_eq!(classic.copy.scrap_bytes, b"ELL".to_vec());
        assert_eq!(classic.copy.stack_after, TEST_SP + 4);

        assert_eq!(classic.cut.text, b"HO".to_vec());
        assert_eq!(classic.cut.selection, (1, 1));
        assert_eq!(classic.cut.scrap_length, 3);
        assert_eq!(classic.cut.scrap_bytes, b"ELL".to_vec());
        assert_eq!(classic.cut.stack_after, TEST_SP + 4);

        assert_eq!(classic.paste.text, b"HELLO".to_vec());
        assert_eq!(classic.paste.selection, (4, 4));
        assert_eq!(classic.paste.scrap_length, 2);
        assert_eq!(classic.paste.scrap_bytes, b"LL".to_vec());
        assert_eq!(classic.paste.stack_after, TEST_SP + 4);

        assert_eq!(classic.delete.text, b"HO".to_vec());
        assert_eq!(classic.delete.selection, (1, 1));
        assert_eq!(classic.delete.scrap_length, 2);
        assert_eq!(classic.delete.scrap_bytes, b"QQ".to_vec());
        assert_eq!(classic.delete.stack_after, TEST_SP + 4);
        assert_eq!(
            themed, classic,
            "systemless-default must not change TextEdit private-scrap edit semantics"
        );
    }

    #[test]
    fn teidle_consumes_tehandle_argument() {
        // Inside Macintosh Volume I (1985), p. I-374 and
        // Inside Macintosh: Text (1993), p. 2-84.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");

        bus.write_long(TEST_SP, te_handle);
        let result = disp.dispatch_dialog(true, 0x1DA, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    #[test]
    fn teidle_toggles_visible_insertion_caret_after_blink_interval() {
        // IM:I I-374 and Text 1993 p. 2-84: TEIdle blinks an active
        // insertion caret, but only after the minimum blink interval
        // initially set to 32 ticks.
        let (mut disp, mut cpu, mut bus) = setup_with_port();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"");
        let te_ptr = bus.read_long(te_handle);
        let (screen_base, row_bytes, _screen_w, _screen_h, _pixel_size) = disp.screen_mode;
        for i in 0..(row_bytes * 80) {
            bus.write_byte(screen_base + i, 0);
        }
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 0);

        disp.tick_count = 100;
        bus.write_long(crate::memory::globals::addr::TICKS, 100);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, te_handle);
        let result = disp.dispatch_dialog(true, 0x1D8, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert!(
            screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
            "TEActivate should show the insertion caret before TEIdle"
        );
        assert_eq!(
            bus.read_long(te_ptr + TrapDispatcher::TE_CARET_TIME_OFFSET),
            100
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_CARET_STATE_OFFSET),
            0
        );

        disp.tick_count = 131;
        bus.write_long(crate::memory::globals::addr::TICKS, 131);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, te_handle);
        let result = disp.dispatch_dialog(true, 0x1DA, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert!(
            screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
            "TEIdle before 32 ticks should leave the caret visible"
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_CARET_STATE_OFFSET),
            0
        );

        disp.tick_count = 132;
        bus.write_long(crate::memory::globals::addr::TICKS, 132);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, te_handle);
        let result = disp.dispatch_dialog(true, 0x1DA, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert!(
            !screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
            "TEIdle at the 32-tick boundary should hide the caret"
        );
        assert_eq!(
            bus.read_long(te_ptr + TrapDispatcher::TE_CARET_TIME_OFFSET),
            132
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_CARET_STATE_OFFSET),
            1
        );

        disp.tick_count = 164;
        bus.write_long(crate::memory::globals::addr::TICKS, 164);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, te_handle);
        let result = disp.dispatch_dialog(true, 0x1DA, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert!(
            screen_pixel_is_set(&bus, screen_base, row_bytes, 1, 0),
            "the next elapsed interval should show the caret again"
        );
        assert_eq!(
            bus.read_long(te_ptr + TrapDispatcher::TE_CARET_TIME_OFFSET),
            164
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_CARET_STATE_OFFSET),
            0
        );
    }

    #[test]
    fn teidle_preserves_active_flag_and_selection_offsets() {
        // Inside Macintosh Volume I (1985), pp. I-374 and I-385: TEIdle
        // blinks caret for an active record, while TEActivate/TEDeactivate
        // control active state. The text length must also remain
        // unchanged across an idle call.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 0xFF);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 2);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
        let pre_telength = bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET);

        bus.write_long(TEST_SP, te_handle);
        let result = disp.dispatch_dialog(true, 0x1DA, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET),
            0xFF
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            2
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 4);
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET),
            pre_telength
        );
    }

    #[test]
    fn teidle_repeated_calls_balance_stack_and_preserve_terec_state() {
        // Inside Macintosh Volume I (1985), p. I-374: TEIdle is intended
        // to be called on every null event from the application's event
        // loop, so per-call pop discipline must compose cleanly across
        // many invocations and the TERec must remain bit-for-bit
        // identical across the sequence. Mirrors B3 of the
        // `a9da_teidle_strict` strict bake: 8 successive TEIdle calls,
        // assert A7 advanced exactly 8 * 4 = 32 bytes AND active /
        // selStart / selEnd / teLength are all preserved.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"WORLD");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET, 0xFF);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 3);
        let pre_active = bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET);
        let pre_sel_start = bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET);
        let pre_sel_end = bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET);
        let pre_telength = bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET);

        for _ in 0..8 {
            cpu.write_reg(Register::A7, TEST_SP);
            bus.write_long(TEST_SP, te_handle);
            let result = disp.dispatch_dialog(true, 0x1DA, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok());
            assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        }

        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_ACTIVE_OFFSET),
            pre_active
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            pre_sel_start
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET),
            pre_sel_end
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET),
            pre_telength
        );
    }

    #[test]
    fn tescroll_offsets_destrect_by_requested_delta_and_pops_args() {
        // IM:I I-388; Inside Macintosh: Text 1993, 2-91.
        let (mut disp, mut cpu, mut bus) = setup_with_port();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET, 10);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2, 20);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4, 70);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 180);

        bus.write_long(TEST_SP, te_handle);
        bus.write_word(TEST_SP + 4, (-3i16) as u16); // dv
        bus.write_word(TEST_SP + 6, 5); // dh
        let result = disp.dispatch_dialog(true, 0x1DD, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET),
            7
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2),
            25
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4),
            67
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6),
            185
        );
    }

    #[test]
    fn teinsert_inserts_before_selection_shifts_range_and_preserves_scrap() {
        // IM:I I-387; Inside Macintosh: Text 1993, 2-94. TEInsert splices
        // the supplied bytes at the selStart offset (BEFORE the selection)
        // without replacing the selected range. The selection range is
        // preserved logically — selStart and selEnd shift forward by the
        // inserted length so the original selected characters still fall
        // inside the range.
        let (mut disp, mut cpu, mut bus) = setup();
        let existing_scrap = TrapDispatcher::allocate_handle_with_data(&mut bus, 2);
        let existing_scrap_ptr = bus.read_long(existing_scrap);
        bus.write_bytes(existing_scrap_ptr, b"QQ");
        bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, existing_scrap);
        bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 2);

        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);

        let insert_ptr = bus.alloc(3);
        bus.write_bytes(insert_ptr, b"XYZ");
        bus.write_long(TEST_SP, te_handle);
        bus.write_long(TEST_SP + 4, 3);
        bus.write_long(TEST_SP + 8, insert_ptr);
        let result = disp.dispatch_dialog(true, 0x1DE, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);

        assert_eq!(
            TrapDispatcher::te_text_bytes(&bus, te_handle),
            b"HXYZELLO".to_vec()
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            4
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 7);
        assert_eq!(
            bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE),
            existing_scrap
        );
        assert_eq!(
            bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH),
            2
        );
        assert_eq!(bus.read_bytes(existing_scrap_ptr, 2), b"QQ".to_vec());
    }

    #[test]
    fn teinsert_insertion_point_inserts_before_caret_and_shifts_caret() {
        // TEInsert inserts immediately before the current selection/insertion
        // point per IM:I I-387; Inside Macintosh: Text 1993, 2-94. The
        // selection range is preserved logically — the insertion point shifts
        // forward by the inserted length so it continues to mark the same
        // place relative to the surrounding original characters.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 2);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 2);

        let insert_ptr = bus.alloc(1);
        bus.write_byte(insert_ptr, b'X');
        bus.write_long(TEST_SP, te_handle);
        bus.write_long(TEST_SP + 4, 1);
        bus.write_long(TEST_SP + 8, insert_ptr);
        let result = disp.dispatch_dialog(true, 0x1DE, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);

        assert_eq!(
            TrapDispatcher::te_text_bytes(&bus, te_handle),
            b"HEXLLO".to_vec()
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            3
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 3);
    }

    #[test]
    fn tesetalignment_writes_just_field_and_pops_args() {
        // IM:I I-388 (TESetJust) and Inside Macintosh: Text 1993, 2-87.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_JUST_OFFSET, 0);

        bus.write_long(TEST_SP, te_handle);
        bus.write_word(TEST_SP + 4, (-1i16) as u16);
        let result = disp.dispatch_dialog(true, 0x1DF, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_JUST_OFFSET) as i16,
            -1
        );
    }

    // ---- TECopy / TECut / TEPaste ($A9D5 / $A9D6 / $A9DB) ----

    #[test]
    fn tecopy_nonempty_selection_updates_textedit_scrap_globals() {
        // IM:I I-386 + I-389.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
        bus.write_long(TEST_SP, te_handle);

        let result = disp.dispatch_dialog(true, 0x1D5, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        let scrap_handle = bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE);
        let scrap_ptr = bus.read_long(scrap_handle);
        assert_ne!(scrap_handle, 0);
        assert_ne!(scrap_ptr, 0);
        assert_eq!(
            bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH),
            3
        );
        assert_eq!(bus.read_bytes(scrap_ptr, 3), b"ELL".to_vec());
    }

    #[test]
    fn tecopy_empty_selection_preserves_existing_scrap_contents() {
        // IM:I I-386: no selected range => no copy.
        let (mut disp, mut cpu, mut bus) = setup();
        let existing_scrap = TrapDispatcher::allocate_handle_with_data(&mut bus, 2);
        let existing_ptr = bus.read_long(existing_scrap);
        bus.write_bytes(existing_ptr, b"ZZ");
        bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, existing_scrap);
        bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 2);

        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 2);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 2);
        bus.write_long(TEST_SP, te_handle);

        let result = disp.dispatch_dialog(true, 0x1D5, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(
            bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE),
            existing_scrap
        );
        assert_eq!(
            bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH),
            2
        );
        assert_eq!(bus.read_bytes(existing_ptr, 2), b"ZZ".to_vec());
    }

    #[test]
    fn tecut_nonempty_selection_copies_to_scrap_and_deletes_selected_text() {
        // IM:I I-391 + I-389.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 4);
        bus.write_long(TEST_SP, te_handle);

        let result = disp.dispatch_dialog(true, 0x1D6, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(
            TrapDispatcher::te_text_bytes(&bus, te_handle),
            b"HO".to_vec()
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            1
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 1);

        let scrap_handle = bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE);
        let scrap_ptr = bus.read_long(scrap_handle);
        assert_eq!(
            bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH),
            3
        );
        assert_eq!(bus.read_bytes(scrap_ptr, 3), b"ELL".to_vec());
    }

    #[test]
    fn tecut_empty_selection_preserves_text_and_scrap_contents() {
        // IM:I I-391: insertion-point selection performs no cut.
        let (mut disp, mut cpu, mut bus) = setup();
        let existing_scrap = TrapDispatcher::allocate_handle_with_data(&mut bus, 2);
        let existing_ptr = bus.read_long(existing_scrap);
        bus.write_bytes(existing_ptr, b"QQ");
        bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, existing_scrap);
        bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 2);

        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 3);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 3);
        bus.write_long(TEST_SP, te_handle);

        let result = disp.dispatch_dialog(true, 0x1D6, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(
            TrapDispatcher::te_text_bytes(&bus, te_handle),
            b"HELLO".to_vec()
        );
        assert_eq!(
            bus.read_long(crate::memory::globals::addr::TE_SCRP_HANDLE),
            existing_scrap
        );
        assert_eq!(
            bus.read_word(crate::memory::globals::addr::TE_SCRP_LENGTH),
            2
        );
        assert_eq!(bus.read_bytes(existing_ptr, 2), b"QQ".to_vec());
    }

    #[test]
    fn tepaste_nonempty_scrap_replaces_selection_and_advances_insertion_point() {
        // IM:I I-387 + I-389.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HLO");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 1);

        let scrap_handle = TrapDispatcher::allocate_handle_with_data(&mut bus, 2);
        let scrap_ptr = bus.read_long(scrap_handle);
        bus.write_bytes(scrap_ptr, b"EL");
        bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, scrap_handle);
        bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 2);

        bus.write_long(TEST_SP, te_handle);
        let result = disp.dispatch_dialog(true, 0x1DB, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(
            TrapDispatcher::te_text_bytes(&bus, te_handle),
            b"HELLO".to_vec()
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            3
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 3);
    }

    #[test]
    fn tepaste_empty_scrap_is_noop() {
        // IM:I I-387: empty scrap inserts nothing.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 3);

        let scrap_handle = TrapDispatcher::allocate_handle_with_data(&mut bus, 2);
        let scrap_ptr = bus.read_long(scrap_handle);
        bus.write_bytes(scrap_ptr, b"ZZ");
        bus.write_long(crate::memory::globals::addr::TE_SCRP_HANDLE, scrap_handle);
        bus.write_word(crate::memory::globals::addr::TE_SCRP_LENGTH, 0);

        bus.write_long(TEST_SP, te_handle);
        let result = disp.dispatch_dialog(true, 0x1DB, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(
            TrapDispatcher::te_text_bytes(&bus, te_handle),
            b"HELLO".to_vec()
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            1
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 3);
    }

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

        bus.write_word(TEST_SP, 0); // viewRect.top
        bus.write_word(TEST_SP + 2, 0); // viewRect.left
        bus.write_word(TEST_SP + 4, 100); // viewRect.bottom
        bus.write_word(TEST_SP + 6, 120); // viewRect.right
        bus.write_word(TEST_SP + 8, 10); // destRect.top
        bus.write_word(TEST_SP + 10, 20); // destRect.left
        bus.write_word(TEST_SP + 12, 90); // destRect.bottom
        bus.write_word(TEST_SP + 14, 80); // destRect.right

        let result = disp.dispatch_dialog(true, 0x1D2, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 16);

        let te_handle = bus.read_long(TEST_SP + 16);
        let te_ptr = bus.read_long(te_handle);
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET),
            10
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2),
            20
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4),
            100
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6),
            120
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET), 0);
        let h_text = bus.read_long(te_ptr + TrapDispatcher::TE_HTEXT_OFFSET);
        assert_ne!(h_text, 0);
        assert_eq!(bus.read_long(h_text), 0);
    }

    // Inside Macintosh: Text (1993), p. 2-78: TEStyleNew returns a
    // multistyled TEHandle, uses -1 sentinels in txSize/lineHeight/
    // fontAscent, and installs style metadata.
    #[test]
    fn testylenew_returns_styled_handle_and_initializes_sentinel_fields() {
        let (mut disp, mut cpu, mut bus) = setup();

        bus.write_word(TEST_SP, 0); // viewRect.top
        bus.write_word(TEST_SP + 2, 0); // viewRect.left
        bus.write_word(TEST_SP + 4, 100); // viewRect.bottom
        bus.write_word(TEST_SP + 6, 120); // viewRect.right
        bus.write_word(TEST_SP + 8, 10); // destRect.top
        bus.write_word(TEST_SP + 10, 20); // destRect.left
        bus.write_word(TEST_SP + 12, 90); // destRect.bottom
        bus.write_word(TEST_SP + 14, 80); // destRect.right

        let result = disp.dispatch_dialog(true, 0x03E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            cpu.read_reg(Register::A7),
            TEST_SP + 16,
            "TEStyleNew should pop two Rect arguments (16 bytes)"
        );

        let te_handle = bus.read_long(TEST_SP + 16);
        assert_ne!(te_handle, 0, "TEStyleNew should return a non-NIL TEHandle");

        let te_ptr = bus.read_long(te_handle);
        assert_ne!(te_ptr, 0, "returned TEHandle should point to a TERec");
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_TX_SIZE_OFFSET),
            0xFFFF
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_LINE_HEIGHT_OFFSET),
            0xFFFF
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_FONT_ASCENT_OFFSET),
            0xFFFF
        );

        let style_handle = bus.read_long(te_ptr + TrapDispatcher::TE_TX_FONT_OFFSET);
        assert_ne!(style_handle, 0, "styled record should carry a style handle");
        let style_ptr = bus.read_long(style_handle);
        assert_ne!(
            style_ptr, 0,
            "style handle should dereference to a style record"
        );
        assert_ne!(
            bus.read_long(style_ptr + TrapDispatcher::TE_STYLE_NULL_STYLE_OFFSET),
            0,
            "style record should include a non-NIL null-style handle"
        );
    }

    // ---- TEStyleNew (pointer-arg convention) ----

    // Inside Macintosh: Text (1993), p. 2-78: TEStyleNew accepts a
    // pointer-arg convention `const Rect *destRect, const Rect *viewRect`
    // per MPW Universal Headers. Pascal LR push: destRect_ptr at SP+4
    // (deepest, pushed first), viewRect_ptr at SP+0 (shallowest, pushed
    // last). Net pop is 8 bytes.
    #[test]
    fn testylenew_pointer_arg_convention_initializes_destrect_viewrect_and_styled_sentinels() {
        let (mut disp, mut cpu, mut bus) = setup();

        // Build distinct destRect and viewRect in guest memory.
        let dest_rect_ptr: u32 = 0x1A0000;
        let view_rect_ptr: u32 = 0x1A0010;
        bus.write_word(dest_rect_ptr, (-1200_i16) as u16);
        bus.write_word(dest_rect_ptr + 2, (-1200_i16) as u16);
        bus.write_word(dest_rect_ptr + 4, (-900_i16) as u16);
        bus.write_word(dest_rect_ptr + 6, (-1000_i16) as u16);
        bus.write_word(view_rect_ptr, (-1100_i16) as u16);
        bus.write_word(view_rect_ptr + 2, (-1300_i16) as u16);
        bus.write_word(view_rect_ptr + 4, (-800_i16) as u16);
        bus.write_word(view_rect_ptr + 6, (-900_i16) as u16);

        // Pascal LR push: viewRect_ptr at SP+0 (last pushed),
        // destRect_ptr at SP+4 (first pushed).
        bus.write_long(TEST_SP, view_rect_ptr);
        bus.write_long(TEST_SP + 4, dest_rect_ptr);

        let result = disp.dispatch_dialog(true, 0x03E, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            cpu.read_reg(Register::A7),
            TEST_SP + 8,
            "TEStyleNew pointer convention should pop 8 bytes (two pointer args)"
        );

        let te_handle = bus.read_long(TEST_SP + 8);
        assert_ne!(te_handle, 0, "TEStyleNew should return a non-NIL TEHandle");
        let te_ptr = bus.read_long(te_handle);
        assert_ne!(te_ptr, 0, "TEHandle should dereference to a TERec");

        // destRect at offset 0x00 round-trips from the destRect_ptr.
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET) as i16,
            -1200,
            "destRect.top must round-trip from caller-supplied destRect_ptr"
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2) as i16,
            -1200,
            "destRect.left must round-trip from caller-supplied destRect_ptr"
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4) as i16,
            -900,
            "destRect.bottom must round-trip from caller-supplied destRect_ptr"
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6) as i16,
            -1000,
            "destRect.right must round-trip from caller-supplied destRect_ptr"
        );

        // viewRect at offset 0x08 round-trips from the viewRect_ptr.
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET) as i16,
            -1100,
            "viewRect.top must round-trip from caller-supplied viewRect_ptr"
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 2) as i16,
            -1300,
            "viewRect.left must round-trip from caller-supplied viewRect_ptr"
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4) as i16,
            -800,
            "viewRect.bottom must round-trip from caller-supplied viewRect_ptr"
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6) as i16,
            -900,
            "viewRect.right must round-trip from caller-supplied viewRect_ptr"
        );

        // Styled sentinels per IM:Text 1993 p. 2-78.
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_TX_SIZE_OFFSET),
            0xFFFF,
            "TEStyleNew must set txSize to -1 (styled-record sentinel)"
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_LINE_HEIGHT_OFFSET),
            0xFFFF,
            "TEStyleNew must set lineHeight to -1 (styled-record sentinel)"
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_FONT_ASCENT_OFFSET),
            0xFFFF,
            "TEStyleNew must set fontAscent to -1 (styled-record sentinel)"
        );

        // Style handle at txFont/txFace overlay (4 bytes at offset 0x4A).
        let style_handle = bus.read_long(te_ptr + TrapDispatcher::TE_TX_FONT_OFFSET);
        assert_ne!(
            style_handle, 0,
            "TEStyleNew must install a non-NIL TEStyleHandle in the txFont/txFace overlay"
        );
        let style_ptr = bus.read_long(style_handle);
        assert_ne!(
            style_ptr, 0,
            "TEStyleHandle should dereference to an allocated TEStyleRec"
        );
    }

    // Inside Macintosh: Text (1993), p. 2-78 + MPW TextEdit.h
    // ONEWORDINLINE(0xA83E): TEStyleNew is `EXTERN_API(TEHandle)`.
    // Pascal FUNCTION protocol: caller pre-allocates 4-byte TEHandle
    // result slot, pushes 2 pointer args (8 bytes); trap pops 8 +
    // writes 4-byte result.
    #[test]
    fn testylenew_function_protocol_consumes_two_pointer_args_and_writes_4_byte_result() {
        let (mut disp, mut cpu, mut bus) = setup();

        let dest_rect_ptr: u32 = 0x1A0020;
        let view_rect_ptr: u32 = 0x1A0030;
        // Both rects: non-empty distinct values so te_new_rect_args
        // selects the pointer convention (8 bytes).
        bus.write_word(dest_rect_ptr, (-500_i16) as u16);
        bus.write_word(dest_rect_ptr + 2, (-500_i16) as u16);
        bus.write_word(dest_rect_ptr + 4, (-300_i16) as u16);
        bus.write_word(dest_rect_ptr + 6, (-400_i16) as u16);
        bus.write_word(view_rect_ptr, (-450_i16) as u16);
        bus.write_word(view_rect_ptr + 2, (-550_i16) as u16);
        bus.write_word(view_rect_ptr + 4, (-250_i16) as u16);
        bus.write_word(view_rect_ptr + 6, (-350_i16) as u16);

        bus.write_long(TEST_SP, view_rect_ptr);
        bus.write_long(TEST_SP + 4, dest_rect_ptr);
        // Poison the result slot at sp+8 and a sentinel past it.
        bus.write_long(TEST_SP + 8, 0xDEADBEEF);
        bus.write_long(TEST_SP + 12, 0xCAFEBABE);

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

        // A7 advanced exactly 8 bytes.
        assert_eq!(
            cpu.read_reg(Register::A7),
            TEST_SP + 8,
            "TEStyleNew pointer-arg convention must pop exactly 8 bytes"
        );

        // The function result slot at the former sp+8 was overwritten
        // by a non-poison TEHandle.
        let te_handle = bus.read_long(TEST_SP + 8);
        assert_ne!(
            te_handle, 0xDEADBEEF,
            "TEStyleNew result slot must be overwritten by a real TEHandle, not the poison sentinel"
        );
        assert_ne!(te_handle, 0, "TEStyleNew result must be a non-NIL TEHandle");

        // The sentinel at sp+12 (4 bytes past the 4-byte result slot)
        // must survive — TEStyleNew must not write past its result.
        assert_eq!(
            bus.read_long(TEST_SP + 12),
            0xCAFEBABE,
            "TEStyleNew must not write past the 4-byte function-result slot"
        );
    }

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

        disp.tx_font = 0;
        disp.tx_face = 0;
        disp.tx_size = 12;
        let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
        disp.initialize_styled_te_record(&mut bus, te_handle, (0, 0, 80, 200), (0, 0, 80, 200));

        let text = b"small\rLARGE";
        let text_ptr = bus.alloc(text.len() as u32);
        bus.write_bytes(text_ptr, text);
        let style_scrap = make_style_scrap(
            &mut bus,
            &[
                (0, 10, 8, 0, 1, 9, (0, 0, 0)),
                (6, 14, 11, 0, 0, 12, (0x1111, 0x2222, 0x3333)),
            ],
        );

        bus.write_word(TEST_SP, 0x0007);
        bus.write_long(TEST_SP + 2, te_handle);
        bus.write_long(TEST_SP + 6, style_scrap);
        bus.write_long(TEST_SP + 10, text.len() as u32);
        bus.write_long(TEST_SP + 14, text_ptr);

        let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            cpu.read_reg(Register::A7),
            TEST_SP + 18,
            "TEStyleInsert must consume selector + hTE + hST + length + text pointer"
        );

        let te_ptr = bus.read_long(te_handle);
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET) as usize,
            text.len()
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_N_LINES_OFFSET),
            2,
            "explicit CR should produce two TextEdit lines"
        );

        let style_handle = bus.read_long(te_ptr + TrapDispatcher::TE_TX_FONT_OFFSET);
        let style_ptr = bus.read_long(style_handle);
        assert_eq!(
            bus.read_word(style_ptr + TrapDispatcher::TE_STYLE_N_RUNS_OFFSET),
            2
        );
        assert_eq!(
            bus.read_word(style_ptr + TrapDispatcher::TE_STYLE_N_STYLES_OFFSET),
            2
        );
        assert_eq!(
            bus.read_word(style_ptr + TrapDispatcher::TE_STYLE_RUNS_OFFSET),
            0
        );
        assert_eq!(
            bus.read_word(style_ptr + TrapDispatcher::TE_STYLE_RUNS_OFFSET + 4),
            6
        );

        let style_table = bus.read_long(style_ptr + TrapDispatcher::TE_STYLE_STYLE_TABLE_OFFSET);
        let style_table_ptr = bus.read_long(style_table);
        assert_eq!(
            bus.read_word(style_table_ptr + TrapDispatcher::ST_ELEMENT_FACE_OFFSET),
            1,
            "first style run should preserve the bold face from the style scrap"
        );
        assert_eq!(
            bus.read_word(style_table_ptr + TrapDispatcher::ST_ELEMENT_SIZE_OFFSET),
            9,
            "first style run should preserve the smaller body size"
        );
        let second_style = style_table_ptr + TrapDispatcher::ST_ELEMENT_SIZE;
        assert_eq!(
            bus.read_word(second_style + TrapDispatcher::ST_ELEMENT_SIZE_OFFSET),
            12
        );
        assert_eq!(
            (
                bus.read_word(second_style + TrapDispatcher::ST_ELEMENT_COLOR_OFFSET),
                bus.read_word(second_style + TrapDispatcher::ST_ELEMENT_COLOR_OFFSET + 2),
                bus.read_word(second_style + TrapDispatcher::ST_ELEMENT_COLOR_OFFSET + 4),
            ),
            (0x1111, 0x2222, 0x3333)
        );

        let lh_table = bus.read_long(style_ptr + TrapDispatcher::TE_STYLE_LH_TABLE_OFFSET);
        let lh_ptr = bus.read_long(lh_table);
        assert_eq!(
            bus.read_word(lh_ptr + TrapDispatcher::LH_ELEMENT_HEIGHT_OFFSET),
            10,
            "first TextEdit line should use first style's line height"
        );
        assert_eq!(
            bus.read_word(
                lh_ptr + TrapDispatcher::LH_ELEMENT_SIZE + TrapDispatcher::LH_ELEMENT_HEIGHT_OFFSET
            ),
            14,
            "second TextEdit line should use second style's line height"
        );

        let runs = disp.te_style_runs(&bus, te_handle, text.len());
        assert_eq!(runs.len(), 2);
        assert_eq!(runs[0].start, 0);
        assert_eq!(runs[0].style_index, 0);
        assert_eq!(runs[0].style.size, 9);
        assert_eq!(runs[1].start, 6);
        assert_eq!(runs[1].style_index, 1);
        assert_eq!(runs[1].style.size, 12);
    }

    // Inside Macintosh: Text (1993), p. 2-92: TEAutoView takes
    // `fAuto: Boolean; hTE: TEHandle`. With Pascal calling convention, hTE
    // (last parameter) is at SP+0 and fAuto at SP+4.
    #[test]
    fn teautoview_reads_hte_from_sp_plus_0_and_fauto_from_sp_plus_4() {
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);

        bus.write_long(TEST_SP, te_handle); // SP+0: hTE (last-pushed)
                                            // Pascal BOOLEAN in high byte (MPW C convention).
        bus.write_byte(TEST_SP + 4, 1); // SP+4: fAuto = TRUE

        let result = disp.dispatch_dialog(true, 0x013, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert!(
            disp.te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL),
            "TEAutoView must enable auto-scroll on the TE handle at SP+0"
        );
    }

    #[test]
    fn teautoview_clears_feature_when_fauto_is_false() {
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);

        // First turn it on.
        disp.set_te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL, true);
        assert!(disp.te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL));

        bus.write_long(TEST_SP, te_handle);
        bus.write_word(TEST_SP + 4, 0); // fAuto = FALSE

        let result = disp.dispatch_dialog(true, 0x013, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            cpu.read_reg(Register::A7),
            TEST_SP + 6,
            "TEAutoView should pop one Boolean and one TEHandle"
        );
        assert!(
            !disp.te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL),
            "TEAutoView(FALSE) must clear the auto-scroll bit on the supplied hTE"
        );
    }

    // IM:Text 2-92 + IM:VI 15-22: TEAutoView and TEFeatureFlag
    // teFAutoScroll expose the same automatic-scroll state.
    #[test]
    fn teautoview_and_tefeatureflag_observe_shared_autoscroll_state() {
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);

        bus.write_long(TEST_SP, te_handle);
        bus.write_byte(TEST_SP + 4, 1); // fAuto = TRUE
        let result = disp.dispatch_dialog(true, 0x013, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 0x000E); // selector
        bus.write_long(TEST_SP + 2, te_handle); // hTE
        bus.write_word(TEST_SP + 6, (-1i16) as u16); // teBitTest
        bus.write_word(TEST_SP + 8, TrapDispatcher::TE_FEATURE_AUTO_SCROLL);
        bus.write_word(TEST_SP + 10, 0xBEEF);
        let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(TEST_SP + 10), 1);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, te_handle);
        bus.write_word(TEST_SP + 4, 0); // fAuto = FALSE
        let result = disp.dispatch_dialog(true, 0x013, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 0x000E); // selector
        bus.write_long(TEST_SP + 2, te_handle); // hTE
        bus.write_word(TEST_SP + 6, (-1i16) as u16); // teBitTest
        bus.write_word(TEST_SP + 8, TrapDispatcher::TE_FEATURE_AUTO_SCROLL);
        bus.write_word(TEST_SP + 10, 0xBEEF);
        let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(bus.read_word(TEST_SP + 10), 0);
    }

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

        let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
        bus.write_word(TEST_SP, 0x000E);
        bus.write_long(TEST_SP + 2, te_handle);
        bus.write_word(TEST_SP + 6, 1); // teBitSet
        bus.write_word(TEST_SP + 8, 0); // teFAutoScroll

        let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        assert_eq!(bus.read_word(TEST_SP + 10), 0);
        assert!(disp.te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL));
    }

    // Inside Macintosh Volume VI (1991), pp. 15-22 and 15-43: selector
    // $000E dispatches to TEFeatureFlag; action TEBitTest (-1) reports
    // current feature state without mutating it.
    #[test]
    fn te_dispatch_feature_flag_test_action_returns_current_state() {
        let (mut disp, mut cpu, mut bus) = setup();

        let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
        disp.set_te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL, true);

        bus.write_word(TEST_SP, 0x000E); // selector
        bus.write_long(TEST_SP + 2, te_handle); // hTE
        bus.write_word(TEST_SP + 6, (-1i16) as u16); // teBitTest
        bus.write_word(TEST_SP + 8, TrapDispatcher::TE_FEATURE_AUTO_SCROLL);
        bus.write_word(TEST_SP + 10, 0xBEEF);

        let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 10);
        assert_eq!(bus.read_word(TEST_SP + 10), 1);
        assert!(
            disp.te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL),
            "teBitTest should not mutate the feature bit"
        );
    }

    // Inside Macintosh Volume VI (1991), p. 15-22: TEFeatureFlag with
    // action teBitClear=0 clears the selector bit and returns the prior
    // state. Symmetric counterpart of `te_dispatch_feature_flag_tracks_auto_scroll_state`
    // for the clear action; mirrors the strict bake's B3 witness in
    // a83d_tedispatch_strict.
    #[test]
    fn tefeatureflag_clear_action_returns_prior_one_state_and_clears_bit() {
        let (mut disp, mut cpu, mut bus) = setup();

        let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
        // Pre-set autoscroll so teBitClear sees a prior-on state.
        disp.set_te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL, true);
        assert!(disp.te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL));

        // THREEWORDINLINE(0x3F3C, 0x000E, 0xA83D) stack frame: selector
        // pre-pushed at sp+0; Pascal LR push order leaves hTE shallowest
        // (sp+2), action middle (sp+6), feature deepest (sp+8); short
        // function-result slot at sp+10.
        bus.write_word(TEST_SP, 0x000E); // selector $000E = TEFeatureFlag
        bus.write_long(TEST_SP + 2, te_handle); // hTE
        bus.write_word(TEST_SP + 6, 0); // action = teBitClear (0)
        bus.write_word(TEST_SP + 8, TrapDispatcher::TE_FEATURE_AUTO_SCROLL);
        bus.write_word(TEST_SP + 10, 0xBEEF); // poison result slot

        let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            cpu.read_reg(Register::A7),
            TEST_SP + 10,
            "TEFeatureFlag must pop 10 bytes (selector + hTE + action + feature)"
        );
        assert_eq!(
            bus.read_word(TEST_SP + 10),
            1,
            "teBitClear must return PRIOR bit state (1 = was set), not the new state"
        );
        assert!(
            !disp.te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL),
            "teBitClear must clear the feature bit after returning its prior state"
        );
    }

    // Inside Macintosh Volume VI (1991), p. 15-22: TEFeatureFlag is
    // FUNCTION TEFeatureFlag(feature, action: INTEGER; hTE: TEHandle): INTEGER.
    // Universal Headers TextEdit.h declares THREEWORDINLINE(0x3F3C, 0x000E, 0xA83D).
    // The 10-byte stack frame (2 selector + 4 hTE + 2 action + 2 feature)
    // is consumed atomically; the post-pop SP points at the 2-byte short
    // function-result slot. Mirrors the strict bake's B5 witness.
    #[test]
    fn tedispatch_function_protocol_consumes_threewordinline_stack_frame_for_tefeatureflag() {
        let (mut disp, mut cpu, mut bus) = setup();

        let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);

        bus.write_word(TEST_SP, 0x000E); // selector
        bus.write_long(TEST_SP + 2, te_handle);
        bus.write_word(TEST_SP + 6, (-1i16) as u16); // teBitTest
        bus.write_word(TEST_SP + 8, TrapDispatcher::TE_FEATURE_AUTO_SCROLL);
        // Poison both the result slot and a sentinel past it.
        bus.write_word(TEST_SP + 10, 0xDEAD);
        bus.write_word(TEST_SP + 12, 0xCAFE);

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

        // SP advanced exactly 10 bytes: selector(2) + hTE(4) + action(2) + feature(2).
        assert_eq!(
            cpu.read_reg(Register::A7),
            TEST_SP + 10,
            "TEFeatureFlag dispatch must advance A7 by exactly 10 bytes"
        );

        // Result slot was overwritten by the prior bit state (0 here).
        assert_ne!(
            bus.read_word(TEST_SP + 10),
            0xDEAD,
            "TEFeatureFlag must overwrite the 2-byte result slot with the prior bit state"
        );

        // Sentinel 2 bytes past the result slot must survive — TEFeatureFlag
        // must not write beyond its function-result word.
        assert_eq!(
            bus.read_word(TEST_SP + 12),
            0xCAFE,
            "TEFeatureFlag must not write past the 2-byte short function-result slot"
        );
    }

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

        let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
        disp.current_port = 0x181000;
        disp.tx_font = 4;
        disp.tx_face = 1;
        disp.tx_mode = 2;
        disp.tx_size = 12;
        disp.initialize_te_record(&mut bus, te_handle, (0, 0, 40, 80), (0, 0, 40, 80));

        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_TX_FONT_OFFSET, 22);
        bus.write_word(te_ptr + TrapDispatcher::TE_TX_FACE_OFFSET, 3);
        bus.write_word(te_ptr + TrapDispatcher::TE_TX_SIZE_OFFSET, 18);

        let style_ptr = 0x300000u32;
        let mode_ptr = 0x300100u32;
        bus.write_word(mode_ptr, 0x000F); // doAll
        bus.write_word(TEST_SP, 0x000A);
        bus.write_long(TEST_SP + 2, te_handle);
        bus.write_long(TEST_SP + 6, style_ptr);
        bus.write_long(TEST_SP + 10, mode_ptr);

        let result = disp.dispatch_dialog(true, 0x03D, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 14);
        assert_eq!(bus.read_word(TEST_SP + 14), 0xFFFF);
        assert_eq!(bus.read_word(style_ptr), 22);
        assert_eq!(bus.read_word(style_ptr + 2), 3);
        assert_eq!(bus.read_word(style_ptr + 4), 18);
        assert_eq!(bus.read_word(style_ptr + 6), 0);
        assert_eq!(bus.read_word(style_ptr + 8), 0);
        assert_eq!(bus.read_word(style_ptr + 10), 0);
    }

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

        let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
        disp.current_port = 0x181000;
        disp.tx_font = 4;
        disp.tx_face = 0;
        disp.tx_mode = 0;
        disp.tx_size = 10;
        disp.initialize_te_record(&mut bus, te_handle, (10, 20, 50, 80), (10, 20, 50, 80));

        bus.write_long(TEST_SP, te_handle);
        bus.write_word(TEST_SP + 4, (-6i16) as u16);
        bus.write_word(TEST_SP + 6, 4);

        let result = disp.dispatch_dialog(true, 0x1DD, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);

        let te_ptr = bus.read_long(te_handle);
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET),
            4
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2),
            24
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4),
            44
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6),
            84
        );
    }

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

        let te_handle = TrapDispatcher::allocate_te_handle(&mut bus);
        disp.current_port = 0x181000;
        disp.tx_font = 4;
        disp.tx_face = 0;
        disp.tx_mode = 0;
        disp.tx_size = 10;
        disp.initialize_te_record(&mut bus, te_handle, (10, 20, 50, 80), (10, 20, 50, 80));
        disp.te_set_text_contents(
            &mut bus,
            te_handle,
            b"one two three four five six seven eight nine ten",
        );

        bus.write_long(TEST_SP, te_handle);
        bus.write_word(TEST_SP + 4, (-6i16) as u16);
        bus.write_word(TEST_SP + 6, 4);

        let result = disp.dispatch_dialog(true, 0x012, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);

        let te_ptr = bus.read_long(te_handle);
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET),
            4
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2),
            20
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4),
            44
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6),
            80
        );
    }

    #[test]
    fn tepinscroll_in_range_negative_dv_offsets_destrect_top_and_bottom_exactly_by_dv() {
        // Inside Macintosh: Text 1993, p. 2-91: "The destination rectangle
        // is offset by the amount scrolled." Mirrors the strict bake fixture
        // a812_tepinscroll_strict band B1 (in-range up-scroll with multi-line
        // text that overflows the view): with text "A\rB\rC\rD\rE\rF\rG\rH"
        // and view height 10, any small negative dv is in-range so destRect.
        // top and destRect.bottom must shift by exactly dv with destRect.
        // left and destRect.right unchanged (dh=0).
        let (mut disp, mut cpu, mut bus) = setup();

        let te_handle = make_te_with_text(&mut disp, &mut bus, b"A\rB\rC\rD\rE\rF\rG\rH");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(
            te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET,
            (-1000i16) as u16,
        );
        bus.write_word(
            te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 2,
            (-1000i16) as u16,
        );
        bus.write_word(
            te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4,
            (-990i16) as u16,
        );
        bus.write_word(
            te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6,
            (-800i16) as u16,
        );
        bus.write_word(
            te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET,
            (-1000i16) as u16,
        );
        bus.write_word(
            te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2,
            (-1000i16) as u16,
        );
        bus.write_word(
            te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4,
            (-990i16) as u16,
        );
        bus.write_word(
            te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6,
            (-800i16) as u16,
        );

        bus.write_long(TEST_SP, te_handle);
        bus.write_word(TEST_SP + 4, (-3i16) as u16);
        bus.write_word(TEST_SP + 6, 0);

        let result = disp.dispatch_dialog(true, 0x012, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);

        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET) as i16,
            -1003,
            "destRect.top must shift by exactly dv=-3"
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2) as i16,
            -1000,
            "destRect.left must be unchanged (dh=0)"
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4) as i16,
            -993,
            "destRect.bottom must shift by exactly dv=-3"
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6) as i16,
            -800,
            "destRect.right must be unchanged (dh=0)"
        );
    }

    #[test]
    fn tepinscroll_pascal_lr_stack_layout_reads_dh_dv_and_hte_from_correct_offsets() {
        // Per Pascal LR-push-first-arg-deepest convention (matching the
        // EXTERN_API expansion in MPW Universal Headers TextEdit.h):
        //   sp+0  hTE: TEHandle  (4) — last pushed, shallowest
        //   sp+4  dv:  INTEGER   (2) — middle
        //   sp+6  dh:  INTEGER   (2) — first pushed, deepest
        // Total pop = 8 bytes; no function-result slot.
        //
        // Stage hTE at sp+0, dv=-2 at sp+4, dh=+7 at sp+6 with a 0xCAFE
        // sentinel at sp+8 to detect over-reads. The HLE must consume
        // exactly 8 bytes and the sentinel must survive.
        let (mut disp, mut cpu, mut bus) = setup();

        let te_handle = make_te_with_text(&mut disp, &mut bus, b"A\rB\rC\rD\rE\rF");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(
            te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET,
            (-500i16) as u16,
        );
        bus.write_word(
            te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 2,
            (-500i16) as u16,
        );
        bus.write_word(
            te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4,
            (-490i16) as u16,
        );
        bus.write_word(
            te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6,
            (-400i16) as u16,
        );
        bus.write_word(
            te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET,
            (-500i16) as u16,
        );
        // destRect.left = -510 (shifted left of view.left by 10, so positive
        // dh can be in-range to actually scroll right)
        bus.write_word(
            te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2,
            (-510i16) as u16,
        );
        bus.write_word(
            te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4,
            (-490i16) as u16,
        );
        bus.write_word(
            te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6,
            (-310i16) as u16,
        );

        bus.write_long(TEST_SP, te_handle);
        bus.write_word(TEST_SP + 4, (-2i16) as u16);
        bus.write_word(TEST_SP + 6, 7);
        bus.write_word(TEST_SP + 8, 0xCAFE);

        let result = disp.dispatch_dialog(true, 0x012, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(
            cpu.read_reg(Register::A7),
            TEST_SP + 8,
            "Pascal PROCEDURE must pop exactly 8 bytes (dh 2 + dv 2 + hTE 4)"
        );
        assert_eq!(
            bus.read_word(TEST_SP + 8),
            0xCAFE,
            "sentinel past the arg frame must survive"
        );
        // Verify destRect was actually mutated to confirm the HLE read
        // its args from the correct stack offsets (a swapped-arg stub
        // would shift by wrong amounts or pick up the sentinel).
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET) as i16,
            -502,
            "destRect.top must shift by dv=-2"
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2) as i16,
            -503,
            "destRect.left must shift by dh=+7 (clamped to max_right=10)"
        );
    }

    #[test]
    fn tepinscroll_clamps_overscroll_when_last_line_is_already_visible() {
        // Inside Macintosh: Text 1993, p. 2-91: TEPinScroll clamps movement
        // so scrolling stops once the last line is visible.
        let (mut disp, mut cpu, mut bus) = setup();

        let te_handle = make_te_with_text(&mut disp, &mut bus, b"A");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET, 10);
        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 2, 20);
        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4, 50);
        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6, 80);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET, 10);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2, 20);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4, 50);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 80);

        bus.write_long(TEST_SP, te_handle);
        bus.write_word(TEST_SP + 4, (-20i16) as u16);
        bus.write_word(TEST_SP + 6, 0);
        let result = disp.dispatch_dialog(true, 0x012, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);

        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET),
            10
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2),
            20
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4),
            50
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6),
            80
        );
    }

    #[test]
    fn teselview_autoscroll_disabled_leaves_destrect_unchanged() {
        // Inside Macintosh: Text 1993, p. 2-92: TESelView only scrolls when
        // auto-scroll is enabled for the TE record.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(
            &mut disp,
            &mut bus,
            b"one two three four five six seven eight nine ten",
        );
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET, 10);
        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 2, 20);
        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4, 50);
        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6, 80);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET, 0);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2, 20);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4, 40);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 80);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 0);
        disp.set_te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL, false);

        bus.write_long(TEST_SP, te_handle);
        let result = disp.dispatch_dialog(true, 0x011, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET),
            0
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2),
            20
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4),
            40
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6),
            80
        );
    }

    #[test]
    fn teselview_autoscroll_enabled_scrolls_destrect_toward_selection() {
        // Inside Macintosh: Text 1993, p. 2-92: TESelView scrolls selection
        // into view when auto-scroll is enabled.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(
            &mut disp,
            &mut bus,
            b"one two three four five six seven eight nine ten",
        );
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET, 10);
        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 2, 20);
        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4, 50);
        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6, 80);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET, 0);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2, 20);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4, 40);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 80);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 0);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 0);
        disp.set_te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL, true);

        bus.write_long(TEST_SP, te_handle);
        let result = disp.dispatch_dialog(true, 0x011, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET),
            10
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2),
            20
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4),
            50
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6),
            80
        );
    }

    #[test]
    fn teselview_apple_canonical_below_view_shifts_destrect_top_up() {
        // Inside Macintosh: Text 1993, p. 2-92 (Apple canonical):
        //   "The top left part of the selection range is scrolled
        //    into view."
        // Witnesses the contract-only assertion
        //   A811:teselview_autoscroll_enabled_with_selection_below_view_shifts_destrect_up_per_apple_canonical
        // which the strict bake intentionally does NOT witness
        // (BasiliskII System 7.5.3 ROM does not scroll destRect in
        // this case — empirically verified). Systemless implements the
        // Apple-canonical semantic.
        //
        // Setup: destRect = (0, 0, 100, 200) tall enough for the
        // entire multi-line text, viewRect = (0, 0, 100, 30) showing
        // only the first ~1.5 lines, selection at the last character
        // (line 7 of "A\rB\r..\rH", well below viewRect.bottom).
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"A\rB\rC\rD\rE\rF\rG\rH");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET, 0);
        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 2, 0);
        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 4, 30);
        bus.write_word(te_ptr + TrapDispatcher::TE_VIEW_RECT_OFFSET + 6, 100);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET, 0);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2, 0);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4, 200);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 100);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 14);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 14);
        disp.set_te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL, true);

        let pre_top = bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET) as i16;

        bus.write_long(TEST_SP, te_handle);
        let result = disp.dispatch_dialog(true, 0x011, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        let post_top = bus.read_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET) as i16;
        assert!(
            post_top < pre_top,
            "TESelView must shift destRect.top UP (negative direction) when selection is below viewRect per IM:Text 1993 p. 2-92; got pre_top={} post_top={}",
            pre_top,
            post_top
        );
    }

    #[test]
    fn teselview_procedure_protocol_consumes_only_tehandle_arg() {
        // Witness for golden assertion id
        //   A811:teselview_procedure_protocol_consumes_tehandle_argument_from_stack
        // Pascal PROCEDURE: 4-byte hTE arg, no result. Sentinel guard
        // at TEST_SP+4 must survive the call (trap must not write past
        // its argument frame). A7 advances by exactly 4 bytes.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"hi");
        // Auto-scroll disabled so the trap is a guaranteed destRect
        // no-op and the only observable side-effect is the SP advance.
        disp.set_te_feature_bit(te_handle, TrapDispatcher::TE_FEATURE_AUTO_SCROLL, false);

        bus.write_long(TEST_SP, te_handle);
        bus.write_word(TEST_SP + 4, 0xCAFE);
        bus.write_word(TEST_SP + 6, 0xBABE);
        let result = disp.dispatch_dialog(true, 0x011, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
        assert_eq!(bus.read_word(TEST_SP + 4), 0xCAFE);
        assert_eq!(bus.read_word(TEST_SP + 6), 0xBABE);
    }

    #[test]
    fn tegetoffset_point_above_destrect_returns_zero() {
        // Inside Macintosh Volume V (1986), p. V-172: TEGetOffset
        // returns the character offset of the start of the first line
        // when the point is above the first line. Pascal LR layout:
        //   sp+0..3 hTE (last pushed, shallowest)
        //   sp+4..5 pt.v
        //   sp+6..7 pt.h
        //   sp+8..9 INTEGER result slot
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"ABC");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET, 10);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2, 20);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4, 40);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 140);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, te_handle);
        bus.write_word(TEST_SP + 4, 5);
        bus.write_word(TEST_SP + 6, 30);
        bus.write_word(TEST_SP + 8, 0xBEEF);
        let result = disp.dispatch_dialog(true, 0x03C, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(TEST_SP + 8) as i16, 0);
    }

    #[test]
    fn tegetoffset_point_below_last_line_returns_telength() {
        // Inside Macintosh Volume V (1986), p. V-172: TEGetOffset
        // returns the character offset of the end of the text when
        // the point is below the last line. Witnesses that the HLE
        // computes a value that varies with the point arg (vs the
        // always-zero result the pre-fix off-by-2 read produced).
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"ABC");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET, 10);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2, 20);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4, 40);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 140);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, te_handle);
        bus.write_word(TEST_SP + 4, 200);
        bus.write_word(TEST_SP + 6, 30);
        bus.write_word(TEST_SP + 8, 0xBEEF);
        let result = disp.dispatch_dialog(true, 0x03C, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(TEST_SP + 8) as i16, 3);
    }

    #[test]
    fn tegetoffset_function_protocol_consumes_point_and_tehandle_args_writes_integer_result() {
        // Pascal FUNCTION protocol: 8 bytes of args (Point + TEHandle)
        // popped, 2-byte INTEGER result written at sp+8. Sentinel at
        // sp+10 must survive — the trap must not write past the
        // 2-byte result slot.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"ABC");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET, 10);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 2, 20);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 4, 40);
        bus.write_word(te_ptr + TrapDispatcher::TE_DEST_RECT_OFFSET + 6, 140);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, te_handle);
        bus.write_word(TEST_SP + 4, 5);
        bus.write_word(TEST_SP + 6, 30);
        bus.write_word(TEST_SP + 8, 0xDEAD);
        bus.write_word(TEST_SP + 10, 0xCAFE);
        let result = disp.dispatch_dialog(true, 0x03C, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_ne!(bus.read_word(TEST_SP + 8), 0xDEAD);
        assert_eq!(bus.read_word(TEST_SP + 10), 0xCAFE);
    }

    #[test]
    fn tefindword_returns_word_bounds_for_interior_positions() {
        // Inside Macintosh: Text (1993), pp. 2-60..2-61: TEFindWord
        // reports the word boundaries surrounding the current position.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"alpha beta");
        let te_ptr = bus.read_long(te_handle);

        cpu.write_reg(Register::A7, TEST_SP);
        cpu.write_reg(Register::D0, 1); // inside "alpha"
        cpu.write_reg(Register::D2, 0x1111);
        cpu.write_reg(Register::A3, te_ptr);
        cpu.write_reg(Register::A4, te_handle);

        let result = disp.dispatch_dialog(false, 0x0FE, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
        assert_eq!(cpu.read_reg(Register::D0) as u16, 0);
        assert_eq!(cpu.read_reg(Register::D1) as u16, 5);
    }

    #[test]
    fn tefindword_returns_second_word_bounds_without_touching_stack() {
        // Same TextEdit hook: a later position inside the second word
        // should return that word's bounds, and the register-based hook
        // must not consume any stack bytes.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"alpha beta");
        let te_ptr = bus.read_long(te_handle);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 0xCAFE);
        cpu.write_reg(Register::D0, 7); // inside "beta"
        cpu.write_reg(Register::D2, 0x2222);
        cpu.write_reg(Register::A3, te_ptr);
        cpu.write_reg(Register::A4, te_handle);

        let result = disp.dispatch_dialog(false, 0x0FE, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP);
        assert_eq!(bus.read_word(TEST_SP), 0xCAFE);
        assert_eq!(cpu.read_reg(Register::D0) as u16, 6);
        assert_eq!(cpu.read_reg(Register::D1) as u16, 10);
    }

    #[test]
    fn tesettext_copies_bytes_updates_length_and_pops_arguments() {
        // Inside Macintosh Volume I (1985), p. I-378: TESetText replaces an
        // edit record's text contents.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"OLD");
        let te_ptr = bus.read_long(te_handle);
        let source_ptr = bus.alloc(4);
        bus.write_bytes(source_ptr, b"NEW!");

        bus.write_long(TEST_SP, te_handle);
        bus.write_long(TEST_SP + 4, 4);
        bus.write_long(TEST_SP + 8, source_ptr);
        let result = disp.dispatch_dialog(true, 0x1CF, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);

        assert_eq!(
            TrapDispatcher::te_text_bytes(&bus, te_handle),
            b"NEW!".to_vec()
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET), 4);
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            4
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 4);
        let n_lines = u32::from(bus.read_word(te_ptr + TrapDispatcher::TE_N_LINES_OFFSET));
        assert!(n_lines > 0);
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_LINE_STARTS_OFFSET + n_lines * 2),
            4
        );
    }

    #[test]
    fn tesettext_nil_or_zero_length_input_clears_text() {
        // Inside Macintosh Volume I (1985), p. I-378: TESetText sets current
        // text contents; empty input yields empty text.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let te_ptr = bus.read_long(te_handle);

        bus.write_long(TEST_SP, te_handle);
        bus.write_long(TEST_SP + 4, 5);
        bus.write_long(TEST_SP + 8, 0);
        let result = disp.dispatch_dialog(true, 0x1CF, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
        assert_eq!(
            TrapDispatcher::te_text_bytes(&bus, te_handle),
            Vec::<u8>::new()
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET), 0);
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            0
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 0);
    }

    #[test]
    fn tesettext_replaces_prior_contents_via_sequential_call() {
        // Inside Macintosh Volume I (1985), p. I-378: TESetText *sets*
        // (not appends to) the current text contents. Mirrors B2 of the
        // a9cf_tesettext_strict bake: TESetText("WORLD!", 6) followed by
        // TESetText("HI", 2) on the same TERec yields teLength == 2
        // with the first two bytes equal to "HI".
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"OLD");
        let te_ptr = bus.read_long(te_handle);

        let first_ptr = bus.alloc(6);
        bus.write_bytes(first_ptr, b"WORLD!");
        bus.write_long(TEST_SP, te_handle);
        bus.write_long(TEST_SP + 4, 6);
        bus.write_long(TEST_SP + 8, first_ptr);
        let r1 = disp.dispatch_dialog(true, 0x1CF, &mut cpu, &mut bus);
        assert!(r1.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
        assert_eq!(
            TrapDispatcher::te_text_bytes(&bus, te_handle),
            b"WORLD!".to_vec()
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET), 6);

        cpu.write_reg(Register::A7, TEST_SP);
        let second_ptr = bus.alloc(2);
        bus.write_bytes(second_ptr, b"HI");
        bus.write_long(TEST_SP, te_handle);
        bus.write_long(TEST_SP + 4, 2);
        bus.write_long(TEST_SP + 8, second_ptr);
        let r2 = disp.dispatch_dialog(true, 0x1CF, &mut cpu, &mut bus);
        assert!(r2.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
        assert_eq!(
            TrapDispatcher::te_text_bytes(&bus, te_handle),
            b"HI".to_vec()
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_LENGTH_OFFSET), 2);
    }

    #[test]
    fn tesettext_balances_stackspace_with_pascal_protocol() {
        // Inside Macintosh Volume I (1985), p. I-378: TESetText is a
        // PROCEDURE with three args (Ptr text, LONGINT length, TEHandle).
        // Pascal LR push order yields a 12-byte arg frame and no result
        // slot. Mirrors B4 of the a9cf_tesettext_strict bake — A7 must
        // advance by exactly 12 bytes regardless of arg values.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"SEED");
        let source = bus.alloc(1);
        bus.write_bytes(source, b"X");

        bus.write_long(TEST_SP, te_handle);
        bus.write_long(TEST_SP + 4, 1);
        bus.write_long(TEST_SP + 8, source);
        bus.write_word(TEST_SP + 12, 0xCAFE);
        let result = disp.dispatch_dialog(true, 0x1CF, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 12);
        assert_eq!(bus.read_word(TEST_SP + 12), 0xCAFE);
        assert_eq!(
            TrapDispatcher::te_text_bytes(&bus, te_handle),
            b"X".to_vec()
        );
    }

    #[test]
    fn tekey_inserts_character_at_caret_and_advances_selection() {
        // Inside Macintosh Volume I (1985), p. I-385 and Text 1993, p. 2-81:
        // TEKey inserts typed characters at the insertion point.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HLO");
        let te_ptr = bus.read_long(te_handle);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 1);

        bus.write_long(TEST_SP, te_handle);
        bus.write_word(TEST_SP + 4, u16::from(b'E'));
        let result = disp.dispatch_dialog(true, 0x1DC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(
            TrapDispatcher::te_text_bytes(&bus, te_handle),
            b"HELO".to_vec()
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            2
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 2);
    }

    #[test]
    fn tekey_backspace_deletes_selection_or_previous_character() {
        // Inside Macintosh Volume I (1985), p. I-385 and Text 1993, p. 2-81:
        // backspace deletes current selection, or the previous character.
        let (mut disp, mut cpu, mut bus) = setup();
        let te_handle = make_te_with_text(&mut disp, &mut bus, b"HELLO");
        let te_ptr = bus.read_long(te_handle);

        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 1);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 3);
        bus.write_long(TEST_SP, te_handle);
        bus.write_word(TEST_SP + 4, 0x0008);
        let result = disp.dispatch_dialog(true, 0x1DC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(
            TrapDispatcher::te_text_bytes(&bus, te_handle),
            b"HLO".to_vec()
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            1
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 1);

        disp.te_set_text_contents(&mut bus, te_handle, b"HELLO");
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET, 2);
        bus.write_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET, 2);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, te_handle);
        bus.write_word(TEST_SP + 4, 0x0008);
        let result = disp.dispatch_dialog(true, 0x1DC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(
            TrapDispatcher::te_text_bytes(&bus, te_handle),
            b"HLLO".to_vec()
        );
        assert_eq!(
            bus.read_word(te_ptr + TrapDispatcher::TE_SEL_START_OFFSET),
            1
        );
        assert_eq!(bus.read_word(te_ptr + TrapDispatcher::TE_SEL_END_OFFSET), 1);
    }

    // ---- HideDialogItem / ShowDialogItem ($A827 / $A828) ----
    // Gate the move-offscreen-and-restore behaviour.

    #[test]
    fn hide_dialog_item_moves_rect_offscreen_and_saves_original() {
        // MTE 1992, 6-123: HideDialogItem offsets left/right by +16384.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x210000u32;
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 4,
                rect: (10, 20, 30, 120),
                text: "OK".into(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        // Stack: SP+0=itemNo(2), SP+2=dialog_ptr(4). Pop 6.
        bus.write_word(TEST_SP, 1);
        bus.write_long(TEST_SP + 2, dialog_ptr);

        let result = disp.dispatch_dialog(true, 0x027, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);

        // Rect is now offscreen.
        let item_rect = disp.dialog_items.get(&dialog_ptr).unwrap()[0].rect;
        assert!(
            item_rect.0 == 10 && item_rect.2 == 30,
            "vertical coordinates are unchanged by HideDialogItem, got {item_rect:?}"
        );
        assert!(
            item_rect.1 == 20 + 16384 && item_rect.3 == 120 + 16384,
            "hidden item rect must be offscreen, got {item_rect:?}"
        );
        // Original saved.
        assert_eq!(
            disp.hidden_dialog_item_rects.get(&(dialog_ptr, 1)).copied(),
            Some((10, 20, 30, 120))
        );
    }

    #[test]
    fn show_dialog_item_restores_saved_rect() {
        // MTE 1992, 6-124: ShowDialogItem restores pre-hide item rect.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x210000u32;
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 4,
                rect: (10, 20 + 16384, 30, 120 + 16384),
                text: "OK".into(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );
        disp.hidden_dialog_item_rects
            .insert((dialog_ptr, 1), (10, 20, 30, 120));

        bus.write_word(TEST_SP, 1);
        bus.write_long(TEST_SP + 2, dialog_ptr);

        let result = disp.dispatch_dialog(true, 0x028, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);

        assert_eq!(
            disp.dialog_items.get(&dialog_ptr).unwrap()[0].rect,
            (10, 20, 30, 120)
        );
        assert!(
            !disp.hidden_dialog_item_rects.contains_key(&(dialog_ptr, 1)),
            "saved rect must be removed after show"
        );
    }

    #[test]
    fn hide_then_show_is_idempotent_roundtrip() {
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x210000u32;
        let orig_rect = (50, 60, 80, 200);
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 4,
                rect: orig_rect,
                text: "Cancel".into(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        bus.write_word(TEST_SP, 1);
        bus.write_long(TEST_SP + 2, dialog_ptr);
        disp.dispatch_dialog(true, 0x027, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        // Reset SP + args for Show.
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 1);
        bus.write_long(TEST_SP + 2, dialog_ptr);
        disp.dispatch_dialog(true, 0x028, &mut cpu, &mut bus)
            .unwrap()
            .unwrap();

        assert_eq!(
            disp.dialog_items.get(&dialog_ptr).unwrap()[0].rect,
            orig_rect,
            "round-trip hide→show must restore the exact original rect"
        );
    }

    #[test]
    fn hide_dialog_item_already_hidden_left_coord_is_noop() {
        // MTE 1992, 6-123: left > 8192 means the item is already hidden.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x210000u32;
        let already_hidden = (10, 9000, 30, 9200);
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 4,
                rect: already_hidden,
                text: "Hidden".into(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        bus.write_word(TEST_SP, 1);
        bus.write_long(TEST_SP + 2, dialog_ptr);

        let result = disp.dispatch_dialog(true, 0x027, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(
            disp.dialog_items.get(&dialog_ptr).unwrap()[0].rect,
            already_hidden
        );
        assert!(
            !disp.hidden_dialog_item_rects.contains_key(&(dialog_ptr, 1)),
            "already hidden items must not record a new saved rect"
        );
    }

    #[test]
    fn show_dialog_item_without_saved_rect_subtracts_offset_when_hidden() {
        // MTE 1992, 6-124: ShowDialogItem uses left > 8192 as hidden predicate.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x210000u32;
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 4,
                rect: (10, 30 + 16384, 30, 130 + 16384),
                text: "ShowMe".into(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        bus.write_word(TEST_SP, 1);
        bus.write_long(TEST_SP + 2, dialog_ptr);

        let result = disp.dispatch_dialog(true, 0x028, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(
            disp.dialog_items.get(&dialog_ptr).unwrap()[0].rect,
            (10, 30, 30, 130)
        );
    }

    #[test]
    fn show_dialog_item_visible_left_coord_is_noop() {
        // MTE 1992, 6-124: left < 8192 means already visible; no-op.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x210000u32;
        let visible = (10, 20, 30, 120);
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 4,
                rect: visible,
                text: "Visible".into(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        bus.write_word(TEST_SP, 1);
        bus.write_long(TEST_SP + 2, dialog_ptr);
        let result = disp.dispatch_dialog(true, 0x028, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 6);
        assert_eq!(disp.dialog_items.get(&dialog_ptr).unwrap()[0].rect, visible);
    }

    // ---- FindDItem ($A984) ----

    #[test]
    fn findditem_returns_zero_based_index_for_hit_and_pops_stack() {
        // MTE 1992, 6-125: FindDialogItem/FindDItem returns 0 for the first
        // item, 1 for the second, etc.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x220000u32;
        disp.dialog_items.insert(
            dialog_ptr,
            vec![
                DialogItem {
                    item_type: 4,
                    rect: (10, 20, 30, 40),
                    text: "A".into(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
                DialogItem {
                    item_type: 4,
                    rect: (40, 50, 70, 90),
                    text: "B".into(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
            ],
        );

        bus.write_word(TEST_SP, 45);
        bus.write_word(TEST_SP + 2, 55);
        bus.write_long(TEST_SP + 4, dialog_ptr);
        let result = disp.dispatch_dialog(true, 0x184, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(TEST_SP + 8) as i16, 1);
    }

    #[test]
    fn findditem_point_outside_all_items_returns_minus_one() {
        // IM:IV 1986, p. IV-60: FindDItem returns -1 when the point does not
        // lie within any item rectangle.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x220100u32;
        disp.dialog_items.insert(
            dialog_ptr,
            vec![DialogItem {
                item_type: 4,
                rect: (10, 20, 30, 40),
                text: "A".into(),
                resource_id: 0,
                proc_ptr: 0,
                sel_start: 0,
                sel_end: 0,
            }],
        );

        bus.write_word(TEST_SP, 99);
        bus.write_word(TEST_SP + 2, 99);
        bus.write_long(TEST_SP + 4, dialog_ptr);
        let result = disp.dispatch_dialog(true, 0x184, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(TEST_SP + 8) as i16, -1);
    }

    #[test]
    fn findditem_returns_first_overlapping_item_and_includes_disabled_items() {
        // IM:IV 1986, p. IV-60: overlap resolution is first item in list.
        // IM:IV 1986, p. IV-60 note: disabled items are still returned.
        let (mut disp, mut cpu, mut bus) = setup();
        let dialog_ptr = 0x220200u32;
        disp.dialog_items.insert(
            dialog_ptr,
            vec![
                DialogItem {
                    item_type: 0x80 | 4, // disabled
                    rect: (10, 20, 40, 60),
                    text: "DisabledTop".into(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
                DialogItem {
                    item_type: 4,
                    rect: (20, 30, 50, 70),
                    text: "EnabledBottom".into(),
                    resource_id: 0,
                    proc_ptr: 0,
                    sel_start: 0,
                    sel_end: 0,
                },
            ],
        );

        // Point lies within both rectangles.
        bus.write_word(TEST_SP, 25);
        bus.write_word(TEST_SP + 2, 35);
        bus.write_long(TEST_SP + 4, dialog_ptr);
        let result = disp.dispatch_dialog(true, 0x184, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 8);
        assert_eq!(bus.read_word(TEST_SP + 8) as i16, 0);
    }

    // ========== Cursor Manager ==========

    // ---- InitCursor ($A850) ----

    #[test]
    fn init_cursor_resets_cursor_level_to_zero_and_sets_arrow_visible() {
        // IM:I I-167: InitCursor sets arrow cursor, sets cursor level to 0,
        // and makes cursor visible.
        let (mut disp, mut cpu, mut bus) = setup();

        // Start from a hidden nested level to prove InitCursor reset.
        disp.cursor_data = None;
        disp.cursor_level = -3;
        disp.cursor_visible = false;

        let result = disp.dispatch_dialog(true, 0x050, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert!(disp.cursor_data.is_some());
        assert_eq!(disp.cursor_level, 0);
        assert!(disp.cursor_visible);
    }

    // ---- SetCursor ($A851) ----

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

        let crsr_ptr = 0x300000u32;

        // Write cursor bitmap data (32 bytes)
        for i in 0..32u32 {
            bus.write_byte(crsr_ptr + i, 0xAA);
        }
        // Write cursor mask data (32 bytes)
        for i in 0..32u32 {
            bus.write_byte(crsr_ptr + 32 + i, 0xFF);
        }
        // Write hotspot
        bus.write_word(crsr_ptr + 64, 5); // hot_v
        bus.write_word(crsr_ptr + 66, 3); // hot_h

        // SP+0: crsr_ptr (4 bytes)
        bus.write_long(TEST_SP, crsr_ptr);

        let result = disp.dispatch_dialog(true, 0x051, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);

        let (data, mask, hot_v, hot_h) = disp.cursor_data().unwrap();
        assert!(data.iter().all(|&b| b == 0xAA));
        assert!(mask.iter().all(|&b| b == 0xFF));
        assert_eq!(hot_v, 5);
        assert_eq!(hot_h, 3);
    }

    #[test]
    fn set_cursor_does_not_force_visibility_when_cursor_is_hidden() {
        // IM:I I-167: if the cursor is hidden, SetCursor changes the
        // current cursor image but it remains hidden until uncovered.
        let (mut disp, mut cpu, mut bus) = setup();
        let crsr_ptr = 0x300100u32;
        for i in 0..32u32 {
            bus.write_byte(crsr_ptr + i, 0x11);
            bus.write_byte(crsr_ptr + 32 + i, 0x22);
        }
        bus.write_word(crsr_ptr + 64, 9);
        bus.write_word(crsr_ptr + 66, 4);
        bus.write_long(TEST_SP, crsr_ptr);

        disp.cursor_level = -1;
        disp.cursor_visible = false;
        let result = disp.dispatch_dialog(true, 0x051, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(disp.cursor_level, -1);
        assert!(!disp.cursor_visible);
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 4);
    }

    // ---- HideCursor ($A852) ----

    #[test]
    fn hide_cursor_decrements_level_and_hides_cursor() {
        // IM:I I-168: HideCursor decrements cursor level (from 0 to -1)
        // and removes the cursor from the screen.
        let (mut disp, mut cpu, mut bus) = setup();
        disp.cursor_level = 0;
        disp.cursor_visible = true;

        let result = disp.dispatch_dialog(true, 0x052, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(disp.cursor_level, -1);
        assert!(!disp.cursor_visible);
    }

    // ---- ShowCursor ($A853) ----

    #[test]
    fn show_cursor_balances_hidecursor_and_reveals_at_level_zero() {
        // IM:I I-168: ShowCursor increments toward 0 and only shows the
        // cursor when level becomes 0.
        let (mut disp, mut cpu, mut bus) = setup();
        disp.cursor_level = -2;
        disp.cursor_visible = false;

        let result = disp.dispatch_dialog(true, 0x053, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(disp.cursor_level, -1);
        assert!(!disp.cursor_visible);

        let result = disp.dispatch_dialog(true, 0x053, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(disp.cursor_level, 0);
        assert!(disp.cursor_visible);
    }

    #[test]
    fn show_cursor_at_level_zero_is_noop() {
        // IM:I I-168: extra ShowCursor calls have no effect and do not
        // increment cursor level above 0.
        let (mut disp, mut cpu, mut bus) = setup();
        disp.cursor_level = 0;
        disp.cursor_visible = true;

        let result = disp.dispatch_dialog(true, 0x053, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(disp.cursor_level, 0);
        assert!(disp.cursor_visible);
    }

    // ---- ObscureCursor ($A856) ----

    #[test]
    fn obscure_cursor_noop_preserves_cursor_level_visibility_and_stack() {
        // IM:I I-168: ObscureCursor has no effect on cursor level and takes
        // no arguments. Systemless's HLE compromise keeps it as a no-op because
        // synthesized mouse-move events would immediately un-obscure anyway.
        let (mut disp, mut cpu, mut bus) = setup();
        disp.cursor_level = -1;
        disp.cursor_visible = false;
        let sp_before = cpu.read_reg(Register::A7);

        let result = disp.dispatch_dialog(true, 0x056, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), sp_before);
        assert_eq!(disp.cursor_level, -1);
        assert!(!disp.cursor_visible);
    }

    #[test]
    fn obscure_cursor_five_call_composition_preserves_stack_pointer_and_level() {
        // Mirrors B2 of the a855_a856_shieldcursor_obscurecursor_strict
        // bake: 5 successive ObscureCursor dispatches inside one
        // StackSpace sandwich leave SP unchanged. Per IM:I I-168 the
        // PROCEDURE has no arguments and no result slot, so each call
        // pops 0 bytes; the cumulative SP delta after N calls is zero.
        // Also pins the cursor-level no-effect contract per IM:I I-168:
        // ObscureCursor "has no effect on the cursor level and must
        // not be balanced by a call to ShowCursor."
        let (mut disp, mut cpu, mut bus) = setup();
        disp.cursor_level = -2;
        disp.cursor_visible = false;
        let sp_before = cpu.read_reg(Register::A7);

        for i in 0..5 {
            let result = disp.dispatch_dialog(true, 0x056, &mut cpu, &mut bus);
            assert!(result.unwrap().is_ok(), "iteration {i} dispatch failed");
            assert_eq!(
                cpu.read_reg(Register::A7),
                sp_before,
                "iteration {i}: ObscureCursor must leave SP unchanged (0-byte pop)",
            );
            assert_eq!(
                disp.cursor_level, -2,
                "iteration {i}: ObscureCursor must NOT change cursor level per IM:I I-168",
            );
        }
    }

    // ---- GetCursor ($A9B9) — IM:I I-474 contract ----

    #[test]
    fn get_cursor_returns_handle_for_system_cursor() {
        // crossCursor (ID 2) is one of the four standard system
        // cursors per IM:I I-475..I-477. Systemless synthesises it via
        // [`TrapDispatcher::synthesize_system_cursor`] because the
        // System file's resource fork isn't loaded; the result must
        // be a non-NIL handle whose master ptr's bitmap matches the
        // synthesised crosshair.
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 2); // crossCursor

        let _ = disp.dispatch_dialog(true, 0x1B9, &mut cpu, &mut bus);
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);

        let handle = bus.read_long(TEST_SP + 2);
        assert_ne!(
            handle, 0,
            "GetCursor(crossCursor) must return non-NIL handle for a built-in system cursor"
        );
        let crsr = bus.read_long(handle);
        let hot_v = bus.read_word(crsr + 64) as i16;
        let hot_h = bus.read_word(crsr + 66) as i16;
        assert_eq!(
            (hot_v, hot_h),
            (7, 7),
            "crossCursor's hotspot is documented at (7,7) in IM:I I-475"
        );
    }

    #[test]
    fn get_cursor_returns_nil_for_unknown_id_per_im_i_474() {
        // IM:I I-474: "If the resource can't be read, GetCursor
        // returns NIL." For an ID that's neither a CURS resource we
        // have loaded nor one of the four standard built-ins, the
        // miss path is NIL — not a fresh empty 68-byte block. Apps
        // that defensively check `if handle = NIL then use_arrow
        // else SetCursor(handle^^)` would otherwise SetCursor a
        // blank cursor onto the screen.
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 999); // not a system cursor, no CURS 999 installed

        let _ = disp.dispatch_dialog(true, 0x1B9, &mut cpu, &mut bus);
        assert_eq!(
            cpu.read_reg(Register::A7),
            TEST_SP + 2,
            "GetCursor must pop 2 bytes (cursorID INTEGER)"
        );
        assert_eq!(
            bus.read_long(TEST_SP + 2),
            0,
            "GetCursor must return NIL when CURS resource is missing per IM:I I-474"
        );
    }

    #[test]
    fn get_cursor_returns_stable_handle_across_repeated_calls_for_system_cursor() {
        // Apps cache GetCursor results at boot and pass them to
        // SetCursor every frame; without handle stability the
        // dispatcher would leak a 68-byte block per call. Pin the
        // cache hit so a future "tighten alloc" change doesn't
        // accidentally drop the cache.
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 4); // watchCursor

        let _ = disp.dispatch_dialog(true, 0x1B9, &mut cpu, &mut bus);
        let h1 = bus.read_long(TEST_SP + 2);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 4);

        let _ = disp.dispatch_dialog(true, 0x1B9, &mut cpu, &mut bus);
        let h2 = bus.read_long(TEST_SP + 2);
        assert_eq!(
            h1, h2,
            "GetCursor must return the same handle for repeated calls on a system cursor ID"
        );
    }

    #[test]
    fn get_cursor_returns_handle_to_loaded_curs_resource() {
        // With a CURS resource installed, GetCursor must return a
        // handle whose master ptr equals the loaded resource bytes
        // — same value-asserting gate as get_icon_returns_handle_to_loaded_icon_resource.
        let (mut disp, mut cpu, mut bus) = setup();
        // CURS records are 68 bytes (32 data + 32 mask + 4 hotSpot).
        let curs_data: Vec<u8> = (0..68).map(|i| (i as u8).wrapping_mul(11)).collect();
        let res_ptr = disp.install_test_resource(&mut bus, *b"CURS", 200, &curs_data);
        bus.write_word(TEST_SP, 200);

        let _ = disp.dispatch_dialog(true, 0x1B9, &mut cpu, &mut bus);
        let handle = bus.read_long(TEST_SP + 2);
        assert_ne!(
            handle, 0,
            "GetCursor must return non-NIL handle when CURS resource is loaded"
        );
        assert_eq!(
            bus.read_long(handle),
            res_ptr,
            "GetCursor's handle must dereference to the loaded CURS resource bytes"
        );
    }

    // ---- GetPattern ($A9B8) — IM:I I-473 contract ----

    #[test]
    fn get_pattern_returns_nil_for_missing_resource_per_im_i_473() {
        // IM:I I-473: "If the resource can't be read, GetPattern
        // returns NIL." The previous Stub returned a handle to a
        // fresh all-0xFF (white) 8-byte block, strictly worse than
        // NIL because callers branching on `handle = NIL` took the
        // FillRect-with-white path instead of the recovery path.
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 1); // patID = 1, no PAT 1 installed

        let _ = disp.dispatch_dialog(true, 0x1B8, &mut cpu, &mut bus);
        assert_eq!(
            cpu.read_reg(Register::A7),
            TEST_SP + 2,
            "GetPattern must pop 2 bytes (patID INTEGER)"
        );
        assert_eq!(
            bus.read_long(TEST_SP + 2),
            0,
            "GetPattern must return NIL when PAT resource is missing per IM:I I-473"
        );
    }

    #[test]
    fn get_pattern_returns_handle_to_loaded_pat_resource() {
        // With a PAT resource installed, GetPattern must return a
        // handle whose master ptr equals the loaded resource bytes.
        // Pin master-ptr-equality so a future regression that
        // allocates fresh memory instead of reusing the loaded ptr
        // fails here. Same gate shape as
        // get_icon_returns_handle_to_loaded_icon_resource.
        let (mut disp, mut cpu, mut bus) = setup();
        let pat_data: [u8; 8] = [0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA];
        let res_ptr = disp.install_test_resource(&mut bus, *b"PAT ", 16, &pat_data);
        bus.write_word(TEST_SP, 16);

        let _ = disp.dispatch_dialog(true, 0x1B8, &mut cpu, &mut bus);
        let handle = bus.read_long(TEST_SP + 2);
        assert_ne!(
            handle, 0,
            "GetPattern must return non-NIL handle when PAT resource is loaded"
        );
        assert_eq!(
            bus.read_long(handle),
            res_ptr,
            "GetPattern's handle must dereference to the loaded PAT resource bytes \
             (not a fresh all-0xFF allocation)"
        );
        // Verify the bytes through the handle deref are the
        // installed sentinel pattern (50/AA stripes), not 0xFF.
        let master = bus.read_long(handle);
        for (i, want) in pat_data.iter().enumerate() {
            assert_eq!(
                bus.read_byte(master + i as u32),
                *want,
                "GetPattern PAT byte +{i} must match installed resource byte"
            );
        }
    }

    #[test]
    fn get_pattern_returns_stable_handle_across_repeated_calls() {
        // Apps that paint with a pattern in tight loops cache the
        // GetPattern result; pin handle stability so a future
        // regression that drops `get_or_create_resource_handle` for
        // a fresh `bus.alloc(4)` per call surfaces here.
        let (mut disp, mut cpu, mut bus) = setup();
        let pat_data: [u8; 8] = [0x0F, 0xF0, 0x0F, 0xF0, 0x0F, 0xF0, 0x0F, 0xF0];
        let _ = disp.install_test_resource(&mut bus, *b"PAT ", 100, &pat_data);

        bus.write_word(TEST_SP, 100);
        let _ = disp.dispatch_dialog(true, 0x1B8, &mut cpu, &mut bus);
        let h1 = bus.read_long(TEST_SP + 2);
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 100);

        let _ = disp.dispatch_dialog(true, 0x1B8, &mut cpu, &mut bus);
        let h2 = bus.read_long(TEST_SP + 2);
        assert_eq!(
            h1, h2,
            "GetPattern must return the same handle for repeated calls on a loaded PAT resource"
        );
    }

    // ---- GetIcon ($A9BB) — IM:I I-473 contract ----

    #[test]
    fn get_icon_returns_nil_when_resource_missing() {
        // IM:I I-473: "If the resource can't be read, GetIcon
        // returns NIL." Critical for apps that defensively check
        // `if handle = NIL` before dereferencing — the prior Stub
        // always returned a non-NIL handle to uninitialised
        // memory which broke that branch.
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 1); // iconID = 1, no ICON 1 installed
        let _ = disp.dispatch_dialog(true, 0x1BB, &mut cpu, &mut bus);
        assert_eq!(
            cpu.read_reg(Register::A7),
            TEST_SP + 2,
            "GetIcon must pop 2 bytes (iconID INTEGER)"
        );
        assert_eq!(
            bus.read_long(TEST_SP + 2),
            0,
            "GetIcon must return NIL when ICON resource is missing per IM:I I-473"
        );
    }

    #[test]
    fn get_icon_returns_handle_to_loaded_icon_resource() {
        // With an ICON resource installed, GetIcon must return a
        // non-NIL handle whose master ptr points at the loaded
        // resource bytes. Pin the master-ptr-equality so a future
        // regression that allocates fresh memory instead of
        // reusing the loaded ptr fails here.
        let (mut disp, mut cpu, mut bus) = setup();
        // Real ICON resources are 128 bytes (32x32 1bpp); fill
        // with a recognisable sentinel pattern so handle deref
        // assertions can verify "this is the right resource."
        let icon_data: Vec<u8> = (0..128).map(|i| (i as u8).wrapping_mul(7)).collect();
        let res_ptr = disp.install_test_resource(&mut bus, *b"ICON", 128, &icon_data);
        bus.write_word(TEST_SP, 128);

        let _ = disp.dispatch_dialog(true, 0x1BB, &mut cpu, &mut bus);
        let handle = bus.read_long(TEST_SP + 2);
        assert_ne!(
            handle, 0,
            "GetIcon must return a non-NIL handle when ICON resource is loaded"
        );
        assert_eq!(
            bus.read_long(handle),
            res_ptr,
            "GetIcon's handle must dereference to the loaded ICON resource bytes \
             (not a fresh uninitialised allocation)"
        );
        // Verify the bytes through the handle deref are the
        // installed sentinel pattern.
        let master = bus.read_long(handle);
        for (i, want) in icon_data.iter().enumerate() {
            assert_eq!(
                bus.read_byte(master + i as u32),
                *want,
                "GetIcon ICON byte +{i} must match installed resource byte"
            );
        }
    }

    #[test]
    fn get_icon_returns_stable_handle_across_repeated_calls() {
        // Per IM:Resource Manager, GetResource returns the SAME
        // handle on repeat calls (unless ReleaseResource has run).
        // GetIcon delegates to GetResource per IM:I I-473, so it
        // must inherit this stability — apps that cache the handle
        // depend on it not changing between alert cycles.
        let (mut disp, mut cpu, mut bus) = setup();
        let icon_data: Vec<u8> = vec![0xCC; 128];
        disp.install_test_resource(&mut bus, *b"ICON", 200, &icon_data);

        bus.write_word(TEST_SP, 200);
        let _ = disp.dispatch_dialog(true, 0x1BB, &mut cpu, &mut bus);
        let h1 = bus.read_long(TEST_SP + 2);

        // Reset SP and call again with the same iconID.
        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 200);
        let _ = disp.dispatch_dialog(true, 0x1BB, &mut cpu, &mut bus);
        let h2 = bus.read_long(TEST_SP + 2);

        assert_ne!(h1, 0, "first GetIcon must return non-NIL");
        assert_eq!(
            h1, h2,
            "GetIcon must return the SAME handle on repeated calls per IM:Resource Mgr stability"
        );
    }

    #[test]
    fn get_icon_pops_two_bytes_per_pascal_signature() {
        // FUNCTION GetIcon(iconID: INTEGER): Handle;
        // Pascal stack frame: result Handle (4) pre-pushed by
        // caller, iconID (2) on top. Trap pops 2 bytes (iconID),
        // result lands at new SP+0 (= old SP+2).
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 999); // any iconID
        let pre_a7 = cpu.read_reg(Register::A7);
        let _ = disp.dispatch_dialog(true, 0x1BB, &mut cpu, &mut bus);
        assert_eq!(
            cpu.read_reg(Register::A7),
            pre_a7 + 2,
            "GetIcon must advance A7 by 2 bytes (iconID INTEGER)"
        );
    }

    // ---- GetPicture ($A9BC) ----

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

        // SP+0: picture id (2 bytes)
        bus.write_word(TEST_SP, 1);

        let result = disp.dispatch_dialog(true, 0x1BC, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);

        let handle = bus.read_long(TEST_SP + 2);
        assert_eq!(handle, 0); // NIL when no PICT resource loaded
    }

    #[test]
    fn get_picture_reloads_released_resource_from_open_resource_file() {
        fn make_resource_fork_bytes(resources: &[([u8; 4], i16, &[u8])]) -> Vec<u8> {
            let mut type_groups: Vec<([u8; 4], Vec<(i16, &[u8], u32)>)> = Vec::new();
            for (res_type, res_id, data) in resources {
                let group_idx = type_groups
                    .iter()
                    .position(|(existing_type, _)| existing_type == res_type)
                    .unwrap_or_else(|| {
                        type_groups.push((*res_type, Vec::new()));
                        type_groups.len() - 1
                    });
                type_groups[group_idx].1.push((*res_id, *data, 0));
            }
            type_groups.sort_by_key(|(res_type, _)| *res_type);
            for (_, entries) in &mut type_groups {
                entries.sort_by_key(|(res_id, _, _)| *res_id);
            }

            let data_offset = 16u32;
            let mut data_section = Vec::new();
            for (_, entries) in &mut type_groups {
                for (_, data, data_pos) in entries {
                    *data_pos = data_section.len() as u32;
                    data_section.extend_from_slice(&(data.len() as u32).to_be_bytes());
                    data_section.extend_from_slice(data);
                }
            }

            let map_offset = data_offset + data_section.len() as u32;
            let type_list_offset = 30u16;
            let type_count = type_groups.len();
            let resource_count: usize = type_groups.iter().map(|(_, entries)| entries.len()).sum();
            let ref_lists_offset = 2 + type_count * 8;
            let name_list_offset =
                type_list_offset as usize + ref_lists_offset + resource_count * 12;
            let map_length = name_list_offset as u32;

            let mut bytes = vec![0u8; (map_offset + map_length) as usize];
            let mut header = [0u8; 16];
            header[0..4].copy_from_slice(&data_offset.to_be_bytes());
            header[4..8].copy_from_slice(&map_offset.to_be_bytes());
            header[8..12].copy_from_slice(&(data_section.len() as u32).to_be_bytes());
            header[12..16].copy_from_slice(&map_length.to_be_bytes());
            bytes[0..16].copy_from_slice(&header);
            bytes[data_offset as usize..data_offset as usize + data_section.len()]
                .copy_from_slice(&data_section);

            let map_start = map_offset as usize;
            bytes[map_start..map_start + 16].copy_from_slice(&header);
            bytes[map_start + 24..map_start + 26].copy_from_slice(&type_list_offset.to_be_bytes());
            bytes[map_start + 26..map_start + 28]
                .copy_from_slice(&(name_list_offset as u16).to_be_bytes());
            bytes[map_start + 28..map_start + 30]
                .copy_from_slice(&((type_count as u16) - 1).to_be_bytes());

            let type_list_start = map_start + type_list_offset as usize;
            bytes[type_list_start..type_list_start + 2]
                .copy_from_slice(&((type_count as u16) - 1).to_be_bytes());
            let mut next_ref_list_offset = ref_lists_offset;
            for (i, (res_type, entries)) in type_groups.iter().enumerate() {
                let type_entry = type_list_start + 2 + i * 8;
                bytes[type_entry..type_entry + 4].copy_from_slice(res_type);
                bytes[type_entry + 4..type_entry + 6]
                    .copy_from_slice(&((entries.len() as u16) - 1).to_be_bytes());
                bytes[type_entry + 6..type_entry + 8]
                    .copy_from_slice(&(next_ref_list_offset as u16).to_be_bytes());

                let ref_list_start = type_list_start + next_ref_list_offset;
                for (j, (res_id, _, data_pos)) in entries.iter().enumerate() {
                    let ref_entry = ref_list_start + j * 12;
                    bytes[ref_entry..ref_entry + 2]
                        .copy_from_slice(&(*res_id as u16).to_be_bytes());
                    bytes[ref_entry + 2..ref_entry + 4].copy_from_slice(&0xFFFFu16.to_be_bytes());
                    bytes[ref_entry + 4] = 0;
                    let data_offset_bytes = data_pos.to_be_bytes();
                    bytes[ref_entry + 5..ref_entry + 8].copy_from_slice(&data_offset_bytes[1..4]);
                }

                next_ref_list_offset += entries.len() * 12;
            }

            bytes
        }

        let (mut disp, mut cpu, mut bus) = setup();
        let pict_data = b"\x00\x11Wordtris reloadable picture data";
        let fork = make_resource_fork_bytes(&[(*b"PICT", 1127, &pict_data[..])]);
        disp.vfs_rsrc.insert("Pictures".to_string(), fork);
        disp.open_resource_file_from_vfs_key(&mut bus, "Pictures", false);

        bus.write_word(TEST_SP, 1127);
        let first = disp.dispatch_dialog(true, 0x1BC, &mut cpu, &mut bus);
        assert!(first.unwrap().is_ok());
        let first_handle = bus.read_long(TEST_SP + 2);
        assert_ne!(first_handle, 0, "first GetPicture must find PICT 1127");
        let first_ptr = bus.read_long(first_handle);
        assert_eq!(bus.read_bytes(first_ptr, pict_data.len()), pict_data);

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_long(TEST_SP, first_handle);
        let release = disp.dispatch_resource(true, 0x1A3, &mut cpu, &mut bus);
        assert!(release.unwrap().is_ok());
        assert_eq!(
            bus.read_long(first_handle),
            0,
            "ReleaseResource should clear the old master pointer"
        );

        cpu.write_reg(Register::A7, TEST_SP);
        bus.write_word(TEST_SP, 1127);
        let second = disp.dispatch_dialog(true, 0x1BC, &mut cpu, &mut bus);
        assert!(second.unwrap().is_ok());
        let second_handle = bus.read_long(TEST_SP + 2);
        assert_ne!(
            second_handle, 0,
            "GetPicture must reload a released PICT from the open resource file"
        );
        assert_ne!(
            second_handle, first_handle,
            "a released resource should be returned through a fresh handle"
        );
        let second_ptr = bus.read_long(second_handle);
        assert_eq!(bus.read_bytes(second_ptr, pict_data.len()), pict_data);
    }

    // ---- GetString ($A9BA) ----

    #[test]
    fn get_string_returns_loaded_str_resource_handle() {
        let (mut disp, mut cpu, mut bus) = setup();
        let str_ptr = bus.alloc(6);
        bus.write_byte(str_ptr, 5);
        bus.write_bytes(str_ptr + 1, b"Hello");
        disp.resources = Some(crate::trap::dispatch::LoadedResources {
            files: std::collections::HashMap::from([(
                0,
                crate::trap::dispatch::ResourceFileMap {
                    loaded: std::collections::HashMap::from([((*b"STR ", 1), str_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::new(),
            search_order: vec![0],
            current_file: 0,
        });

        // SP+0: string id (2 bytes)
        bus.write_word(TEST_SP, 1);

        let result = disp.dispatch_dialog(true, 0x1BA, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);

        let handle = bus.read_long(TEST_SP + 2);
        assert_ne!(handle, 0);
        assert_eq!(bus.read_long(handle), str_ptr);
    }

    #[test]
    fn get_string_returns_nil_when_resource_is_missing() {
        let (mut disp, mut cpu, mut bus) = setup();
        bus.write_word(TEST_SP, 999);

        let result = disp.dispatch_dialog(true, 0x1BA, &mut cpu, &mut bus);
        assert!(result.unwrap().is_ok());
        assert_eq!(cpu.read_reg(Register::A7), TEST_SP + 2);
        assert_eq!(bus.read_long(TEST_SP + 2), 0);
    }

    // ---- Unhandled trap returns None ----

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

        let result = disp.dispatch_dialog(true, 0xFFFF, &mut cpu, &mut bus);
        assert!(result.is_none());
    }
}
