// tests/dired_acceptance.rs --- dired arc Stage 1 acceptance.

//! Acceptance for the dired view (`docs/dired-framing.md` §14 items
//! 1-16, Q#DR2-DR10). Item 17 --- "the fixture still passes" --- is a
//! gate item rather than a test here: `m8_1`/`m8_2`/`m8_3` prove the
//! `read_dir` opt is additive by continuing to pass unchanged.
//!
//! Discipline, following the Stage 0 suite:
//!
//! * every in-buffer claim is driven by a **real key** through
//!   `dispatch_key`, so a dead mode-keymap entry cannot pass vacuously;
//! * `pmacs.dired.open` is called directly only where a test needs an
//!   opt the interactive command does not carry (`display = "panel"`),
//!   and it is the documented public entry point in those cases;
//! * every listing is async, so each dispatch is followed by `pump`,
//!   which drives `tick_async` until the coroutine and its worker job
//!   have both settled.
//!
//! Fixtures use `.txt` files and empty `pmacs.lsp.config`, so no
//! `buffer.after-load` hook spawns a language server. Note the suite
//! asserts nothing about LSP, so the wipe cannot make an assertion
//! vacuous (the Lean 4 round-1 trap).

use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant, SystemTime};

use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
use pmacs::cell::{CellGrid, CellSize, Glyph};
use pmacs::editor::EditorState;
use pmacs::editor_core::normalize_buffer_path;
use pmacs::protocol::FrontendId;
use pmacs::window::WindowId;
use tempfile::TempDir;

const ROWS: u32 = 24;
const COLS: u32 = 100;

// ---------------------------------------------------------------------------
// Harness
// ---------------------------------------------------------------------------

fn key(code: KeyCode, mods: KeyModifiers) -> KeyEvent {
    KeyEvent {
        code,
        modifiers: mods,
        kind: KeyEventKind::Press,
        state: KeyEventState::NONE,
    }
}

fn ctrl(s: &mut EditorState, c: char) {
    s.dispatch_key(
        FrontendId::LOCAL,
        key(KeyCode::Char(c), KeyModifiers::CONTROL),
    );
}

fn press(s: &mut EditorState, code: KeyCode) {
    s.dispatch_key(FrontendId::LOCAL, key(code, KeyModifiers::NONE));
}

fn type_char(s: &mut EditorState, c: char) {
    s.dispatch_key(FrontendId::LOCAL, key(KeyCode::Char(c), KeyModifiers::NONE));
}

fn type_str(s: &mut EditorState, text: &str) {
    for ch in text.chars() {
        type_char(s, ch);
    }
}

fn exec(s: &EditorState, src: &str) {
    s.lua_host.lua().load(src.to_string()).exec().unwrap();
}

fn eval<T: mlua::FromLuaMulti>(s: &EditorState, src: &str) -> T {
    s.lua_host.lua().load(src.to_string()).eval().unwrap()
}

/// A fresh editor with a declared frame geometry (a grid frontend's real
/// frame size *is* its geometry declaration, and the panel tests need
/// one before any side window can be placed).
fn editor() -> EditorState {
    let s = EditorState::new();
    exec(&s, "pmacs.lsp.config = {}");
    s.sync_frame_geometry(FrontendId::LOCAL, CellSize::new(ROWS, COLS));
    s
}

/// An editor whose active buffer is a real file inside `dir`, so the
/// `C-x d` prompt prefills with that directory and `C-x C-j` has a file
/// to jump from.
fn editor_in(dir: &Path) -> (EditorState, PathBuf) {
    let anchor = dir.join("anchor.txt");
    std::fs::write(&anchor, b"anchor\n").expect("write anchor");
    let s = editor();
    let anchor_str = anchor.display().to_string();
    exec(&s, &format!("pmacs.buffer.find_or_open({anchor_str:?})"));
    (s, anchor)
}

/// Drive the async runtime until no coroutine is parked and no worker
/// job is pending. Every dired command dispatches `read_dir` on a
/// worker and resumes on a later tick, so nothing dired does is
/// observable until this returns.
fn pump(s: &mut EditorState) {
    let deadline = Instant::now() + Duration::from_secs(10);
    let mut spins = 0u32;
    loop {
        let idle: bool = eval(
            s,
            "return pmacs._async.parked_count() == 0 and pmacs._async.pending_count() == 0",
        );
        if idle {
            return;
        }
        assert!(Instant::now() < deadline, "async pump deadline exceeded");
        s.tick_async();
        spins += 1;
        if spins > 64 {
            std::thread::sleep(Duration::from_millis(1));
        }
    }
}

/// The canonical form of `path` — the core's own normalizer, which is
/// exactly what `pmacs.path.canonicalize` calls.
fn canon(path: &Path) -> String {
    normalize_buffer_path(path.to_path_buf())
        .to_string_lossy()
        .into_owned()
}

fn active_text(s: &EditorState) -> String {
    eval(
        s,
        "local b = pmacs.window.buffer()\nreturn b:slice(0, b:len())",
    )
}

fn active_lines(s: &EditorState) -> Vec<String> {
    active_text(s).lines().map(str::to_owned).collect()
}

fn active_name(s: &EditorState) -> String {
    eval(
        s,
        "return pmacs.describe.buffer(pmacs.window.buffer()).name",
    )
}

fn active_path(s: &EditorState) -> Option<String> {
    eval(
        s,
        "local b = pmacs.window.buffer()\n\
         if b == nil then return nil end\n\
         local ok, p = pcall(function() return b:path() end)\n\
         if ok then return p end\n\
         return nil",
    )
}

fn status(s: &EditorState) -> String {
    s.core.borrow().status.clone()
}

fn buffer_names(s: &EditorState) -> Vec<String> {
    eval(
        s,
        "local out = {}\n\
         for _, id in ipairs(pmacs.buffer.list()) do\n\
           out[#out + 1] = pmacs.describe.buffer(id).name\n\
         end\n\
         return out",
    )
}

fn dired_buffer_names(s: &EditorState) -> Vec<String> {
    let mut names: Vec<String> = buffer_names(s)
        .into_iter()
        .filter(|n| n.starts_with("*dired:"))
        .collect();
    names.sort();
    names
}

/// One layout offset from dired's own constants, so column assertions
/// cannot drift from the module that computes them.
fn layout(s: &EditorState, field: &str) -> usize {
    let value: i64 = eval(s, &format!("return pmacs.dired._layout.{field}"));
    usize::try_from(value).expect("layout offsets are non-negative")
}

/// The rendered name column of one listing line.
fn line_name(s: &EditorState, line: &str) -> String {
    let start = layout(s, "NAME_START");
    line.get(start..).unwrap_or("").to_owned()
}

/// The 0-based line the entry named `name` renders on.
fn line_of(s: &EditorState, name: &str) -> usize {
    let lines = active_lines(s);
    for (index, line) in lines.iter().enumerate().skip(1) {
        let rendered = line_name(s, line);
        if rendered == name || rendered.starts_with(&format!("{name} -> ")) {
            return index;
        }
    }
    panic!("no listing line for {name:?} in {lines:#?}");
}

