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

use super::dispatch::{DialogItem, DialogPopupTrackingState, DialogTrackingState};
use super::types::{Rect, ShapeOp};
use crate::cpu::{CpuOps, Register};
use crate::memory::{MacMemoryBus, MemoryBus};
use crate::quickdraw::fonts::get_font_face_scaled;
use crate::quickdraw::text::get_font_metrics;
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();

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())
}

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

    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;
    #[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_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 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 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 {
        if item_handle == 0 {
            return String::new();
        }

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

        let len = bus.get_alloc_size(data_ptr).unwrap_or(0) as usize;
        let bytes = bus.read_bytes(data_ptr, len);
        String::from_utf8_lossy(&bytes).to_string()
    }

    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 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 data_len = bus.read_byte(ditl_ptr + offset + 1) as u32;
            offset += 2; // itmtype + itmlen
            let padded = (data_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);
        }
    }

    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 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 (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 {
                    rect_width
                        .saturating_sub(self.te_measure_text_width(
                            font,
                            size.max(1),
                            &text_bytes,
                            line_start,
                            line_end,
                        ))
                        .saturating_sub(1)
                };
                if just == 1 {
                    line_width / 2
                } else {
                    line_width
                }
            }
            _ => 0,
        };
        let x = if on_break {
            dest_rect.1.saturating_add(left_offset)
        } else {
            dest_rect
                .1
                .saturating_add(left_offset)
                .saturating_add(self.te_measure_text_width(
                    font,
                    size.max(1),
                    &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 (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 line_width = rect_width
                    .saturating_sub(self.te_measure_text_width(
                        font,
                        size.max(1),
                        &text_bytes,
                        line_start,
                        line_end,
                    ))
                    .saturating_sub(1);
                if just == 1 {
                    line_width / 2
                } else {
                    line_width
                }
            }
            _ => 0,
        };

        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 = self.te_char_width(font, size.max(1), *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 metrics = get_font_metrics(self.tx_font, self.tx_size.max(1));
            return (
                self.tx_font,
                self.tx_face,
                self.tx_size.max(1),
                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 = bus.read_word(te_ptr + Self::TE_TX_SIZE_OFFSET) as i16;
        let metrics = get_font_metrics(tx_font, tx_size.max(1));
        (
            tx_font,
            tx_face,
            tx_size.max(1),
            self.fg_color,
            metrics.ascent + metrics.descent + metrics.leading,
            metrics.ascent,
        )
    }

    #[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.tx_size.max(1));
            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.tx_size.max(1));
        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_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 metrics = get_font_metrics(self.tx_font, self.tx_size.max(1));
        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,
                self.tx_size.max(1) 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,
                self.tx_size.max(1) 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_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)
    }

    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 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 (_face, scale) = get_font_face_scaled(font, size.max(1));
        let ch = byte as char;
        if let Some((glyph, _)) = crate::quickdraw::text::get_glyph(font, size.max(1), 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 lines = if text_bytes.is_empty() {
            Vec::new()
        } else {
            self.te_wrap_lines(
                font,
                size.max(1),
                &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;
        }
        let styled = Self::te_is_styled_record(bus, te_ptr);

        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 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,
                                );
                            }
                        }
                    }
                }
            }
        }

        self.te_reset_styled_metadata(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, // 0 / -2 / any other → flush left
        }
    }

    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);
        if text_bytes.is_empty() {
            return;
        }

        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);
        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 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.max(1);
        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 = self.te_wrap_lines(font, size.max(1), &text_bytes, box_width);

        let mut top = dest_rect.0;
        let mut visible_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 =
                self.te_measure_text_width(font, size.max(1), &text_bytes, *start, trimmed_end);
            let x = Self::te_line_origin_x(just, dest_rect.1, dest_rect.3, line_width);
            self.pn_loc = (top.saturating_add(line_ascent), x);
            for &byte in &text_bytes[*start..trimmed_end] {
                self.draw_char(cpu, bus, byte as char);
            }
            top = line_bottom;
        }

        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],
    ) {
        // NewDialog/GetNewDialog duplicates the DITL and replaces itmhand with
        // live item handles (text handles, control handles, picture handles).
        // Inside Macintosh Volume I, I-412; executor dialCreate.cpp
        for (index, item) in items.iter().enumerate() {
            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 => bus.read_long(item_handle_addr),
                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 => 0,
                _ => 0,
            };

            bus.write_long(item_handle_addr, item_handle);
        }
    }

    // ========== 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 = String::from_utf8_lossy(&title_bytes).to_string();

        // 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 = bus.read_word(ptr + padded_end);

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

    /// 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> {
        let max_index = bus.read_word(ptr) as i16; // number of items minus 1
        let count = (max_index + 1) as usize;
        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) as u32;
            offset += 2;

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

            let mut text = String::new();
            let mut resource_id: i16 = 0;

            if data_len_byte > 0 {
                match base_type {
                    // icon, picture, resCtrl: 2-byte resource ID
                    32 | 64 | 7 if data_len_byte >= 2 => {
                        resource_id = bus.read_word(ptr + offset) as i16;
                    }
                    // button (4), statText (8), editText (16): text data
                    4 | 5 | 6 | 8 | 16 => {
                        let bytes = bus.read_bytes(ptr + offset, data_len_byte as usize);
                        text = String::from_utf8_lossy(&bytes).to_string();
                    }
                    _ => {}
                }
            }

            // Advance past data, padded to even boundary
            let padded = (data_len_byte + 1) & !1;
            offset += padded;

            // The DITL stores only a placeholder for userItem procedures.
            // Applications install the live ProcPtr after dialog creation via
            // SetDItem or direct writes to the duplicated DITL.
            // Inside Macintosh Volume I, I-426 to I-427
            let proc_ptr = 0;

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

    /// 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
    }

    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;
        if edit_field < 0 {
            return (String::new(), 0, default_item);
        }

        let edit_item = edit_field + 1;
        let edit_text = Self::text_item_string_from_handle(
            bus,
            Self::dialog_item_handle(bus, dialog_ptr, edit_item),
        );
        if !edit_text.is_empty() {
            return (edit_text, edit_item, default_item);
        }

        let fallback = items
            .get(edit_field 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 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();
                }
            }
        }
    }

    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 {
            bus.alloc(170)
        };

        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,
            go_away_flag,
            ref_con,
        );
        self.set_current_port_state(bus, cpu, dlg_ptr, None);

        bus.write_word(dlg_ptr + 108, proc_id as u16);
        bus.write_long(dlg_ptr + 156, items_handle);

        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 {
            // Save the clean background before drawing any controls, so that
            // ModalDialog can restore it on dismiss.  Without this, ModalDialog
            // captures saved_pixels *after* we've already drawn buttons here,
            // and restoring them leaves button-text artefacts on screen.
            let background = self.save_dialog_pixels(bus, bounds);
            self.dialog_saved_pixels.insert(dlg_ptr, background);

            // Fill the dialog area with white immediately so that any
            // QuickDraw calls the game makes before ModalDialog (e.g.,
            // Marathon draws its custom popup buttons between GetNewDialog
            // and ModalDialog) render against a clean white background
            // rather than whatever game content was behind the dialog.
            // ModalDialog's draw_dialog call will later redraw the border
            // and items on top of this, with the game's custom content
            // preserved by the userItem save/restore mechanism there.
            // Inside Macintosh Volume I, I-413
            // Only fill with white if the dialog has standard controls.
            // Game-managed dialogs (all userItems) handle their own background
            // and filling white would overwrite game content (e.g. HUD panels).
            let all_user_items = items.iter().all(|item| (item.item_type & 0x7F) == 0);
            if !all_user_items {
                let (sb, rb, sw, sh, ps) = self.get_screen_params();
                let (top, left, bottom, right) = bounds;
                Self::fb_fill_rect(bus, sb, rb, ps, sw, sh, top, left, bottom, right, false);
            }

            // NewDialog/GetNewDialog draw controls immediately and mark their
            // rectangles valid so the subsequent update pass redraws only the
            // remaining items.
            // Macintosh Toolbox Essentials 1992, 6-114 to 6-118
            for item in &items {
                let (it, il, ib, ir) = item.rect;
                let abs_top = bounds.0 + it;
                let abs_left = bounds.1 + il;
                let abs_bottom = bounds.0 + ib;
                let abs_right = bounds.1 + ir;
                match item.item_type & 0x7F {
                    4 => self.draw_button(
                        bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, true,
                    ),
                    5 => self.draw_checkbox(
                        bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, false,
                    ),
                    6 => self.draw_radio(
                        bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, false,
                    ),
                    8 => self.draw_static_text(
                        bus, abs_top, abs_left, abs_bottom, abs_right, &item.text,
                    ),
                    16 => self.draw_edit_text_with_cursor(
                        bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, false, false,
                    ),
                    _ => continue,
                }
                self.validate_window_rect(bus, dlg_ptr, item.rect);
            }
            self.queue_window_update_event(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 save_top = top - 5; // dBoxProc outer border
        let save_left = left - 5;
        let save_bottom = bottom + 5; // shadow + dBoxProc border
        let save_right = right + 5;
        // 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
    }

    /// 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 save_top = top - 5;
        let save_left = left - 5;
        let save_bottom = bottom + 5;
        let save_right = right + 5;
        // 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.
        }
    }

    /// 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,
    ) -> Vec<((i16, i16, i16, i16), bool)> {
        items
            .iter()
            .enumerate()
            .filter(|(i, it)| {
                (it.item_type & 0x7F) == 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(
        &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,
        skip_popup_user_items: bool,
    ) {
        let user_item_rects =
            self.user_item_preserve_rects(dialog_ptr, bounds, items, skip_popup_user_items);
        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);
    }

    /// 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(
        &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,
    ) {
        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 (all userItems) since the game draws its own background
        // and the white fill would overwrite game content outside the item rects.
        let all_user_items = items.iter().all(|item| (item.item_type & 0x7F) == 0);
        if !all_user_items {
            Self::fb_fill_rect(
                bus,
                screen_base,
                row_bytes,
                pixel_size,
                screen_width,
                screen_height,
                top,
                left,
                bottom,
                right,
                false,
            );
        } else {
            eprintln!(
                "[DIALOG] Skipping white fill for game-managed dialog at ({},{},{},{}), {} items",
                top,
                left,
                bottom,
                right,
                items.len()
            );
        }

        // 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 => {
                // dBoxProc: double border with 3px white gap between them.
                // Inside Macintosh Volume I, I-299
                Self::fb_fill_rect(
                    bus,
                    screen_base,
                    row_bytes,
                    pixel_size,
                    screen_width,
                    screen_height,
                    top - 5,
                    left - 5,
                    bottom + 5,
                    right + 5,
                    false,
                );
                // Inner border at content edge
                self.draw_rect_border(bus, top, left, bottom, right);
                // Outer border with 3px gap
                self.draw_rect_border(bus, top - 4, left - 4, bottom + 4, right + 4);
                // Basilisk's platinum theme adds a 3-pixel decorative shadow
                // at top-5..top-3 and bottom+3..+5 (lavender/gray/purple
                // gradient, not pure white) — proper fidelity requires
                // emulating the platinum theme window manager.
            }
            3 => {
                // altDBoxProc: single border + shadow
                self.draw_rect_border(bus, top, left, bottom, right);
                self.draw_shadow(bus, top, left, bottom, right);
            }
            _ => {
                // 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()
            );
        }

        // 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;
            }

            match base_type {
                // Button (ctrlItem + btnCtrl = 4)
                4 => {
                    self.draw_button(
                        bus,
                        abs_top,
                        abs_left,
                        abs_bottom,
                        abs_right,
                        &item.text,
                        item_num == default_item,
                    );
                }
                // Checkbox (ctrlItem + chkCtrl = 5)
                5 => {
                    let checked = self
                        .dialog_control_values
                        .get(&(dialog_ptr, item_num))
                        .copied()
                        .unwrap_or(0)
                        != 0;
                    self.draw_checkbox(
                        bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, checked,
                    );
                }
                // Radio button (ctrlItem + radCtrl = 6)
                6 => {
                    self.draw_radio(
                        bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, false,
                    );
                }
                // 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
                    };
                    self.draw_edit_text_with_cursor(
                        bus,
                        abs_top,
                        abs_left,
                        abs_bottom,
                        abs_right,
                        display_text,
                        item_num == edit_item,
                        false,
                    );
                }
                // Icon (32)
                32 => {
                    if !skip_pictures && item.resource_id != 0 {
                        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,
                            );
                        }
                    }
                }
                // 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 =
                            String::from_utf8_lossy(&Self::control_title_bytes(bus, ctrl_ptr))
                                .into_owned();

                        match proc_id_ctrl {
                            0 => self.draw_button(
                                bus,
                                abs_top,
                                abs_left,
                                abs_bottom,
                                abs_right,
                                &title,
                                item_num == default_item,
                            ),
                            1 => self.draw_checkbox(
                                bus,
                                abs_top,
                                abs_left,
                                abs_bottom,
                                abs_right,
                                &title,
                                value != 0,
                            ),
                            2 => self.draw_radio(
                                bus,
                                abs_top,
                                abs_left,
                                abs_bottom,
                                abs_right,
                                &title,
                                value != 0,
                            ),
                            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 selected = value.max(1) as usize;
                                let item_title = self.popup_menu_item_title(bus, min, selected);
                                self.draw_popup_control(
                                    bus,
                                    abs_top,
                                    abs_left,
                                    abs_bottom,
                                    abs_right,
                                    &item_title.unwrap_or_default(),
                                );
                            }
                            _ => {
                                self.draw_button(
                                    bus,
                                    abs_top,
                                    abs_left,
                                    abs_bottom,
                                    abs_right,
                                    &title,
                                    item_num == default_item,
                                );
                            }
                        }
                    } else {
                        self.draw_button(
                            bus,
                            abs_top,
                            abs_left,
                            abs_bottom,
                            abs_right,
                            "",
                            item_num == default_item,
                        );
                    }
                }
                // userItem (0) and unknown: skip
                _ => {}
            }
        }
    }

    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;
        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;
            }

            match item.item_type & 0x7F {
                4 => self.draw_button(
                    bus,
                    abs_top,
                    abs_left,
                    abs_bottom,
                    abs_right,
                    &item.text,
                    item_num == default_item,
                ),
                5 => {
                    let checked = self
                        .dialog_control_values
                        .get(&(dialog_ptr, item_num))
                        .copied()
                        .unwrap_or(0)
                        != 0;
                    self.draw_checkbox(
                        bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, checked,
                    );
                }
                6 => self.draw_radio(
                    bus, abs_top, abs_left, abs_bottom, abs_right, &item.text, false,
                ),
                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 =
                            String::from_utf8_lossy(&Self::control_title_bytes(bus, ctrl_ptr))
                                .into_owned();
                        match proc_id_ctrl {
                            0 => self.draw_button(
                                bus,
                                abs_top,
                                abs_left,
                                abs_bottom,
                                abs_right,
                                &title,
                                item_num == default_item,
                            ),
                            1 => self.draw_checkbox(
                                bus,
                                abs_top,
                                abs_left,
                                abs_bottom,
                                abs_right,
                                &title,
                                value != 0,
                            ),
                            2 => self.draw_radio(
                                bus,
                                abs_top,
                                abs_left,
                                abs_bottom,
                                abs_right,
                                &title,
                                value != 0,
                            ),
                            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 selected = value.max(1) as usize;
                                let item_title = self.popup_menu_item_title(bus, min, selected);
                                self.draw_popup_control(
                                    bus,
                                    abs_top,
                                    abs_left,
                                    abs_bottom,
                                    abs_right,
                                    &item_title.unwrap_or_default(),
                                );
                            }
                            _ => self.draw_button(
                                bus,
                                abs_top,
                                abs_left,
                                abs_bottom,
                                abs_right,
                                &title,
                                item_num == default_item,
                            ),
                        }
                    } else {
                        self.draw_button(
                            bus,
                            abs_top,
                            abs_left,
                            abs_bottom,
                            abs_right,
                            "",
                            item_num == default_item,
                        );
                    }
                }
                8 => {
                    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
                    };
                    self.draw_edit_text_with_cursor(
                        bus,
                        abs_top,
                        abs_left,
                        abs_bottom,
                        abs_right,
                        display_text,
                        item_num == edit_item,
                        false,
                    );
                }
                _ => {}
            }
        }
    }

    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();
        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;
        }
    }

    /// 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,
            );
        }
    }

    /// 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,
    ) {
        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
            self.get_screen_params();

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

        // Standard popup menu button appearance.
        // Macintosh Toolbox Essentials 1992, 5-26
        let oval_w: i16 = 8;
        let oval_h: i16 = 8;
        let r = Rect {
            top,
            left,
            bottom,
            right,
        };

        // Fill interior with white using round-rect spans
        let fill_spans = self.compute_rrect_spans(&r, oval_w, oval_h);
        for y in top..bottom {
            let idx = (y - top) as usize;
            if idx < fill_spans.len() {
                let (sl, sr) = fill_spans[idx];
                Self::fb_hline(
                    bus,
                    screen_base,
                    row_bytes,
                    pixel_size,
                    screen_width,
                    screen_height,
                    y,
                    sl,
                    sr,
                    false,
                );
            }
        }

        // 1-px round-rect border
        self.fb_frame_round_rect(bus, top, left, bottom, right, oval_w, oval_h, 1);

        // 1-px drop shadow (right edge and bottom edge, offset +1)
        // Macintosh Toolbox Essentials 1992, 5-26
        let shadow_r = Rect {
            top: top + 2,
            left: left + 2,
            bottom: bottom + 1,
            right: right + 1,
        };
        let shadow_spans = self.compute_rrect_spans(&shadow_r, oval_w, oval_h);
        let inner_spans = self.compute_rrect_spans(&r, oval_w, oval_h);
        for y in shadow_r.top..shadow_r.bottom {
            let si = (y - shadow_r.top) as usize;
            if si >= shadow_spans.len() {
                continue;
            }
            let (sl, sr) = shadow_spans[si];
            // Only draw shadow pixels outside the main rect fill area
            let ii = (y - top) as usize;
            let (_il, ir) = if y >= top && y < bottom && ii < inner_spans.len() {
                inner_spans[ii]
            } else {
                (sr, sl) // no inner overlap, draw full shadow span
            };
            // Right shadow segment
            if sr > ir {
                for x in ir.max(sl)..sr {
                    Self::fb_set_pixel(
                        bus,
                        screen_base,
                        row_bytes,
                        pixel_size,
                        screen_width,
                        screen_height,
                        x,
                        y,
                        true,
                    );
                }
            }
            // Bottom shadow segment (below main rect)
            if y >= bottom {
                for x in sl..sr {
                    Self::fb_set_pixel(
                        bus,
                        screen_base,
                        row_bytes,
                        pixel_size,
                        screen_width,
                        screen_height,
                        x,
                        y,
                        true,
                    );
                }
            }
        }

        // Downward-pointing triangle on right side (popup indicator)
        // Macintosh Toolbox Essentials 1992, 5-26
        let mid_y = (top + bottom) / 2;
        let tri_x = right - 10;
        for row in 0..4i16 {
            Self::fb_hline(
                bus,
                screen_base,
                row_bytes,
                pixel_size,
                screen_width,
                screen_height,
                mid_y - 2 + row,
                tri_x - 3 + row,
                tri_x + 3 - row,
                true,
            );
        }

        // Selected item text inside the box
        // Macintosh Toolbox Essentials 1992, 5-26
        if !title.is_empty() {
            let metrics = get_font_metrics(font_id, font_size);
            let text_x = left + 6;
            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 redraw_dialog_popup_controls(
        &self,
        bus: &mut MacMemoryBus,
        popup_draws: &[(i16, i16, i16, i16, String)],
    ) {
        for (top, left, bottom, right, title) in popup_draws {
            self.draw_popup_control(bus, *top, *left, *bottom, *right, title);
        }
    }

    /// 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,
    ) {
        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
            self.get_screen_params();
        // White fill
        Self::fb_fill_rect(
            bus,
            screen_base,
            row_bytes,
            pixel_size,
            screen_width,
            screen_height,
            top,
            left,
            bottom,
            right,
            false,
        );
        // Border
        self.draw_rect_border(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,
            );
        }

        // Center text
        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,
        );
    }

    /// 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,
    ) {
        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
            self.get_screen_params();
        let box_size = 12i16;
        let box_top = top + 1;
        let box_left = left;
        // 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 2..box_size - 2 {
                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
        let font_id = 0i16;
        let font_size = 12i16;
        let metrics = get_font_metrics(font_id, font_size);
        let text_x = box_left + box_size + 4;
        let text_y = box_top + (box_size - (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,
        );
    }

    /// 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,
    ) {
        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
            self.get_screen_params();
        let r = 6i16; // radius
        let cx = left + r;
        let cy = top + r + 1;
        // Simple circle approximation: draw a box for now
        Self::fb_fill_rect(
            bus,
            screen_base,
            row_bytes,
            pixel_size,
            screen_width,
            screen_height,
            cy - r,
            cx - r,
            cy + r,
            cx + r,
            false,
        );
        self.draw_rect_border(bus, cy - r, cx - r, cy + r, cx + r);
        if selected {
            Self::fb_fill_rect(
                bus,
                screen_base,
                row_bytes,
                pixel_size,
                screen_width,
                screen_height,
                cy - 3,
                cx - 3,
                cy + 3,
                cx + 3,
                true,
            );
        }
        // Label
        let font_id = 0i16;
        let font_size = 12i16;
        let metrics = get_font_metrics(font_id, font_size);
        let text_x = cx + r + 4;
        let text_y = cy - r + (2 * r - (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,
        );
    }

    /// 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(&String::from_utf8_lossy(&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();
        let font_id = 0i16;
        let font_size = 12i16;
        let metrics = get_font_metrics(font_id, font_size);
        let line_height = metrics.ascent + metrics.descent + metrics.leading.max(2);
        let max_width = right - left;
        let mut y = top + metrics.ascent;
        if y >= bottom && bottom > top {
            y = bottom - 1;
        }

        let substituted = self.apply_param_text(text);
        // Word-wrap text to fit within the display rect
        let words: Vec<&str> = substituted.split(' ').collect();
        let mut current_line = String::new();

        for word in &words {
            let test_line = if current_line.is_empty() {
                word.to_string()
            } else {
                format!("{} {}", current_line, word)
            };
            let test_width = Self::fb_measure_string(&test_line, font_id, font_size);
            if test_width > max_width && !current_line.is_empty() {
                if y <= bottom {
                    Self::fb_draw_string(
                        bus,
                        screen_base,
                        row_bytes,
                        pixel_size,
                        screen_width,
                        screen_height,
                        left,
                        y,
                        &current_line,
                        font_id,
                        font_size,
                    );
                }
                y += line_height;
                current_line = word.to_string();
            } else {
                current_line = test_line;
            }
        }
        // Draw the last line
        if !current_line.is_empty() && y <= bottom {
            Self::fb_draw_string(
                bus,
                screen_base,
                row_bytes,
                pixel_size,
                screen_width,
                screen_height,
                left,
                y,
                &current_line,
                font_id,
                font_size,
            );
        }
    }

    /// 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,
    ) {
        self.draw_edit_text_with_cursor(bus, top, left, bottom, right, text, selected, !selected);
    }

    fn draw_edit_text_with_cursor(
        &self,
        bus: &mut MacMemoryBus,
        top: i16,
        left: i16,
        bottom: i16,
        right: i16,
        text: &str,
        selected: bool,
        show_cursor: bool,
    ) {
        let (screen_base, row_bytes, screen_width, screen_height, pixel_size) =
            self.get_screen_params();
        // White fill
        Self::fb_fill_rect(
            bus,
            screen_base,
            row_bytes,
            pixel_size,
            screen_width,
            screen_height,
            top,
            left,
            bottom,
            right,
            false,
        );
        // Border (inset look)
        self.draw_rect_border(bus, top, left, bottom, right);
        // Text inside with 2px padding
        let font_id = 0i16;
        let font_size = 12i16;
        let metrics = get_font_metrics(font_id, font_size);
        let text_y = top + 2 + metrics.ascent;
        Self::fb_draw_string(
            bus,
            screen_base,
            row_bytes,
            pixel_size,
            screen_width,
            screen_height,
            left + 3,
            text_y,
            text,
            font_id,
            font_size,
        );
        if selected {
            // Select all text (invert the inner area to show selection highlight)
            // On a real Mac, ModalDialog selects all text in the active editText field
            // by default. TextEdit inverts the selection rectangle.
            if !text.is_empty() {
                self.invert_button_rect(bus, top + 1, left + 1, bottom - 1, right - 1);
            }
        } 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 {
                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,
                    );
                }
            }
        }
    }

    /// 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));
                }
            }
        }
    }

    /// 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 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 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 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_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 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 = bus.read_word(ctrl_ptr + 20) as i16;
        let Some(menu_idx) = self.menus.iter().position(|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, 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 item_height: i16 = 16;
        let item_idx = (mouse_y - top - 1) / item_height;
        if item_idx < 0 || (item_idx as usize) >= menu.items.len() {
            return 0;
        }
        let item = &menu.items[item_idx as usize];
        if item.text == "-" || !item.enabled {
            return 0;
        }
        item_idx + 1
    }

    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(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 dropdown_rect = self
                    .dialog_tracking
                    .as_ref()
                    .and_then(|tracking| tracking.active_popup.as_ref())
                    .map(|popup| popup.dropdown_rect);
                if let Some(dropdown_rect) = dropdown_rect {
                    if old_item > 0 {
                        self.invert_dropdown_item_rect(bus, 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, 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);
        }

        let Some(saved) = self.dialog_tracking.take() else {
            return;
        };
        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);
    }

    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 (mut bounds, proc_id, visible, items_id, title, position) =
                        Self::parse_dlog(bus, dlog_data, 256);

                    // Apply positioning constant
                    // Macintosh Toolbox Essentials 1992, pp. 4-125 to 4-126
                    // references/executor/src/dial/dialCreate.cpp, dialog_compute_rect()
                    match position {
                        // alertPositionMainScreen: upper third of screen
                        // Macintosh Toolbox Essentials 1992, p. 4-126
                        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) / 3;
                            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
                    }

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

                    let items = if let Some(ditl_data) = ditl_info {
                        // Estimate DITL data length (generous upper bound)
                        Self::parse_ditl(bus, ditl_data, 4096)
                    } else {
                        eprintln!(
                            "[TRAP] GetNewDialog({}): DITL {} not found",
                            dialog_id, items_id
                        );
                        Vec::new()
                    };

                    eprintln!(
                        "[TRAP] GetNewDialog({}) bounds=({},{},{},{}) procID={} items={} title=\"{}\"",
                        dialog_id, bounds.0, bounds.1, bounds.2, bounds.3,
                        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);
                    // IM:I-410: GetNewDialog returns NIL when the DLOG
                    // resource can't be found.
                    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. In Systemless's HLE
            // there's no user interaction, so each call collapses
            // to "look up the ALRT, parse the stages word, return
            // the bold (default) item for the current stage,
            // increment AlertStage." This matches what a real
            // user pressing Return would do — and is what most
            // games defensively expect (return 1 = OK button =
            // dismiss the alert).
            //
            // 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 0:    boldItmNum flag
            //                                 (0 = item 1 bold/
            //                                 default, 1 = item 2
            //                                 bold/default)
            //                       bit 1:    boxDrawn flag
            //                                 (alert is shown if
            //                                 set; suppressed if
            //                                 clear)
            //                       bits 2-3: 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 user-event-loop side of
            // the alert (cursor tracking, button hit-testing) is
            // also skipped — we just return the documented default
            // item and advance the stage.
            // 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 AlertStage ($0A9A) or -1 if ALRT missing; 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);
                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);
                // 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 {
                    // Read the 16-bit stages word at offset +10
                    // (after 8-byte boundsRect + 2-byte itemsID).
                    let stages = bus.read_word(alrt_data + 10);
                    // 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 = (stage_word as u32).min(3);
                    // Each stage occupies 4 bits, stage 1 in the
                    // low nibble (bits 0..3), stage 4 in the high
                    // nibble (bits 12..15).
                    let nibble = ((stages as u32) >> (stage_idx * 4)) & 0xF;
                    // bit 3 (MSB) of nibble = boldItm per StageList
                    // PACKED RECORD layout (IM:I I-422): boldItm,
                    // boxDrwn, sound[2]. Assembly mask okDismissal=8
                    // (IM:I I-424) confirms bit 3. 0=item 1, 1=item 2.
                    let bold_item: i16 = if (nibble & 0x08) == 0 { 1 } else { 2 };
                    // 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);
                    bold_item
                } 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(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 top = bus.read_word(alrt_data) as i16;
                        let left = bus.read_word(alrt_data + 2) as i16;
                        let bottom = bus.read_word(alrt_data + 4) as i16;
                        let right = bus.read_word(alrt_data + 6) as i16;
                        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 items_id = bus.read_word(alrt_data + 8) as i16;
                            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 items = Self::parse_ditl(bus, ditl_data, 4096);
                                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)
            // IsDialogEvent ($A97F): Reads event record, checks dialog window, matches event types, performs point containment
            (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 result = if let Some(dialog_ptr) = self.dialog_from_window_event(what, message)
                {
                    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
                };
                bus.write_word(sp + 4, if result { 0xFFFF } else { 0 });
                cpu.write_reg(Register::A7, sp + 4);
                Ok(())
            }

            // DialogSelect ($A980)
            // DialogSelect ($A980): Handles keyboard/mouse events in modeless dialogs, hit testing, item updates
            (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;

                if let Some(dialog_ptr) = self.dialog_from_window_event(what, message) {
                    let bounds = Self::dialog_screen_bounds(bus, dialog_ptr);
                    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 => {
                                let proc_id = bus.read_word(dialog_ptr + 108) as i16;
                                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,
                                );
                            }
                            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;
                                    if !is_disabled {
                                        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;
                                    }
                                }
                            }
                            3 | 5 => {
                                let (_edit_text, edit_item, _default_item) =
                                    Self::dialog_edit_state(bus, dialog_ptr, &items);
                                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 base_type = item.item_type & 0x7F;
                                        let is_disabled = (item.item_type & 0x80) != 0;
                                        if base_type == 16 && !is_disabled {
                                            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);
                    }
                }

                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 = bus.read_word(dialog_ptr + 108) as i16;
                    let (edit_text, edit_item, default_item) =
                        Self::dialog_edit_state(bus, dialog_ptr, &items);
                    self.draw_dialog_preserving_user_items(
                        bus,
                        bounds,
                        proc_id,
                        "",
                        &items,
                        default_item,
                        &edit_text,
                        edit_item,
                        false,
                        dialog_ptr,
                        false,
                    );
                    self.dialog_items.insert(dialog_ptr, items);
                }
                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
            // pre-dialog background pixels (captured during NewDialog
            // by save_dialog_pixels) back to the screen. Without this
            // restore, any game that skips ModalDialog and runs its
            // own event loop (e.g. Escape Velocity's shuttle-name
            // input dialog) leaves a dialog-shaped hole over the
            // window behind — the "dialog dismiss leaves dead pixels"
            // bug.
            //
            // 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 dialog_ptr = bus.read_long(sp);
                eprintln!("[TRAP] DisposDialog(${:08X})", dialog_ptr);
                let was_front = self.front_window == dialog_ptr;
                let exposed_rect = self.window_bounds;

                // Blit the saved background pixels back to the screen
                // BEFORE removing any tracking state. For the restore
                // to hit the right screen area, the dialog must still
                // be the front window — otherwise its bounds have
                // already been replaced.
                if was_front {
                    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_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_cancel_items.remove(&dialog_ptr);
                self.dialog_modal_entered.remove(&dialog_ptr);
                self.untrack_window(bus, dialog_ptr);

                // Restore previous front window if this dialog was on top.
                // On a real Mac, CloseWindow (called internally by DisposDialog)
                // removes the window from the window list and the next window
                // becomes the front window.
                // Inside Macintosh Volume I, I-283
                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),
                            );
                            // Closing the front dialog exposes part of the
                            // underlying front window. Mark the exposed rect
                            // invalid so BeginUpdate/EndUpdate paths redraw it.
                            self.invalidate_window_rect(bus, prev_window, exposed_local);
                            // IM:I I-413/I-415: closing a front dialog reveals
                            // the previously obscured window content. Queue an
                            // update event so custom event loops that rely on
                            // updateEvt to repaint the uncovered window get a
                            // redraw opportunity immediately after CloseDialog.
                            self.queue_window_update_event(prev_window);
                        }
                    }
                }

                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);
                    }
                    // Games (e.g. Marathon) use InsertMenu -> GetDItem to
                    // associate a dynamic menu with an enabled userItem popup.
                    // Keep the association one-shot; Damage Incorporated polls
                    // disabled portrait userItems after menu setup.
                    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 {
                            self.dialog_item_popup_menus
                                .insert((dialog_ptr, item_no), menu_id);
                            // Save the original DITL rect before SetDItem narrows it.
                            self.dialog_popup_original_rects
                                .insert((dialog_ptr, item_no), item.rect);
                        }
                    }
                } 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);
                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);
                }
                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 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 {
                            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_popup.is_some() {
                        self.handle_dialog_popup_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_popup.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.
                    if filter_proc != 0 {
                        let result_addr = self.dialog_filter_result_addr;
                        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;
                        }
                        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 {
                            let handled_mouse_down = self
                                .dialog_tracking
                                .as_ref()
                                .and_then(|t| t.last_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(),
                                )
                            };
                            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();
                            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()
                                );
                            }
                            cpu.write_reg(Register::A7, stack_ptr + 8);
                            return Some(Ok(()));
                        }
                        pending_event = self
                            .dialog_tracking
                            .as_mut()
                            .and_then(|t| t.last_filter_event.take());
                    }

                    if flash_remaining > 0 {
                        // Button flash animation
                        let flash_item = self.dialog_tracking.as_ref().unwrap().flash_item;
                        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;

                        if remaining > 0 {
                            // Toggle button highlight
                            let items = &t.items;
                            if flash_item > 0 && (flash_item as usize) <= items.len() {
                                let item = &items[(flash_item - 1) as usize];
                                let (it, il, ib, ir) = item.rect;
                                let abs_top = bounds.0 + it;
                                let abs_left = bounds.1 + il;
                                let abs_bottom = bounds.0 + ib;
                                let abs_right = bounds.1 + ir;
                                self.invert_button_rect(
                                    bus, abs_top, abs_left, abs_bottom, abs_right,
                                );
                            }
                        } else {
                            // 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();
                            self.restore_dialog_pixels(bus, saved.bounds, &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);
                            }
                            cpu.write_reg(Register::A7, stack_ptr + 8);
                        }
                        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(e) = event {
                        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 => {
                                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;
                            }
                            // 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 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 {
                                    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 (it, il, ib, ir) = item.rect;
                                                let abs_top = bounds.0 + it;
                                                let abs_left = bounds.1 + il;
                                                let abs_bottom = bounds.0 + ib;
                                                let abs_right = bounds.1 + ir;
                                                self.invert_button_rect(
                                                    bus, abs_top, abs_left, abs_bottom, abs_right,
                                                );
                                                let t = self.dialog_tracking.as_mut().unwrap();
                                                t.flash_remaining = 6;
                                                t.flash_delay = 3;
                                                t.flash_item = hit;
                                            }
                                            // 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();
                                                self.dialog_saved_pixels
                                                    .insert(dlg_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);
                                                }
                                                cpu.write_reg(Register::A7, stack_ptr + 8);
                                            }
                                            // EditText click: set as active
                                            16 => {
                                                let t = self.dialog_tracking.as_mut().unwrap();
                                                Self::sync_tracking_active_edit_item(t);
                                                t.edit_item = hit;
                                                t.edit_text = t
                                                    .items
                                                    .get((hit - 1) as usize)
                                                    .map(|item| item.text.clone())
                                                    .unwrap_or_default();
                                                t.edit_text_modified = false;
                                                bus.write_word(dialog_ptr + 164, (hit - 1) as u16);
                                                self.refresh_dialog_tracking_snapshot(bus);
                                            }
                                            // 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
                                            0 if self
                                                .dialog_item_popup_menus
                                                .contains_key(&(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.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);
                                            }
                                            // 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.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 tracking = self.dialog_tracking.as_mut().unwrap();

                                match char_code {
                                    // Return or Enter: trigger default button
                                    0x0D | 0x03 => {
                                        let def = tracking.default_item;
                                        if def > 0 && (def as usize) <= tracking.items.len() {
                                            let item = &tracking.items[(def - 1) as usize];
                                            let (it, il, ib, ir) = item.rect;
                                            let abs_top = bounds.0 + it;
                                            let abs_left = bounds.1 + il;
                                            let abs_bottom = bounds.0 + ib;
                                            let abs_right = bounds.1 + ir;
                                            self.invert_button_rect(
                                                bus, abs_top, abs_left, abs_bottom, abs_right,
                                            );
                                            let t = self.dialog_tracking.as_mut().unwrap();
                                            t.flash_remaining = 6;
                                            t.flash_delay = 3;
                                            t.flash_item = def;
                                        }
                                    }
                                    // Escape: trigger cancel button
                                    0x1B => {
                                        let cancel = tracking.cancel_item;
                                        if cancel > 0 && (cancel as usize) <= tracking.items.len() {
                                            let item = &tracking.items[(cancel - 1) as usize];
                                            let (it, il, ib, ir) = item.rect;
                                            let abs_top = bounds.0 + it;
                                            let abs_left = bounds.1 + il;
                                            let abs_bottom = bounds.0 + ib;
                                            let abs_right = bounds.1 + ir;
                                            self.invert_button_rect(
                                                bus, abs_top, abs_left, abs_bottom, abs_right,
                                            );
                                            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 => {
                                        if tracking.edit_item > 0 {
                                            Self::sync_tracking_active_edit_item(tracking);
                                        }
                                        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);
                                            self.refresh_dialog_tracking_snapshot(bus);
                                        }
                                    }
                                    // Backspace/Delete
                                    0x08 => {
                                        if tracking.edit_item > 0 {
                                            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();
                                            }
                                            Self::sync_tracking_active_edit_item(tracking);
                                        }
                                    }
                                    // Printable ASCII
                                    0x20..=0x7E => {
                                        if tracking.edit_item > 0 {
                                            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);
                                            Self::sync_tracking_active_edit_item(tracking);
                                        }
                                    }
                                    _ => {}
                                }
                            }
                            _ => {}
                        }
                    }
                } 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 most recently created dialog's items
                    let dialog_ptr = self.front_window;
                    if let Some(mut items) = self.dialog_items.get(&dialog_ptr).cloned() {
                        // 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);

                        // If all items are userItem type, the game manages drawing
                        // itself — skip our draw to avoid overwriting game content.
                        let all_user_items = items.iter().all(|it| (it.item_type & 0x7F) == 0);

                        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 saved_pixels = self
                            .dialog_saved_pixels
                            .remove(&dialog_ptr)
                            .unwrap_or_else(|| self.save_dialog_pixels(bus, bounds));
                        if !all_user_items && !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. Games (e.g. Marathon)
                            // often draw custom controls (popup buttons, sliders) into
                            // userItem rects via QuickDraw before calling ModalDialog.
                            // draw_dialog's white fill would erase that content; we
                            // capture it here and restore it afterwards so the snapshot
                            // (rendered_pixels) reflects the combined state.
                            // Inside Macintosh Volume I, I-405
                            self.draw_dialog_preserving_user_items(
                                bus,
                                bounds,
                                proc_id,
                                &title,
                                &items,
                                default_item,
                                &edit_text,
                                edit_item,
                                false,
                                dialog_ptr,
                                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 popup_draws: Vec<(i16, i16, i16, i16, String)> = items
                            .iter()
                            .enumerate()
                            .filter_map(|(i, item)| {
                                let item_no = (i + 1) as i16;
                                if (item.item_type & 0x7F) != 0 {
                                    return None;
                                }
                                let menu_id =
                                    *self.dialog_item_popup_menus.get(&(dialog_ptr, item_no))?;
                                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) = self
                                    .dialog_popup_original_rects
                                    .get(&(dialog_ptr, item_no))
                                    .copied()
                                    .unwrap_or(item.rect);
                                Some((
                                    bounds.0 + it_t,
                                    bounds.1 + it_l,
                                    bounds.0 + it_b,
                                    bounds.1 + it_r,
                                    checked_text,
                                ))
                            })
                            .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.
                        // Even game-managed dialogs can rely on Dialog Manager
                        // userItem callbacks for portions of their content.
                        // Inside Macintosh Volume I, I-405
                        let mut draw_proc_queue = VecDeque::new();
                        for (i, item) in items.iter().enumerate() {
                            let base_type = item.item_type & 0x7F;
                            if base_type == 0 && item.proc_ptr != 0 {
                                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: false,
                            draw_proc_queue,
                            draw_procs_done: !has_draw_procs,
                            rendered_pixels_final: !has_draw_procs,
                            filter_proc,
                            game_managed: all_user_items,
                            last_filter_event: None,
                            popup_draws,
                            active_popup: None,
                        });
                        // 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());
                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((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((_, ptr)) = self.find_resource_any(*b"CURS", cursor_id) {
                    self.get_or_create_resource_handle(bus, *b"CURS", cursor_id, ptr)
                } 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((_, ptr)) = self.find_resource_any(*b"PAT ", pat_id) {
                    self.get_or_create_resource_handle(bus, *b"PAT ", pat_id, ptr)
                } 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((_, ptr)) = self.find_resource_any(*b"ICON", icon_id) {
                    let h = self.get_or_create_resource_handle(bus, *b"ICON", icon_id, ptr);
                    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((_, ptr)) = self.find_resource_any(*b"PICT", pic_id) {
                    let h = self.get_or_create_resource_handle(bus, *b"PICT", pic_id, ptr);
                    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((_, ptr)) = self.find_resource_any(*b"STR ", string_id) {
                    self.get_or_create_resource_handle(bus, *b"STR ", string_id, ptr)
                } 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,
                                                resolved_size.max(1),
                                            );
                                            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, resolved_size.max(1));
                                    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 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);
                            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);
                        let new_styles = bus.read_long(sp + 8);
                        let style_handle = Self::te_style_handle(bus, te_handle);
                        if style_handle != 0 && new_styles != 0 {
                            let style_ptr = bus.read_long(style_handle);
                            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
                            };
                            let scrap_ptr = bus.read_long(new_styles);
                            if style_table_ptr != 0 && scrap_ptr != 0 {
                                let base = scrap_ptr + Self::SCRAP_STYLE_TAB_OFFSET;
                                bus.write_word(
                                    style_table_ptr + Self::ST_ELEMENT_HEIGHT_OFFSET,
                                    bus.read_word(base + Self::SCRAP_STYLE_HEIGHT_OFFSET),
                                );
                                bus.write_word(
                                    style_table_ptr + Self::ST_ELEMENT_ASCENT_OFFSET,
                                    bus.read_word(base + Self::SCRAP_STYLE_ASCENT_OFFSET),
                                );
                                bus.write_word(
                                    style_table_ptr + Self::ST_ELEMENT_FONT_OFFSET,
                                    bus.read_word(base + Self::SCRAP_STYLE_FONT_OFFSET),
                                );
                                bus.write_word(
                                    style_table_ptr + Self::ST_ELEMENT_FACE_OFFSET,
                                    bus.read_word(base + Self::SCRAP_STYLE_FACE_OFFSET),
                                );
                                bus.write_word(
                                    style_table_ptr + Self::ST_ELEMENT_SIZE_OFFSET,
                                    bus.read_word(base + Self::SCRAP_STYLE_SIZE_OFFSET),
                                );
                                bus.write_word(
                                    style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET,
                                    bus.read_word(base + Self::SCRAP_STYLE_COLOR_OFFSET),
                                );
                                bus.write_word(
                                    style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 2,
                                    bus.read_word(base + Self::SCRAP_STYLE_COLOR_OFFSET + 2),
                                );
                                bus.write_word(
                                    style_table_ptr + Self::ST_ELEMENT_COLOR_OFFSET + 4,
                                    bus.read_word(base + Self::SCRAP_STYLE_COLOR_OFFSET + 4),
                                );
                            }
                        }
                        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 = match align {
                                1 => box_left + (box_width - lw) / 2, // teJustCenter
                                -1 => box_right - lw,                 // teJustRight
                                _ => box_left, // teJustLeft / teForceLeft / 0
                            };
                            self.pn_loc = (y, x);
                            for &byte in &text_bytes[*start..trimmed_end] {
                                self.draw_char(cpu, bus, byte as char);
                            }
                            y += line_height;
                        }
                    }
                }
                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 ($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);
                }
                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
            // 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);
                }
                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 compromise: Systemless doesn't model TextEdit caret
            // blinking. Per IM:I I-374 a real TEIdle would: (a) read
            // TickCount ($016A) and compare against the TE record's
            // caretTime field, (b) when the interval elapses, toggle
            // the caret's visibility flag, (c) XOR-paint the caret
            // bar at the current selStart position. The first two
            // steps need TE record fields (caretTime, caretState)
            // that the current TERec layout doesn't track; the third
            // needs the per-line pixel-position-of-byte-offset
            // machinery that TEClick also lacks. So apps that call
            // TEIdle on every null event get a no-op — the caret in
            // any TE field stays in whatever state TEActivate /
            // TEDeactivate left it in (typically a static caret at
            // selStart). For the scripted event model
            // where there are no null events between scripted
            // actions, the missing blink is invisible.
            //
            // 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. The HLE achieves
            // this trivially by popping the argument and returning
            // without inspecting the TERec.
            //
            // 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_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);
                if trace_textedit_enabled() {
                    eprintln!("[TE] TEIdle hTE=${:08X}", bus.read_long(sp));
                }
                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);
            // Sets the active edit field + its selection range in the
            // dialog so the next ModalDialog / UpdtDialog redraw
            // displays the caret in the right place.
            // Inside Macintosh Volume I, I-414
            //
            // 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).
            //
            // HLE compromise: real-Mac SelectDialogItemText also
            // calls TESetSelect on the item's TEHandle. Systemless's
            // DialogItem doesn't carry a TEHandle (the dialog
            // edit-text path is implemented entirely via
            // DialogItem.text + sel_start/sel_end fields, not via
            // a per-item TERec). The clamped (sel_start, sel_end)
            // pair is stored back into the DialogItem so a future
            // ModalDialog redraw can highlight the right bytes.
            // Apps that programmatically GetDialogItemText after
            // the call see the same text; SetDialogItemText
            // ($A98F, Partial) replaces it.
            // SelectDialogItemText ($A97E): Per IM:I I-414 selects text [strtSel..endSel) in editText item itemNo: clamps both indices to text.len(), normalizes (0,-1) → (0,text.len()) per "select all" special case, swaps if strtSel > endSel, stores back into DialogItem.sel_start/sel_end, marks the item as DialogRecord.editField at offset 164. No-op for non-editText items per IM. Pops 10 bytes (theDialog 4 + itemNo 2 + strtSel 2 + endSel 2).
            (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);
                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);
                                    }
                                }
                            }
                        }
                    }
                }
                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 {
                    String::from_utf8_lossy(&bus.read_pstring(title_ptr)).into_owned()
                } 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);
                let was_front = self.front_window == dialog_ptr;
                let exposed_rect = self.window_bounds;

                // IM:I I-413: deletes the dialog window from the window list.
                self.untrack_window(bus, dialog_ptr);

                // Restore previous front window (same as DisposDialog)
                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),
                            );
                            // Mirror CloseWindow/PaintBehind exposure semantics:
                            // closing a front dialog invalidates the newly
                            // uncovered area in the promoted window.
                            self.invalidate_window_rect(bus, prev_window, exposed_local);
                            // IM:I I-283/I-425: DisposDialog internally
                            // closes the front dialog and exposes the window
                            // behind it. Queue updateEvt for the promoted
                            // front window so app redraw loops can repaint
                            // newly revealed regions.
                            self.queue_window_update_event(prev_window);
                        }
                    }
                }

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

            // UpdtDialog ($A978)
            // Redraws all items in the update region of the dialog.
            // PROCEDURE UpdtDialog(theDialog: DialogPtr; updateRgn: RgnHandle);
            // Inside Macintosh Volume I, I-415
            //
            // Delegates to the full dialog redraw path (same as DrawDialog) —
            // BeginUpdate/EndUpdate on the calling side already clips drawing
            // to the actual update region, so redrawing every item is correct
            // if somewhat wasteful.
            // UpdtDialog ($A978): Redraws every item in the dialog (BeginUpdate/EndUpdate already clip drawing to the actual updateRgn on the caller side); does not honor updateRgn for sub-region selection. Pops 8 bytes per IM:I I-415
            (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(()));
                }
                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 = bus.read_word(dialog_ptr + 108) as i16;
                    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,
                    );
                    self.dialog_items.insert(dialog_ptr, items);
                }
                Ok(())
            }

            // CouldDialog ($A979) / FreeDialog ($A97A)
            // PROCEDURE CouldDialog(dialogID: INTEGER);
            // PROCEDURE FreeDialog (dialogID: INTEGER);
            // Inside Macintosh Volume I, I-415
            //
            // Both traps validate the primary 'DLOG' resource and
            // propagate ResError ($0A60). Per IM:I I-415 they also
            // cascade to the dialog window's WDEF, the DITL, and
            // any items defined as resources — but Systemless's HLE
            // has no resource-purgeability axis (loaded resources
            // live in the bus allocator until shutdown and no
            // heap compaction runs), so the cascade collapses to
            // the primary-template lookup. The ResErr write is
            // the only HLE-visible side effect.
            // CouldDialog ($A979): Validates DLOG resource is loaded; writes ResErr noErr on both hit and miss in BasiliskII. WDEF/DITL/items cascade omitted (HLE has no purgeable-bit axis).
            // FreeDialog ($A97A): Validates DLOG resource is loaded; writes ResErr noErr on both hit and miss in BasiliskII. WDEF/DITL/items cascade omitted (HLE has no purgeable-bit axis).
            (true, 0x179) | (true, 0x17A) => {
                let sp = cpu.read_reg(Register::A7);
                let dialog_id = bus.read_word(sp) as i16;
                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)
            // Moves the item's 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, offsets itemRect.left/right by +16384
            // so the item becomes offscreen; already-hidden items are unchanged (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;
                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 {
                                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);
                                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;
                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),
                                    )
                                };
                                items[idx].rect = restored_rect;
                                updated_control_rect = Some(restored_rect);
                                self.invalidate_window_rect(bus, dialog_ptr, restored_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
            //
            // Companion of CouldDialog/FreeDialog ($A979/$A97A —
            // see arm above). HLE collapses the trap to writing
            // ResErr ($0A60) = noErr and otherwise leaves the
            // loaded ALRT handle untouched. BasiliskII treats
            // missing alert IDs as harmless no-ops; the
            // WDEF/DITL/items cascade documented at IM:I I-420 is
            // still omitted.
            (true, 0x189) => {
                let sp = cpu.read_reg(Register::A7);
                let _alert_id = bus.read_word(sp) as i16;
                bus.write_word(0x0A60, 0);
                cpu.write_reg(Register::A7, sp + 2);
                Ok(())
            }

            // FreeAlert ($A98A)
            // PROCEDURE FreeAlert(alertID: INTEGER);
            // Inside Macintosh Volume I, I-420
            //
            // See CouldAlert ($A989) above.
            (true, 0x18A) => {
                let sp = cpu.read_reg(Register::A7);
                let _alert_id = bus.read_word(sp) as i16;
                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
            // SetDialogItemText ($A98F): Updates edit text in active dialog tracking state
            (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);
                if text_str_ptr != 0 {
                    let bytes = bus.read_pstring(text_str_ptr);
                    let len = bytes.len();
                    let text = String::from_utf8_lossy(&bytes).to_string();
                    if let Some(ref mut tracking) = self.dialog_tracking {
                        // Dialog tracking active — update live edit text
                        if tracking.edit_item > 0 {
                            tracking.edit_text = text.clone();
                        }
                    }

                    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()
                    {
                        if trace_dialog_items_enabled() {
                            eprintln!(
                                "[DIALOG-ITEM] SetDialogItemText pc=${:08X} dialog=${:08X} item={} handle=${:08X} text={:?}",
                                pc,
                                dlg_ptr,
                                idx + 1,
                                item_handle,
                                text
                            );
                        }
                        if let Some(items) = self.dialog_items.get_mut(&dlg_ptr) {
                            if idx < items.len() {
                                items[idx].text = text;
                            }
                        }
                    } else if trace_dialog_items_enabled() {
                        eprintln!(
                            "[DIALOG-ITEM] SetDialogItemText pc=${:08X} dialog=<unknown> handle=${:08X} text={:?}",
                            pc,
                            item_handle,
                            text
                        );
                    }
                }

                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
            // GetDialogItemText ($A990): Returns text from tracking state or reads from item handle
            (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 {
                            String::from_utf8_lossy(&bus.read_pstring(title_ptr)).into_owned()
                        } 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, TEST_SP};
    use crate::cpu::{CpuOps, Register};
    use crate::memory::{MacMemoryBus, MemoryBus};
    use crate::trap::dispatch::DialogItem;
    use crate::trap::menu::{Menu, MenuItem};
    use crate::trap::TrapDispatcher;
    use std::collections::VecDeque;

    // ---- 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);
    }

    // ---- 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,
        });

        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,
        });

        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_returns_nil_when_dlog_missing() {
        // Inside Macintosh Volume I, I-410: GetNewDialog returns NIL when
        // the DLOG resource can't be located.
        let (mut disp, mut cpu, mut bus) = setup();

        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"
        );
    }

    // save_dialog_pixels / restore_dialog_pixels must guard against off-screen y
    // (negative after top-5, 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_five_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 = -3 (negative).
        let saved = d.save_dialog_pixels(&bus, (2, 2, 50, 50));
        // Row width = (55 - (-3)) = 58; row count = 58.
        let row_width = 58usize;
        let row_count = 58usize;
        assert_eq!(saved.len(), row_width * row_count);

        // First 3 rows are off-screen (y = -3, -2, -1) → zero-padded.
        for row in 0..3 {
            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=-3 so cols 0..3 are off-screen → 0,
        // cols 3..58 read from the 0x42-filled framebuffer.
        let row0 = &saved[3 * row_width..4 * row_width];
        for (col, &px) in row0.iter().enumerate().take(3) {
            assert_eq!(
                px, 0x00,
                "off-screen column {} within on-screen row must be zero",
                col
            );
        }
        for (col, &px) in row0.iter().enumerate().skip(3) {
            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 + 5 - (600 - 5)) as usize;
        let row_width = (50 + 5 - (0 - 5)) as usize;
        assert_eq!(saved.len(), row_count * row_width);
    }

    #[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_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!(
            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.
        assert_eq!(
            TrapDispatcher::te_line_origin_x(0, 20, 200, 80),
            20,
            "teJustLeft (0) must anchor at 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 (overrides any
        // localised right-to-left default).
        assert_eq!(
            TrapDispatcher::te_line_origin_x(-2, 20, 200, 80),
            20,
            "teForceLeft (-2) must anchor at box_left"
        );
    }

    #[test]
    fn te_line_origin_te_just_system_defaults_left() {
        // teJustSystem (0): localised default = left for LTR.
        assert_eq!(
            TrapDispatcher::te_line_origin_x(0, 20, 200, 80),
            20,
            "teJustSystem must default to flush 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). Each nibble's bit 0 = boldItmNum
    // (0 → item 1 default, 1 → item 2 default).

    /// 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);
    }

    // ---- 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 alert_returns_item_1_when_stage_1_bold_flag_clear() {
        // stages = 0x0000 → all 4 nibbles have bit 0 clear →
        // bold item = 1 (OK button) at every stage.
        let (mut disp, mut cpu, mut bus) = setup();
        let alrt = build_alrt_template(/*itemsID*/ 200, /*stages*/ 0x0000);
        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 = 0x0008 → stage 1 nibble bit 3 set (okDismissal
        // mask per IM:I I-424) → bold item = 2 (Cancel button).
        let (mut disp, mut cpu, mut bus) = setup();
        let alrt = build_alrt_template(200, 0x0008);
        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 = 0x8080 → 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 = bit 3. 5th call stays at stage 4.
        let (mut disp, mut cpu, mut bus) = setup();
        let alrt = build_alrt_template(200, 0x8080);
        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, 0x0000);
        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_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, 0x0000);
        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, 0x0000);
        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;
        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);
    }

    #[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;
        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;
        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);
    }

    #[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);
    }

    // ---- 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_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 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 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);
    }

    #[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_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"
        );
    }

    // ---- 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_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,
        });

        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());
    }

    // 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 a 5-pixel
    // margin around the bounds (for dBoxProc drop-shadow). The tests
    // use bounds (100,100,150,200) → save area (95,95)..(155,205) =
    // 60 rows × 110 cols = 6600 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 95u32..155 {
            for x in 95u32..205 {
                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). 60*110=6600 bytes.
        disp.dialog_saved_pixels
            .insert(dialog_ptr, vec![0x33; 60 * 110]);

        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 95u32..155 {
            for x in 95u32..205 {
                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 5-pixel-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; 60 * 110]);

        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 + 94 * row_bytes + x), 0xAA);
        }
        // Below the save rect:
        for x in 85u32..215 {
            assert_eq!(bus.read_byte(screen_base + 155 * row_bytes + x), 0xAA);
        }
        // Left of the save rect:
        for y in 85u32..165 {
            assert_eq!(bus.read_byte(screen_base + y * row_bytes + 94), 0xAA);
        }
        // Right of the save rect:
        for y in 85u32..165 {
            assert_eq!(bus.read_byte(screen_base + y * row_bytes + 205), 0xAA);
        }
        // Bytes INSIDE the save area must now be 0x33.
        for y in 95u32..155 {
            for x in 95u32..205 {
                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; 60 * 110]);

        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_associates_recent_inserted_menu_with_enabled_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: 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_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_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_eq!(disp.last_inserted_menu_id, None);
    }

    // ---- 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 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_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_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 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_ignores_user_item_placeholder_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, 0);
    }

    #[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_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 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,
        });

        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: (100, 100, 280, 320),
            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,
        });
        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);

        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));
    }

    #[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,
                },
            ],
        );

        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!(!tracking.draw_procs_done);
        assert!(!tracking.rendered_pixels_final);
    }

    #[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![(20, 30, 42, 180, String::new())],
            active_popup: 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_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,
        });
        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_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,
        });
        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));
    }

    // ---- 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_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);
        }
    }

    #[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_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"
        );
    }

    // 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
    }

    // ---- 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(),
                    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());
    }
}