/// Seat the cursor on `name`'s line. Test scaffolding: the *keys* that
/// move by line are exercised separately (acceptance 6).
fn seat_on(s: &EditorState, name: &str) {
    let line = line_of(s, name);
    exec(s, &format!("pmacs.editor.move_to_line({line})"));
}

fn cursor_line(s: &EditorState) -> usize {
    let value: i64 = eval(s, "return pmacs.editor.cursor_line()");
    usize::try_from(value).expect("cursor lines are non-negative")
}

/// The entry name under the cursor, or `None` on the header/footer.
fn cursor_entry(s: &EditorState) -> Option<String> {
    let line = cursor_line(s);
    if line == 0 {
        return None;
    }
    let lines = active_lines(s);
    lines.get(line).map(|text| line_name(s, text))
}

/// Open `path` through the public entry point, pumping to settle.
/// Returns the raised message, if it raised.
fn open_dired(s: &mut EditorState, path: &str, opts: &str) -> Option<String> {
    exec(
        s,
        &format!(
            "_G.DIRED_ERR = nil\n\
             pmacs.async(function()\n\
               local ok, err = pcall(pmacs.dired.open, {path:?}, {opts})\n\
               if not ok then\n\
                 _G.DIRED_ERR = type(err) == 'table' and tostring(err.message) or tostring(err)\n\
               end\n\
             end)"
        ),
    );
    pump(s);
    eval(s, "return _G.DIRED_ERR")
}

fn open_ok(s: &mut EditorState, path: &Path, opts: &str) {
    let raised = open_dired(s, &path.display().to_string(), opts);
    assert!(
        raised.is_none(),
        "dired.open must succeed; raised {raised:?}"
    );
}

fn side_window(s: &EditorState) -> Option<WindowId> {
    s.core.borrow().side_window_for(FrontendId::LOCAL)
}

fn window_buffer_name(s: &EditorState, window: WindowId) -> String {
    let buffer_id = s
        .core
        .borrow()
        .windows
        .get(&window)
        .map(|w| w.buffer_id)
        .expect("window is live");
    let registry = s.lua_host.registry().borrow();
    registry
        .get(buffer_id)
        .expect("buffer is live")
        .name()
        .to_owned()
}

fn active_window(s: &EditorState) -> WindowId {
    s.core.borrow().active_window_id()
}

/// Paint one real frame and return its rows as text.
fn painted_rows(s: &EditorState) -> Vec<String> {
    let size = CellSize::new(ROWS, COLS);
    let mut cells = vec![pmacs::cell::Cell::default(); (ROWS * COLS) as usize];
    let mut grid = CellGrid {
        cells: &mut cells,
        stride: COLS,
        size,
    };
    pmacs::editor::paint_frame(s, FrontendId::LOCAL, &HashMap::new(), &mut grid, size);
    (0..ROWS)
        .map(|row| {
            (0..COLS)
                .map(|col| match &cells[(row * COLS + col) as usize].glyph {
                    Glyph::Char(ch) => *ch,
                    Glyph::Cluster(_) => '?',
                    Glyph::Continuation => ' ',
                })
                .collect::<String>()
                .trim_end()
                .to_owned()
        })
        .collect()
}

/// `a.txt` (5 bytes), `b.txt` (6 bytes), `subdir/`, and `link ->
/// a.txt`.
fn fixture_dir() -> TempDir {
    let td = tempfile::tempdir().expect("tempdir");
    std::fs::write(td.path().join("a.txt"), b"hello").expect("write a");
    std::fs::write(td.path().join("b.txt"), b"world!").expect("write b");
    std::fs::create_dir(td.path().join("subdir")).expect("mkdir");
    std::fs::write(td.path().join("subdir").join("inner.txt"), b"deep\n").expect("write inner");
    std::os::unix::fs::symlink("a.txt", td.path().join("link")).expect("symlink");
    td
}

// ---------------------------------------------------------------------------
// 1 --- listing shape
// ---------------------------------------------------------------------------

/// Header line plus one line per entry, with kind char, perms, size,
/// mtime, and name; a symlink renders `l` with ` -> target`; the entry
/// count matches `read_dir`. Driven through the real `C-x d`, accepting
/// the prefilled directory.
#[test]
fn dired_renders_a_header_and_one_line_per_entry() {
    let td = fixture_dir();
    let (mut s, _anchor) = editor_in(td.path());

    ctrl(&mut s, 'x');
    type_char(&mut s, 'd');
    assert!(
        eval::<bool>(&s, "return pmacs.minibuffer.is_active()"),
        "C-x d must open a prompt"
    );
    assert_eq!(
        eval::<String>(&s, "return pmacs.minibuffer.contents()"),
        canon(td.path()),
        "the prompt prefills with the current buffer's directory, so RET \
         opens where you are"
    );
    press(&mut s, KeyCode::Enter);
    pump(&mut s);

    let lines = active_lines(&s);
    assert_eq!(
        lines[0],
        format!("{}:", canon(td.path())),
        "line 0 is the header"
    );
    let on_disk = std::fs::read_dir(td.path()).expect("read_dir").count();
    assert_eq!(
        lines.len() - 1,
        on_disk,
        "one line per entry, no footer on a clean listing: {lines:#?}"
    );

    let kind_start = layout(&s, "KIND_START");
    let perms_start = layout(&s, "PERMS_START");
    let perms_end = layout(&s, "PERMS_END");
    let size_start = layout(&s, "SIZE_START");

    let a = &lines[line_of(&s, "a.txt")];
    assert_eq!(&a[kind_start..=kind_start], "-", "a regular file: {a:?}");
    let perms = &a[perms_start..perms_end];
    assert_eq!(perms.len(), 9, "nine permission characters: {perms:?}");
    assert!(
        perms.starts_with("rw"),
        "owner may read and write a file we just wrote: {perms:?}"
    );
    assert_eq!(
        a[size_start..size_start + 10].trim(),
        "5",
        "the size column carries a.txt's five bytes: {a:?}"
    );
    assert!(
        a[perms_end..size_start].chars().all(char::is_whitespace),
        "columns are space-separated: {a:?}"
    );

    let sub = &lines[line_of(&s, "subdir")];
    assert_eq!(&sub[kind_start..=kind_start], "d", "a directory: {sub:?}");

    let link = &lines[line_of(&s, "link")];
    assert_eq!(&link[kind_start..=kind_start], "l", "a symlink: {link:?}");
    assert_eq!(
        line_name(&s, link),
        "link -> a.txt",
        "a symlink shows its target"
    );

    // The mark column is reserved and blank in Stage 1 (Q#DR4): filling
    // it in is Stage 2's job, and reserving it now is what keeps Stage
    // 2 from moving every column right of it.
    let mark_start = layout(&s, "MARK_START");
    for line in &lines[1..] {
        assert_eq!(
            &line[mark_start..kind_start],
            "  ",
            "the mark column renders blank: {line:?}"
        );
    }

    let mtime_start = layout(&s, "MTIME_START");
    let name_start = layout(&s, "NAME_START");
    let stamp = &a[mtime_start..name_start - 1];
    assert_eq!(stamp.len(), 16, "fixed-width mtime: {stamp:?}");
    assert!(
        stamp.starts_with("20") && stamp.contains('-') && stamp.contains(':'),
        "an ISO-ish minute-precision timestamp: {stamp:?}"
    );
}

/// The columns are a CONTRACT, not a formatting preference: `_layout` is
/// exported and Stage 3's column-classifying intercept is planned
/// against it. A size that does not fit ten digits (10 GB and up — VM
/// images, core dumps) must therefore yield precision rather than width,
/// the way `fmt_mtime` already does. Without that, one line's mtime and
/// name shift right and nothing notices until Stage 3.
#[test]
fn dired_keeps_its_columns_when_a_size_exceeds_the_field() {
    let td = tempfile::tempdir().expect("tempdir");
    std::fs::write(td.path().join("small.txt"), b"x").expect("write small");
    let huge = td.path().join("huge.img");
    // Sparse: `set_len` allocates nothing on any filesystem pmacs
    // supports. If one refuses, the premise cannot be established.
    let file = std::fs::File::create(&huge).expect("create huge");
    if file.set_len(12_000_000_000).is_err() {
        eprintln!("filesystem refused a sparse 12 GB file; skipping");
        return;
    }
    drop(file);
    let reported = std::fs::metadata(&huge).expect("stat huge").len();
    assert!(
        reported > 9_999_999_999,
        "fixture premise: the size must exceed ten digits, got {reported}"
    );

    let mut s = editor();
    open_ok(&mut s, td.path(), "nil");
    let size_start = layout(&s, "SIZE_START");
    let mtime_start = layout(&s, "MTIME_START");
    let name_start = layout(&s, "NAME_START");

    let lines = active_lines(&s);
    for name in ["huge.img", "small.txt"] {
        let line = &lines[line_of(&s, name)];
        let size = &line[size_start..mtime_start - 1];
        assert_eq!(
            size.len(),
            10,
            "the size field must stay ten columns wide: {line:?}"
        );
        let stamp = &line[mtime_start..name_start - 1];
        assert!(
            stamp.starts_with("20") && stamp.contains(':'),
            "so the mtime still starts where the layout says: {line:?}"
        );
        assert_eq!(
            line_name(&s, line),
            name,
            "and the name still starts at NAME_START"
        );
    }

    // The oversized value degrades to a magnitude rather than a
    // placeholder, so the listing still says how big the file is.
    let huge_line = &lines[line_of(&s, "huge.img")];
    let size = huge_line[size_start..mtime_start - 1].trim();
    assert!(
        size.ends_with('G') || size.ends_with('T'),
        "an oversized size keeps its magnitude: {size:?}"
    );
    // A size that DOES fit stays exact.
    let small_line = &lines[line_of(&s, "small.txt")];
    assert_eq!(
        small_line[size_start..mtime_start - 1].trim(),
        "1",
        "a size that fits is still the exact byte count"
    );
}

// ---------------------------------------------------------------------------
// 2 --- visit dispatches on kind, through the panel-safe primitive
// ---------------------------------------------------------------------------

/// `RET` on a directory descends; on a file it opens the file; on the
/// header it does nothing.
#[test]
fn dired_visit_dispatches_on_entry_kind() {
    let td = fixture_dir();
    let mut s = editor();
    open_ok(&mut s, td.path(), "nil");

    // Header: no entry, so nothing happens.
    exec(&s, "pmacs.editor.move_to_line(0)");
    let before = active_name(&s);
    press(&mut s, KeyCode::Enter);
    pump(&mut s);
    assert_eq!(
        active_name(&s),
        before,
        "RET on the header must not visit anything"
    );

    // Directory: descend into its own dired buffer.
    seat_on(&s, "subdir");
    press(&mut s, KeyCode::Enter);
    pump(&mut s);
    assert_eq!(
        active_name(&s),
        format!("*dired:{}*", canon(&td.path().join("subdir"))),
        "RET on a directory opens that directory's dired buffer"
    );
    assert_eq!(
        line_name(&s, &active_lines(&s)[1]),
        "inner.txt",
        "the descended listing is the subdirectory's"
    );

    // File: the fixture's "requires the buffer-from-file API" error is
    // gone --- `f` is the same command as RET.
    seat_on(&s, "inner.txt");
    type_char(&mut s, 'f');
    pump(&mut s);
    assert_eq!(
        active_path(&s).map(PathBuf::from),
        Some(PathBuf::from(canon(
            &td.path().join("subdir").join("inner.txt")
        ))),
        "RET/f on a file opens the file bound to its path"
    );
    assert_eq!(
        eval::<String>(&s, "return pmacs.window.buffer():slice(0, 4)"),
        "deep",
        "the file's real contents load"
    );
}

/// A symlink's kind is `"symlink"` in both `read_dir` and `stat` (both
/// are lstat-based), so nothing in the entry says what it points at.
/// `RET` therefore tries the descent and falls back to a file visit —
/// one read, since `open_directory` reads before touching any editor
/// state and its failure *is* the "not a directory" answer.
#[test]
fn dired_visit_follows_a_symlink_to_the_kind_of_its_target() {
    let td = fixture_dir();
    std::os::unix::fs::symlink("subdir", td.path().join("linkdir")).expect("symlink to dir");

    let mut s = editor();
    open_ok(&mut s, td.path(), "nil");

    // A symlink to a directory descends. The path is NOT resolved
    // (canonicalization is lexical), so the buffer names the way the user
    // navigated — Emacs parity.
    seat_on(&s, "linkdir");
    press(&mut s, KeyCode::Enter);
    pump(&mut s);
    assert_eq!(
        active_name(&s),
        format!("*dired:{}*", canon(&td.path().join("linkdir"))),
        "a symlinked directory descends under the path we walked"
    );
    assert_eq!(
        line_name(&s, &active_lines(&s)[1]),
        "inner.txt",
        "and shows the target directory's contents"
    );

    // A symlink to a file opens the file.
    type_char(&mut s, '^');
    pump(&mut s);
    seat_on(&s, "link");
    press(&mut s, KeyCode::Enter);
    pump(&mut s);
    let path = active_path(&s).expect("a file must be open");
    assert!(
        path.ends_with("/link"),
        "the visit keeps the link's own path; got {path}"
    );
    assert_eq!(
        eval::<String>(&s, "return pmacs.window.buffer():slice(0, 5)"),
        "hello",
        "with the target's contents"
    );
}

/// The panel case, which is the real assertion (Q#DR10): with dired
/// displayed as a panel, `RET` on a file leaves the dired panel alive
/// and puts the file in the document window. Falsified by swapping
/// `display_file` for `find_or_open`, which switches the active window
/// in both branches before firing hooks --- the panel swallows itself.
#[test]
fn dired_visit_from_a_panel_keeps_the_panel_and_uses_the_document_window() {
    let td = fixture_dir();
    let (mut s, anchor) = editor_in(td.path());
    let document = active_window(&s);
    open_ok(&mut s, td.path(), r#"{ display = "panel" }"#);

    let panel = side_window(&s).expect("display = panel must create a side window");
    assert_eq!(active_window(&s), panel, "the panel is selected");
    assert_eq!(
        window_buffer_name(&s, panel),
        format!("*dired:{}*", canon(td.path())),
        "the panel shows dired"
    );

    seat_on(&s, "a.txt");
    press(&mut s, KeyCode::Enter);
    pump(&mut s);

    let panel_after = side_window(&s).expect("the dired panel must survive a file visit");
    assert_eq!(panel_after, panel, "the same side window, not a new one");
    assert_eq!(
        window_buffer_name(&s, panel_after),
        format!("*dired:{}*", canon(td.path())),
        "the panel still shows dired"
    );
    assert_eq!(
        window_buffer_name(&s, document),
        canon(&td.path().join("a.txt")),
        "the visited file lands in the document window"
    );
    assert_eq!(
        active_path(&s).map(PathBuf::from),
        Some(PathBuf::from(canon(&td.path().join("a.txt")))),
        "and it is what the visit selected"
    );
    assert!(
        anchor.exists(),
        "fixture sanity: the anchor file was never touched"
    );
}

// ---------------------------------------------------------------------------
// 3 --- one buffer per directory, canonicalized
// ---------------------------------------------------------------------------

/// Descending twice then ascending twice yields the *same* buffers as
/// the first visit, and every dired buffer's name describes the
/// directory it displays.
#[test]
fn dired_navigation_reuses_one_buffer_per_directory() {
    let td = tempfile::tempdir().expect("tempdir");
    let deep = td.path().join("one").join("two");
    std::fs::create_dir_all(&deep).expect("mkdir -p");
    std::fs::write(deep.join("leaf.txt"), b"leaf\n").expect("write leaf");

    let mut s = editor();
    open_ok(&mut s, td.path(), "nil");
    exec(&s, "_G.ROOT = pmacs.window.buffer()");

    seat_on(&s, "one");
    press(&mut s, KeyCode::Enter);
    pump(&mut s);
    exec(&s, "_G.ONE = pmacs.window.buffer()");
    seat_on(&s, "two");
    press(&mut s, KeyCode::Enter);
    pump(&mut s);
    exec(&s, "_G.TWO = pmacs.window.buffer()");
    assert_eq!(
        active_name(&s),
        format!("*dired:{}*", canon(&deep)),
        "each buffer's name describes the directory it displays"
    );

    // Back up, with `^`.
    type_char(&mut s, '^');
    pump(&mut s);
    assert!(
        eval::<bool>(&s, "return pmacs.window.buffer() == _G.ONE"),
        "ascending returns to the SAME buffer, not a fresh one"
    );
    assert_eq!(
        cursor_entry(&s).as_deref(),
        Some("two"),
        "`^` seats the cursor on the directory it came from"
    );
    type_char(&mut s, '^');
    pump(&mut s);
    assert!(
        eval::<bool>(&s, "return pmacs.window.buffer() == _G.ROOT"),
        "and again at the next level up"
    );

    // Down again: still the same two buffers.
    seat_on(&s, "one");
    press(&mut s, KeyCode::Enter);
    pump(&mut s);
    assert!(eval::<bool>(&s, "return pmacs.window.buffer() == _G.ONE"));
    seat_on(&s, "two");
    press(&mut s, KeyCode::Enter);
    pump(&mut s);
    assert!(eval::<bool>(&s, "return pmacs.window.buffer() == _G.TWO"));
    assert_eq!(
        dired_buffer_names(&s).len(),
        3,
        "three directories visited, three dired buffers: {:?}",
        dired_buffer_names(&s)
    );
}

/// Three spellings of one directory yield ONE buffer, because names and
/// lookups both go through the canonical form (Q#DR2).
#[test]
fn dired_canonicalizes_before_naming_and_lookup() {
    let td = fixture_dir();
    let base = td.path().display().to_string();
    let name = td
        .path()
        .file_name()
        .expect("tempdir has a basename")
        .to_string_lossy()
        .into_owned();

    let mut s = editor();
    open_ok(&mut s, td.path(), "nil");
    let raised = open_dired(&mut s, &format!("{base}/"), "nil");
    assert!(raised.is_none(), "trailing slash must open: {raised:?}");
    let raised = open_dired(&mut s, &format!("{base}/../{name}"), "nil");
    assert!(raised.is_none(), "a `..` round trip must open: {raised:?}");

    assert_eq!(
        dired_buffer_names(&s),
        vec![format!("*dired:{}*", canon(td.path()))],
        "three spellings, one buffer"
    );
}

/// `dired.kill-when-opening` (Emacs 28's opt-out): the departed buffer
/// is gone after a descent.
#[test]
fn dired_kill_when_opening_kills_the_departed_buffer() {
    let td = fixture_dir();
    let mut s = editor();
    exec(&s, "pmacs.config.set('dired.kill-when-opening', true)");
    open_ok(&mut s, td.path(), "nil");
    assert_eq!(dired_buffer_names(&s).len(), 1);

    seat_on(&s, "subdir");
    press(&mut s, KeyCode::Enter);
    pump(&mut s);

    assert_eq!(
        dired_buffer_names(&s),
        vec![format!("*dired:{}*", canon(&td.path().join("subdir")))],
        "descending killed the buffer it left"
    );
    // And the setting is what did it: the default keeps both.
    exec(&s, "pmacs.config.set('dired.kill-when-opening', false)");
    type_char(&mut s, '^');
    pump(&mut s);
    assert_eq!(
        dired_buffer_names(&s).len(),
        2,
        "with the setting off, the departed buffer survives: {:?}",
        dired_buffer_names(&s)
    );
}

// ---------------------------------------------------------------------------
// 3b --- canonicalization parity
// ---------------------------------------------------------------------------

/// The Lua canonicalizer and the core normalizer agree on every edge in
/// one shared list --- because they are the *same function*
/// (`pmacs.path.canonicalize` is `normalize_buffer_path`). Stage 1
/// deliberately did not mirror the normalizer in Lua: a second
/// implementation that disagreed on `//tmp` or a `..` at root would
/// mint two buffers for one directory with no error anywhere, and the
/// mirror would then owe Stage 2 a removal.
#[test]
fn dired_canonicalization_is_the_cores_own_normalizer() {
    let s = editor();
    let cases = [
        "//tmp",
        "/tmp/",
        "/tmp/../tmp",
        "/tmp/./x/../y",
        "/../..",
        "/",
        ".",
        "relative/path",
        "~",
        "~/inside",
        "~notauser/x",
    ];
    for case in cases {
        let from_lua: String = eval(&s, &format!("return pmacs.path.canonicalize({case:?})"));
        let from_rust = normalize_buffer_path(PathBuf::from(case))
            .to_string_lossy()
            .into_owned();
        assert_eq!(
            from_lua, from_rust,
            "canonicalization must not fork for {case:?}"
        );
    }

    // And the form dired names buffers with is that same form.
    let td = fixture_dir();
    let mut s = s;
    open_ok(&mut s, td.path(), "nil");
    assert_eq!(active_name(&s), format!("*dired:{}*", canon(td.path())));
}

// ---------------------------------------------------------------------------
// 3c --- panel descent
// ---------------------------------------------------------------------------

/// A directory descent in a panel-displayed dired stays in the *same*
/// side window (Q#DR10): the next directory is the same kind of thing as
/// the current one and belongs in the same slot. Neither replaced by a
/// document window nor duplicated.
///
/// **This test does not pin the routing itself, and says so rather than
/// implying otherwise:** dired holds the focus in its own panel here, so
/// a raw `switch_buffer` lands in that same window and the assertions
/// below hold either way (verified — the mutation is VACUOUS against
/// this test). What distinguishes `display { side = … }` from the raw
/// switch is dedication, so the discriminating pin is
/// `dired_descent_from_a_dedicated_panel_leaves_the_pin_alone` below.
#[test]
fn dired_directory_descent_stays_in_its_side_window() {
    let td = fixture_dir();
    let (mut s, anchor) = editor_in(td.path());
    let document = active_window(&s);
    open_ok(&mut s, td.path(), r#"{ display = "panel" }"#);
    let panel = side_window(&s).expect("a side window");

    seat_on(&s, "subdir");
    press(&mut s, KeyCode::Enter);
    pump(&mut s);

    assert_eq!(
        side_window(&s),
        Some(panel),
        "the same side window, not a second one"
    );
    assert_eq!(
        window_buffer_name(&s, panel),
        format!("*dired:{}*", canon(&td.path().join("subdir"))),
        "showing the new directory"
    );
    assert_eq!(
        window_buffer_name(&s, document),
        canon(&anchor),
        "the document window is untouched"
    );
    assert_eq!(active_window(&s), panel, "and dired keeps the focus");
}

/// A **dedicated** panel is a different story, and the framing's R2-3
/// expectation ("the new dired buffer inherits the dedication") is
/// falsified by the substrate: `display_buffer` never replaces the
/// buffer in a slot dedicated to another one --- it discards every
/// side-specific parameter and falls back to the document window
/// (Q#BP3 2.iii). Dired does not try to unpin the user's panel, so the
/// pin holds and the new directory appears in the document area, which
/// is also what Emacs's `display-buffer` does with a dedicated window.
#[test]
fn dired_descent_from_a_dedicated_panel_leaves_the_pin_alone() {
    let td = fixture_dir();
    let (mut s, _anchor) = editor_in(td.path());
    let document = active_window(&s);
    open_ok(&mut s, td.path(), r#"{ display = "panel" }"#);
    let panel = side_window(&s).expect("a side window");
    exec(
        &s,
        &format!(
            "pmacs.window.set_params({}, {{ dedicated = true }})",
            panel.raw()
        ),
    );

    seat_on(&s, "subdir");
    press(&mut s, KeyCode::Enter);
    pump(&mut s);

    assert_eq!(
        side_window(&s),
        Some(panel),
        "no second side window is created"
    );
    assert_eq!(
        window_buffer_name(&s, panel),
        format!("*dired:{}*", canon(td.path())),
        "the dedicated slot keeps the buffer it was pinned to"
    );
    assert!(
        eval::<bool>(
            &s,
            &format!("return pmacs.window.params({}).dedicated", panel.raw())
        ),
        "and it is still dedicated afterward"
    );
    assert_eq!(
        window_buffer_name(&s, document),
        format!("*dired:{}*", canon(&td.path().join("subdir"))),
        "the new directory falls back to the document window"
    );
}

// ---------------------------------------------------------------------------
// 4 --- ownership check
// ---------------------------------------------------------------------------

/// A foreign buffer that merely *has* dired's name is not adopted (F7):
/// `pmacs.buffer.create` takes any caller-chosen name, and dired paints
/// with `bypass_intercept`, so adopting one would silently clobber a
/// user's data.
#[test]
fn dired_does_not_adopt_a_foreign_buffer_with_its_name() {
    let td = fixture_dir();
    let mut s = editor();
    let name = format!("*dired:{}*", canon(td.path()));
    exec(
        &s,
        &format!(
            "local b = pmacs.buffer.create({name:?})\n\
             b:insert(0, 'FOREIGN CONTENTS')\n\
             _G.FOREIGN = b"
        ),
    );
    // Even with the major mode set, which is the weaker ownership test
    // the framing floated: the handle table is the authority.
    exec(&s, "pmacs.buffer.set_major_mode(_G.FOREIGN, 'dired')");

    open_ok(&mut s, td.path(), "nil");

    assert_eq!(
        eval::<String>(&s, "return _G.FOREIGN:slice(0, _G.FOREIGN:len())"),
        "FOREIGN CONTENTS",
        "the foreign buffer's contents must be byte-identical"
    );
    assert!(
        !eval::<bool>(&s, "return pmacs.window.buffer() == _G.FOREIGN"),
        "dired must not display the foreign buffer"
    );
    assert_eq!(
        active_name(&s),
        format!("{name}<2>"),
        "dired opens under a disambiguated name instead"
    );
    assert!(
        active_lines(&s)[0].ends_with(':'),
        "and it is a real listing: {:?}",
        active_lines(&s)[0]
    );
}

// ---------------------------------------------------------------------------
// 5 --- read-only discipline
// ---------------------------------------------------------------------------

/// An ordinary self-insert is rejected by the intercept and leaves the
/// text byte-identical, while dired's own repaint succeeds through
/// `bypass_intercept`. `set_round_trip_input` is pinned through the
/// **production** seam a semantic frontend reads (`dispatch_idle_for`,
/// published as `DispatchIdle`) rather than by a direct-call assertion:
/// without it, a GPU session would optimistically apply `g` as an
/// insert instead of letting it reach the revert binding.
#[test]
fn dired_buffer_is_read_only_and_round_trips_input() {
    let td = fixture_dir();
    let mut s = editor();
    open_ok(&mut s, td.path(), "nil");
    let before = active_text(&s);

    // A document window, deliberately: the panel arm of the same gate
    // (`!window.is_side()`) would otherwise be what makes this pass.
    assert!(
        !s.core
            .borrow()
            .windows
            .get(&active_window(&s))
            .expect("live window")
            .is_side(),
        "fixture premise: dired is in a document window here"
    );
    assert!(
        !s.dispatch_idle_for(FrontendId::LOCAL),
        "a round-trip buffer must turn optimistic apply OFF"
    );

    // `z` is bound nowhere in dired mode, so it reaches self-insert.
    type_char(&mut s, 'z');
    assert_eq!(
        active_text(&s),
        before,
        "the read-only intercept must reject a self-insert"
    );
    assert!(
        status(&s).contains("read-only"),
        "and say so; got {:?}",
        status(&s)
    );

    // Dired's own writes still land: revert repaints the whole buffer.
    std::fs::write(td.path().join("c.txt"), b"new\n").expect("write c");
    type_char(&mut s, 'g');
    pump(&mut s);
    assert!(
        active_text(&s).contains("c.txt"),
        "dired's own repaint bypasses the intercept: {:?}",
        active_text(&s)
    );
}

// ---------------------------------------------------------------------------
// 6 --- mode keymap
// ---------------------------------------------------------------------------

/// The keys resolve through `scope = "mode"` with no per-buffer
/// binding: a *second* dired buffer, created by a descent that calls no
/// `keymap.bind` of its own, still responds to `n`, `g`, and `^`. The
/// mode also shows in the statusline, through a real painted frame.
#[test]
fn dired_keys_resolve_through_the_mode_keymap() {
    let td = fixture_dir();
    let mut s = editor();
    open_ok(&mut s, td.path(), "nil");
    seat_on(&s, "subdir");
    press(&mut s, KeyCode::Enter);
    pump(&mut s);

    assert_eq!(
        eval::<Option<String>>(&s, "return pmacs.buffer.major_mode(pmacs.window.buffer())"),
        Some("dired".to_owned()),
        "the descended buffer carries the mode"
    );
    assert_eq!(
        eval::<i64>(
            &s,
            "local n = 0\n\
             for _, entry in ipairs(pmacs.keymap.list()) do\n\
               if entry.scope:find('buffer') then n = n + 1 end\n\
             end\n\
             return n"
        ),
        0,
        "and no buffer-scoped binding exists anywhere"
    );

    // `n` moves by line through the mode binding.
    exec(&s, "pmacs.editor.move_to_line(0)");
    type_char(&mut s, 'n');
    assert_eq!(cursor_line(&s), 1, "`n` moves down one line");

    // `g` reverts: a file added externally appears.
    std::fs::write(td.path().join("subdir").join("second.txt"), b"x\n").expect("write second");
    type_char(&mut s, 'g');
    pump(&mut s);
    assert!(
        active_text(&s).contains("second.txt"),
        "`g` re-read the directory: {:?}",
        active_text(&s)
    );

    // `^` ascends.
    type_char(&mut s, '^');
    pump(&mut s);
    assert_eq!(
        active_name(&s),
        format!("*dired:{}*", canon(td.path())),
        "`^` ascends from the second buffer too"
    );

    let rows = painted_rows(&s);
    let mode_line = rows
        .iter()
        .rev()
        .find(|row| row.contains("dired"))
        .unwrap_or_else(|| panic!("no painted row mentions the mode: {rows:#?}"));
    assert!(
        mode_line.contains("dired"),
        "the major mode shows in the statusline: {mode_line:?}"
    );
}

// ---------------------------------------------------------------------------
// 7 --- cursor preservation
// ---------------------------------------------------------------------------

/// The cursor is re-seated by BASENAME across a repaint (Q#DR9), and
/// falls back to the nearest surviving line when the entry is gone.
/// Every repaint is wholesale, so a dired that dropped to line 0 after
/// each revert would be unusable.
#[test]
fn dired_revert_reseats_the_cursor_by_basename() {
    let td = tempfile::tempdir().expect("tempdir");
    for name in ["c.txt", "d.txt", "e.txt"] {
        std::fs::write(td.path().join(name), b"x").expect("write");
    }
    let mut s = editor();
    open_ok(&mut s, td.path(), "nil");
    seat_on(&s, "d.txt");
    let line_before = cursor_line(&s);

    // Two files that sort BEFORE it, so its line index has to change.
    std::fs::write(td.path().join("a.txt"), b"x").expect("write a");
    std::fs::write(td.path().join("b.txt"), b"x").expect("write b");
    type_char(&mut s, 'g');
    pump(&mut s);

    assert_ne!(
        cursor_line(&s),
        line_before,
        "fixture premise: the line index moved"
    );
    assert_eq!(
        cursor_entry(&s).as_deref(),
        Some("d.txt"),
        "the cursor follows the basename, not the line"
    );

    // Now the entry disappears: land on the nearest surviving line.
    let vanished_line = cursor_line(&s);
    std::fs::remove_file(td.path().join("d.txt")).expect("rm d");
    type_char(&mut s, 'g');
    pump(&mut s);
    assert!(
        cursor_line(&s) > 0,
        "a vanished entry must not drop the cursor to the header"
    );
    assert_eq!(
        cursor_line(&s),
        vanished_line.min(active_lines(&s).len() - 1),
        "it lands on the nearest surviving line"
    );
}

/// A revert settles a tick or more later, and the user may have left in
/// the meantime. `pmacs.editor.move_to_line` is **ambient** — it moves
/// whatever window is active — so an unguarded re-seat moves an
/// unrelated buffer's cursor to a line index that only means something
/// in the dired listing. The paint is safe either way because it names
/// its buffer; this pins the half that does not.
#[test]
fn dired_revert_does_not_seat_a_buffer_the_user_switched_to() {
    let td = tempfile::tempdir().expect("tempdir");
    for name in ["a.txt", "b.txt", "c.txt", "d.txt", "e.txt"] {
        std::fs::write(td.path().join(name), b"x").expect("write");
    }
    let notes = td.path().join("notes.txt");
    std::fs::write(&notes, b"one\ntwo\nthree\nfour\nfive\nsix\n").expect("write notes");

    let mut s = editor();
    open_ok(&mut s, td.path(), "nil");
    exec(&s, "_G.DIRED_BUF = pmacs.window.buffer()");
    // A late line, so a stale seat would be visible in the other buffer.
    seat_on(&s, "e.txt");
    let dired_line = cursor_line(&s);
    assert!(
        dired_line >= 4,
        "fixture premise: a late line, got {dired_line}"
    );

    // Start the revert, then leave BEFORE the read settles.
    type_char(&mut s, 'g');
    exec(
        &s,
        &format!(
            "pmacs.buffer.find_or_open({:?})",
            notes.display().to_string()
        ),
    );
    assert_eq!(cursor_line(&s), 0, "a freshly opened file starts at line 0");
    pump(&mut s);

    assert_eq!(
        active_path(&s).map(PathBuf::from),
        Some(PathBuf::from(canon(&notes))),
        "the switch stands: the revert must not pull the user back"
    );
    assert_eq!(
        cursor_line(&s),
        0,
        "and it must not move the cursor of the buffer they moved to"
    );

    // The revert itself still happened: the dired buffer is repainted,
    // and returning to it seats normally on the next command.
    std::fs::write(td.path().join("f.txt"), b"x").expect("write f");
    exec(&s, "pmacs.window.switch_buffer(_G.DIRED_BUF)");
    type_char(&mut s, 'g');
    pump(&mut s);
    assert!(
        active_text(&s).contains("f.txt"),
        "the dired buffer still reverts when it is the active one: {:?}",
        active_text(&s)
    );
}

// ---------------------------------------------------------------------------
// 8 --- sort modes
// ---------------------------------------------------------------------------

/// `s` cycles name -> mtime -> size -> name; mtime sorts newest first
/// and size largest first, each with a stable name tiebreak; the cursor
/// stays on its basename across the reorder.
#[test]
fn dired_sort_cycles_name_then_mtime_then_size() {
    let td = tempfile::tempdir().expect("tempdir");
    // Explicit sizes and mtimes, so neither order depends on the
    // filesystem's timestamp resolution or on write ordering.
    let plan = [
        ("a.txt", 3usize, 1_000u64),
        ("b.txt", 1, 3_000),
        ("c.txt", 2, 2_000),
    ];
    for (name, size, mtime) in plan {
        let path = td.path().join(name);
        std::fs::write(&path, vec![b'x'; size]).expect("write");
        let file = std::fs::File::options()
            .write(true)
            .open(&path)
            .expect("open for set_modified");
        file.set_modified(SystemTime::UNIX_EPOCH + Duration::from_secs(mtime))
            .expect("set mtime");
    }

    let mut s = editor();
    open_ok(&mut s, td.path(), "nil");
    let names = |s: &EditorState| -> Vec<String> {
        active_lines(s)
            .iter()
            .skip(1)
            .map(|line| line_name(s, line))
            .collect()
    };
    assert_eq!(
        names(&s),
        vec!["a.txt", "b.txt", "c.txt"],
        "the initial order is by name"
    );

    seat_on(&s, "c.txt");
    type_char(&mut s, 's');
    assert_eq!(
        names(&s),
        vec!["b.txt", "c.txt", "a.txt"],
        "mtime sorts newest first"
    );
    assert!(
        status(&s).contains("mtime"),
        "and reports the new mode: {:?}",
        status(&s)
    );
    assert_eq!(
        cursor_entry(&s).as_deref(),
        Some("c.txt"),
        "the cursor stays on its basename across the reorder"
    );

    type_char(&mut s, 's');
    assert_eq!(
        names(&s),
        vec!["a.txt", "c.txt", "b.txt"],
        "size sorts largest first"
    );
    type_char(&mut s, 's');
    assert_eq!(
        names(&s),
        vec!["a.txt", "b.txt", "c.txt"],
        "and cycles back"
    );
}

// ---------------------------------------------------------------------------
// 9 --- tolerant listing
// ---------------------------------------------------------------------------

/// A child whose `lstat` fails no longer fails the whole listing
/// (Q#DR6): the readable entries render, the footer counts what could
/// not be read, and the default (non-opt) call still returns a bare
/// array — both forms are exercised here, so the frozen fixture's
/// contract cannot regress unnoticed.
#[test]
fn dired_tolerant_listing_renders_what_it_can_and_counts_the_rest() {
    use std::os::unix::fs::PermissionsExt;
    let td = tempfile::tempdir().expect("tempdir");
    let dir = td.path().join("no-search");
    std::fs::create_dir(&dir).expect("mkdir");
    std::fs::write(dir.join("readable.txt"), b"x").expect("write readable");
    std::fs::write(dir.join("blocked.txt"), b"x").expect("write blocked");
    // Readable but not searchable: `readdir` yields the names, every
    // child `lstat` fails.
    std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o400)).expect("chmod 400");
    if std::fs::symlink_metadata(dir.join("readable.txt")).is_ok() {
        std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700)).expect("restore");
        eprintln!("lstat still succeeds without search permission (root?); skipping");
        return;
    }

    let mut s = editor();
    let raised = open_dired(&mut s, &dir.display().to_string(), "nil");
    std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700)).expect("restore");
    assert!(
        raised.is_none(),
        "a per-entry failure must not fail the listing: {raised:?}"
    );

    let lines = active_lines(&s);
    assert_eq!(
        lines.last().map(String::as_str),
        Some("2 entries unreadable"),
        "the footer names how much of the view is missing: {lines:#?}"
    );

    // Both call shapes, in one test: the bare array is what the frozen
    // M8.2 fixture consumes with `ipairs`.
    let shapes: Vec<i64> = eval(
        &s,
        &format!(
            "local out = {{}}\n\
             pmacs.async(function()\n\
               local bare = pmacs.fs.read_dir({:?}):await()\n\
               local tolerant = pmacs.fs.read_dir({:?}, {{ tolerant = true }}):await()\n\
               _G.SHAPES = {{\n\
                 #bare,\n\
                 bare.entries == nil and 1 or 0,\n\
                 #tolerant.entries,\n\
                 tolerant.errors ~= nil and 1 or 0,\n\
                 #tolerant.errors,\n\
               }}\n\
             end)\n\
             return out",
            td.path().display().to_string(),
            td.path().display().to_string()
        ),
    );
    assert!(shapes.is_empty(), "the async body has not run yet");
    pump(&mut s);
    let shapes: Vec<i64> = eval(&s, "return _G.SHAPES");
    assert_eq!(
        shapes,
        vec![1, 1, 1, 1, 0],
        "bare: one entry and no `entries` field; tolerant: one entry plus \
         an empty error channel"
    );

    // A failure on the parent itself is still fatal.
    let missing = td.path().join("does-not-exist");
    let raised = open_dired(&mut s, &missing.display().to_string(), "nil");
    assert!(
        raised.is_some(),
        "an unopenable directory has no partial answer"
    );
}

// ---------------------------------------------------------------------------
// 10 --- tolerant symlink targets
// ---------------------------------------------------------------------------

/// A symlink whose target is not UTF-8 lists successfully with the
/// entry present and its target reported unknown (F5). Falsified by
/// reverting the `read_link`/target arm in `read_dir_blocking`, which
/// takes the whole listing down.
#[cfg(not(target_os = "macos"))]
#[test]
fn dired_lists_a_symlink_whose_target_is_not_utf8() {
    use std::os::unix::ffi::OsStrExt;
    let td = tempfile::tempdir().expect("tempdir");
    std::fs::write(td.path().join("real.txt"), b"x").expect("write real");
    std::os::unix::fs::symlink(
        std::ffi::OsStr::from_bytes(b"target-\xff"),
        td.path().join("weird"),
    )
    .expect("symlink");

    let mut s = editor();
    let raised = open_dired(&mut s, &td.path().display().to_string(), "nil");
    assert!(
        raised.is_none(),
        "one weird symlink must not take the directory down: {raised:?}"
    );

    let lines = active_lines(&s);
    let weird = &lines[line_of(&s, "weird")];
    assert_eq!(
        line_name(&s, weird),
        "weird -> ?",
        "the entry is listed with an unknown target"
    );
    assert!(
        lines.iter().any(|line| line_name(&s, line) == "real.txt"),
        "and the readable sibling is still there: {lines:#?}"
    );
    assert_eq!(
        lines.last().map(String::as_str),
        Some("1 entries unreadable"),
        "the footer counts it: {lines:#?}"
    );
}

// ---------------------------------------------------------------------------
// 11 --- unknown opts keys
// ---------------------------------------------------------------------------

/// A typo'd opt errors naming the key instead of silently listing in
/// fatal mode (framing §8, minor c). Silently ignoring it is exactly
/// how a tolerant listing would degrade with no signal at all.
#[test]
fn read_dir_rejects_an_unknown_opts_key() {
    let td = fixture_dir();
    let s = editor();
    let message: String = eval(
        &s,
        &format!(
            "local ok, err = pcall(pmacs.fs.read_dir, {:?}, {{ tolerat = true }})\n\
             if ok then return 'NO ERROR' end\n\
             return tostring(err)",
            td.path().display().to_string()
        ),
    );
    assert!(
        message.contains("tolerat") && message.contains("unknown opts key"),
        "the error must name the offending key; got {message:?}"
    );

    // A wrongly-typed known key is rejected too.
    let message: String = eval(
        &s,
        &format!(
            "local ok, err = pcall(pmacs.fs.read_dir, {:?}, {{ tolerant = 'yes' }})\n\
             if ok then return 'NO ERROR' end\n\
             return tostring(err)",
            td.path().display().to_string()
        ),
    );
    assert!(
        message.contains("tolerant must be a boolean"),
        "got {message:?}"
    );
}

// ---------------------------------------------------------------------------
// 12 --- non-UTF-8 names stay fatal
// ---------------------------------------------------------------------------

/// A non-UTF-8 *name* is a path-representation problem, not a listing
/// one: dired reports the structured error and creates no buffer.
/// Rendering it tolerantly would hand dired a name it could not pass
/// back through `rename`.
#[cfg(not(target_os = "macos"))]
#[test]
fn dired_reports_a_non_utf8_name_and_creates_no_buffer() {
    use std::os::unix::ffi::OsStrExt;
    let td = tempfile::tempdir().expect("tempdir");
    std::fs::write(
        td.path()
            .join(std::ffi::OsStr::from_bytes(b"bad-\xff-name")),
        b"",
    )
    .expect("write entry");

    let mut s = editor();
    let before = active_name(&s);
    // Through the real command, so the reporting path is the one a user
    // hits rather than `pmacs.dired.open`'s raise.
    exec(
        &s,
        &format!(
            "pmacs.async(function()\n\
               local ok, err = pcall(pmacs.dired.open, {:?})\n\
               if not ok then\n\
                 pmacs.editor.set_status('dired: ' .. tostring(err.message))\n\
               end\n\
             end)",
            td.path().display().to_string()
        ),
    );
    pump(&mut s);

    let line = status(&s);
    assert!(
        line.contains("non-UTF-8") && line.contains("255"),
        "the structured error must surface with the offending raw bytes; \
         got {line:?}"
    );
    assert!(
        dired_buffer_names(&s).is_empty(),
        "and no dired buffer was created: {:?}",
        dired_buffer_names(&s)
    );
    assert_eq!(active_name(&s), before, "the active buffer is untouched");
}

// ---------------------------------------------------------------------------
// 13 --- dired-jump
// ---------------------------------------------------------------------------

/// `C-x C-j` opens dired on this file's directory with the cursor on
/// that file's line; from a buffer with no path it reports and creates
/// nothing.
#[test]
fn dired_jump_seats_the_cursor_on_the_visited_file() {
    let td = fixture_dir();
    let (mut s, anchor) = editor_in(td.path());

    ctrl(&mut s, 'x');
    ctrl(&mut s, 'j');
    pump(&mut s);

    assert_eq!(
        active_name(&s),
        format!("*dired:{}*", canon(td.path())),
        "dired opens on the file's directory"
    );
    assert_eq!(
        cursor_entry(&s).as_deref(),
        Some("anchor.txt"),
        "with the cursor on the file we jumped from"
    );
    assert!(anchor.exists());

    // From a pathless buffer: report, create nothing.
    exec(
        &s,
        "pmacs.window.switch_buffer(pmacs.buffer.create('*pathless*'))",
    );
    let before = dired_buffer_names(&s);
    ctrl(&mut s, 'x');
    ctrl(&mut s, 'j');
    pump(&mut s);
    assert!(
        status(&s).contains("no file"),
        "the reason must surface; got {:?}",
        status(&s)
    );
    assert_eq!(
        dired_buffer_names(&s),
        before,
        "and nothing new was created"
    );
    assert_eq!(active_name(&s), "*pathless*", "nor was anything displayed");
}

// ---------------------------------------------------------------------------
// 14 --- quit
// ---------------------------------------------------------------------------

/// `q` restores the previously active buffer; in a side window it
/// routes through `pmacs.window.quit`, matching `listview.quit`'s
/// Q#BP11b split.
#[test]
fn dired_quit_restores_the_previous_buffer_and_closes_a_panel() {
    let td = fixture_dir();
    let (mut s, anchor) = editor_in(td.path());
    open_ok(&mut s, td.path(), "nil");
    type_char(&mut s, 'q');
    assert_eq!(
        active_name(&s),
        canon(&anchor),
        "`q` returns to the buffer dired was opened from"
    );

    // The panel arm: `q` deletes the side window rather than switching
    // the buffer inside it.
    open_ok(&mut s, td.path(), r#"{ display = "panel" }"#);
    assert!(side_window(&s).is_some(), "fixture premise: a side window");
    type_char(&mut s, 'q');
    assert_eq!(
        side_window(&s),
        None,
        "`q` in a side window routes through window.quit"
    );
    assert_eq!(
        active_name(&s),
        canon(&anchor),
        "and focus lands back in the document window"
    );
}

// ---------------------------------------------------------------------------
// 15 --- failure leaves nothing behind
// ---------------------------------------------------------------------------

/// `C-x d` on a nonexistent directory creates no buffer, switches no
/// window, and reports the reason (the fixture's
/// `dired_open_failure_leaves_editor_unchanged` invariant), driven
/// through the real prompt.
#[test]
fn dired_open_failure_leaves_the_editor_unchanged() {
    let td = fixture_dir();
    let (mut s, anchor) = editor_in(td.path());
    let before_window = active_window(&s);
    let before_names = buffer_names(&s);

    ctrl(&mut s, 'x');
    type_char(&mut s, 'd');
    // The field prefills with the anchor's directory; append a path
    // component that does not exist.
    type_str(&mut s, "/nope");
    press(&mut s, KeyCode::Enter);
    pump(&mut s);

    let line = status(&s);
    assert!(
        line.starts_with("dired: "),
        "the failure surfaces as dired's own status message; got {line:?}"
    );
    assert_eq!(
        buffer_names(&s),
        before_names,
        "no buffer was created: {:?}",
        buffer_names(&s)
    );
    assert_eq!(active_window(&s), before_window, "no window changed");
    assert_eq!(
        active_name(&s),
        canon(&anchor),
        "the active buffer is intact"
    );
}

// ---------------------------------------------------------------------------
// 16 --- scale
// ---------------------------------------------------------------------------

/// A 10,000-entry directory renders within the fixture's established
/// 200 ms budget, on the builtin path. Carries the fixture's macOS
/// ignore gate: hosted macOS debug runners do not consistently satisfy
/// it.
#[test]
#[cfg_attr(
    target_os = "macos",
    ignore = "hosted macOS debug runners do not consistently satisfy this timing gate"
)]
fn dired_renders_10k_entries_within_200ms() {
    let td = tempfile::tempdir().expect("tempdir");
    for i in 0..10_000 {
        std::fs::write(td.path().join(format!("f{i:05}")), b"").expect("write fixture entry");
    }

    let mut s = editor();
    let started = Instant::now();
    open_ok(&mut s, td.path(), "nil");
    let elapsed = started.elapsed();

    assert_eq!(
        active_lines(&s).len(),
        10_001,
        "header plus one line per entry"
    );
    assert!(
        elapsed < Duration::from_millis(200),
        "10K entries must render within 200ms; took {elapsed:?}"
    );
}
