// tests/m8_2_acceptance.rs --- T M8.2 dired-class acceptance.

//! Acceptance for the M8.2 deliverable: a Lua-only directory-view
//! package, installed via `install_local`, exercising the dev-loop
//! infrastructure shipped in M8.1 end-to-end.
//!
//! The package source-of-record for this test lives in-tree at
//! `tests/fixtures/pmacs-dired/`. A development copy also exists
//! alongside this repo at `~/Repos/util/pmacs-dired/`, but the
//! fixture is canonical: the test must remain reproducible from a
//! fresh clone of *this* repo with no sibling checkouts.
//!
//! The fixture is structurally external (separate manifest, loaded
//! via `install_local`); only its on-disk location is internal.
//! That preserves the M8 acceptance claim that dired-class fits
//! the buffer-and-views universality story without core support.
//!
//! Spec acceptance bullets being pinned:
//!
//! * Opening a directory of 10000 entries renders within 200 ms.
//! * Subdirectories navigate with Enter; parent with Backspace.
//! * Sort modes switch via commands, regenerate the buffer
//!   deterministically.

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

use pmacs::editor::EditorState;
use pmacs::frontend::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use pmacs::lua_bindings::PackageInstallOverride;
use pmacs::protocol::FrontendId;
use tempfile::TempDir;

/// Construct a synthetic keyboard event for tests that exercise
/// the editor's key-dispatch path. Mirrors the helper inside
/// `editor.rs`'s test module.
fn plain_key(code: KeyCode) -> KeyEvent {
    KeyEvent {
        code,
        modifiers: KeyModifiers::NONE,
        kind: KeyEventKind::Press,
        state: crossterm::event::KeyEventState::NONE,
    }
}

/// Resolve the on-disk path to the M8.2 dired package source.
/// Points at the in-tree fixture so the test is reproducible from
/// a fresh clone with no sibling checkouts.
fn dired_package_path() -> PathBuf {
    let here = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    here.join("tests").join("fixtures").join("pmacs-dired")
}

/// Pump the editor's async tick until `predicate` returns true or
/// the deadline fires. Mirrors the helper in `m4`/`m8_1` — the
/// dired package's `open()` flow goes through `pmacs.async(...)`
/// → `pmacs.fs.read_dir(...):await()`, which only settles on a
/// `tick_async()` cycle.
fn pump_until<F: Fn(&EditorState) -> bool>(state: &mut EditorState, predicate: F) {
    let deadline = Instant::now() + Duration::from_secs(5);
    while !predicate(state) {
        assert!(
            Instant::now() < deadline,
            "async pump deadline exceeded after 5s"
        );
        state.tick_async();
        std::thread::sleep(Duration::from_millis(2));
    }
}

/// Spin up an editor with package install overrides pointing at a
/// fresh temp dir. The dired package is then `install_local`-ed
/// from its sibling source path.
///
/// `EditorState::new()` sets the init-complete flag during startup
/// (the integration-test build doesn't get the `cfg(test)` guard
/// that lib tests do), so we reopen the init phase before
/// `install_local`. The `reopen_init_phase_for_testing` helper is
/// the documented test escape hatch.
fn editor_with_dired() -> (EditorState, TempDir, TempDir) {
    let cache = tempfile::tempdir().expect("cache tempdir");
    let user_root = tempfile::tempdir().expect("user-root tempdir");
    let mut state = EditorState::new();
    state.lua_host.reopen_init_phase_for_testing();
    state.lua_host.set_package_install_override(
        PackageInstallOverride::new()
            .with_cache_dir(cache.path().to_path_buf())
            .with_user_install_root(user_root.path().to_path_buf()),
    );
    let pkg = dired_package_path();
    let pkg_str = pkg.display().to_string();
    let install = format!(
        r#"
        pmacs.packages.install_local("{pkg_str}")
        require("pmacs-dired")
    "#
    );
    state
        .lua_host
        .eval(Some("dired-install"), &install)
        .unwrap_or_else(|e| panic!("install_local + require failed: {e}"));
    (state, cache, user_root)
}

/// Read the active buffer's full text.
fn active_buffer_text(state: &mut EditorState) -> String {
    state
        .lua_host
        .lua()
        .load(
            r"
            local buf = pmacs.window.buffer()
            return buf:slice(0, buf:len())
        ",
        )
        .eval::<String>()
        .expect("read active buffer")
}

// ---------------------------------------------------------------------------
// T M8.2 --- header + per-entry shape
// ---------------------------------------------------------------------------

#[test]
fn dired_open_renders_header_and_one_line_per_entry() {
    let dir = tempfile::tempdir().expect("tempdir");
    std::fs::write(dir.path().join("a.txt"), b"hello").expect("write a");
    std::fs::write(dir.path().join("b.txt"), b"world!").expect("write b");
    std::fs::create_dir(dir.path().join("subdir")).expect("mkdir");

    let (mut state, _c, _u) = editor_with_dired();
    let path_str = dir.path().display().to_string();

    let open = format!(
        r#"
        _G.M8_2_DONE = nil
        pmacs.async(function()
            require("pmacs-dired").open("{path_str}")
            _G.M8_2_DONE = true
        end)
    "#
    );
    state
        .lua_host
        .eval(Some("open"), &open)
        .unwrap_or_else(|e| panic!("dired.open failed: {e}"));
    pump_until(&mut state, |s| {
        s.lua_host
            .lua()
            .globals()
            .get::<bool>("M8_2_DONE")
            .unwrap_or(false)
    });

    let text = active_buffer_text(&mut state);
    let lines: Vec<&str> = text.lines().collect();
    // 1 header + 3 entries.
    assert_eq!(
        lines.len(),
        4,
        "expected 1 header + 3 entries, got {} lines: {text}",
        lines.len()
    );

    // Header is the path with trailing colon.
    assert_eq!(lines[0], format!("{path_str}:"));

    // Each entry line starts with a kind char then 9 perm chars
    // then a space. Subdir lines start with 'd', files with '-'.
    let mut saw_subdir = false;
    let mut saw_a = false;
    let mut saw_b = false;
    for line in &lines[1..] {
        assert!(line.len() > 11, "entry line too short: {line:?}");
        if line.contains(" subdir") {
            assert!(
                line.starts_with('d'),
                "subdir line should start with 'd': {line}"
            );
            saw_subdir = true;
        }
        if line.contains(" a.txt") {
            assert!(
                line.starts_with('-'),
                "a.txt line should start with '-': {line}"
            );
            assert!(line.contains(" 5 "), "size 5 should appear: {line}");
            saw_a = true;
        }
        if line.contains(" b.txt") {
            assert!(
                line.starts_with('-'),
                "b.txt line should start with '-': {line}"
            );
            assert!(line.contains(" 6 "), "size 6 should appear: {line}");
            saw_b = true;
        }
    }
    assert!(
        saw_subdir && saw_a && saw_b,
        "all three entries must render"
    );
}

// ---------------------------------------------------------------------------
// T M8.2 --- 10K entries render under 200 ms
// ---------------------------------------------------------------------------

#[test]
#[cfg_attr(
    target_os = "macos",
    ignore = "hosted macOS debug runners do not consistently satisfy this timing gate"
)]
fn dired_open_renders_10k_entries_under_200ms() {
    // Build a directory of 10K small files. The fixture creation
    // itself isn't fast (10K syscalls), so we measure only the
    // dired open() call, not setup.
    let dir = tempfile::tempdir().expect("tempdir");
    for i in 0..10_000 {
        std::fs::write(dir.path().join(format!("f{i:05}")), b"").expect("write fixture entry");
    }

    let (mut state, _c, _u) = editor_with_dired();
    let path_str = dir.path().display().to_string();

    let open = format!(
        r#"
        _G.M8_2_DONE = nil
        _G.M8_2_OPEN_START = nil
        _G.M8_2_OPEN_END = nil
        pmacs.async(function()
            _G.M8_2_OPEN_START = os.clock()
            require("pmacs-dired").open("{path_str}")
            _G.M8_2_OPEN_END = os.clock()
            _G.M8_2_DONE = true
        end)
    "#
    );
    state
        .lua_host
        .eval(Some("open10k"), &open)
        .unwrap_or_else(|e| panic!("dired.open(10K) failed: {e}"));

    let outer_start = Instant::now();
    pump_until(&mut state, |s| {
        s.lua_host
            .lua()
            .globals()
            .get::<bool>("M8_2_DONE")
            .unwrap_or(false)
    });
    let outer_elapsed = outer_start.elapsed();

    // The pump-loop measurement includes our 2ms sleeps between
    // tick_async cycles. The async-runtime budget for the
    // read_dir reply is bounded by the worker pool's dispatch
    // latency plus the actual lstat-loop cost. For the spec's
    // 200ms ceiling we use the wall-clock from open() to pump
    // settle, which the inner os.clock() reading also captures
    // for cross-checking.
    assert!(
        outer_elapsed < Duration::from_millis(200),
        "M8.2 spec: 10K entries must render within 200ms; took {outer_elapsed:?}"
    );

    // Sanity: the buffer has 1 header + 10000 entry lines.
    let text = active_buffer_text(&mut state);
    let line_count = text.lines().count();
    assert_eq!(
        line_count, 10_001,
        "10K entries + 1 header = 10001 lines; got {line_count}"
    );
}

// ---------------------------------------------------------------------------
// T M8.2 --- sort modes regenerate the buffer deterministically
// ---------------------------------------------------------------------------

#[test]
fn dired_sort_commands_regenerate_buffer_deterministically() {
    // Three files with deliberately disjoint name/size ordering so
    // each sort mode produces a distinct expected order. mtime is
    // hard to control deterministically without sleep; we rely on
    // the name + size cases.
    let dir = tempfile::tempdir().expect("tempdir");
    std::fs::write(dir.path().join("a"), b"1234567890").expect("a"); // 10 bytes
    std::fs::write(dir.path().join("b"), b"x").expect("b"); // 1 byte
    std::fs::write(dir.path().join("c"), b"yy").expect("c"); // 2 bytes

    let (mut state, _c, _u) = editor_with_dired();
    let path_str = dir.path().display().to_string();

    // Open and verify default sort (by name).
    let open = format!(
        r#"
        _G.DONE = nil
        pmacs.async(function()
            require("pmacs-dired").open("{path_str}")
            _G.DONE = true
        end)
    "#
    );
    state
        .lua_host
        .eval(Some("sort-open"), &open)
        .unwrap_or_else(|e| panic!("open failed: {e}"));
    pump_until(&mut state, |s| {
        s.lua_host
            .lua()
            .globals()
            .get::<bool>("DONE")
            .unwrap_or(false)
    });

    let by_name_order = entry_filenames_in_buffer(&mut state);
    assert_eq!(
        by_name_order,
        vec!["a", "b", "c"],
        "default sort should be by name ascending"
    );

    // Sort by size (largest first): a (10), c (2), b (1).
    state
        .lua_host
        .eval(
            Some("sort-size"),
            r#"pmacs.command.invoke("pmacs-dired.sort-size")"#,
        )
        .unwrap_or_else(|e| panic!("sort-size command failed: {e}"));
    let by_size_order = entry_filenames_in_buffer(&mut state);
    assert_eq!(
        by_size_order,
        vec!["a", "c", "b"],
        "sort-size should produce largest first"
    );

    // Re-sort by name and verify we're back to alphabetical.
    state
        .lua_host
        .eval(
            Some("sort-name"),
            r#"pmacs.command.invoke("pmacs-dired.sort-name")"#,
        )
        .unwrap_or_else(|e| panic!("sort-name command failed: {e}"));
    let by_name_again = entry_filenames_in_buffer(&mut state);
    assert_eq!(
        by_name_again,
        vec!["a", "b", "c"],
        "re-sort by name should be deterministic"
    );
}

/// Extract entry filenames in display order from the active dired
/// buffer. Skips the header (line 0) and pulls the trailing token
/// of each entry line (filename) for comparison.
fn entry_filenames_in_buffer(state: &mut EditorState) -> Vec<String> {
    let text = active_buffer_text(state);
    text.lines()
        .skip(1)
        .filter_map(|line| line.split_whitespace().last().map(str::to_string))
        .collect()
}

// ---------------------------------------------------------------------------
// T M8.2 --- parent-directory navigation rebuilds the listing
// ---------------------------------------------------------------------------

#[test]
fn dired_parent_command_navigates_up_one_level() {
    // Open dired on a subdirectory, run the parent command, verify
    // the buffer now lists the parent's contents.
    let root = tempfile::tempdir().expect("tempdir");
    let child = root.path().join("child");
    std::fs::create_dir(&child).expect("mkdir child");
    std::fs::write(child.join("inner.txt"), b"i").expect("write inner");
    std::fs::write(root.path().join("sibling.txt"), b"s").expect("write sibling");

    let (mut state, _c, _u) = editor_with_dired();
    let child_str = child.display().to_string();

    let open = format!(
        r#"
        _G.DONE = nil
        pmacs.async(function()
            require("pmacs-dired").open("{child_str}")
            _G.DONE = true
        end)
    "#
    );
    state
        .lua_host
        .eval(Some("open-child"), &open)
        .unwrap_or_else(|e| panic!("open child failed: {e}"));
    pump_until(&mut state, |s| {
        s.lua_host
            .lua()
            .globals()
            .get::<bool>("DONE")
            .unwrap_or(false)
    });

    // Sanity: buffer shows child's contents.
    assert!(
        active_buffer_text(&mut state).contains("inner.txt"),
        "child's listing should contain inner.txt"
    );

    // Run the parent command. It dispatches another async work,
    // so we pump again.
    state
        .lua_host
        .eval(
            Some("parent"),
            r#"
                _G.DONE2 = nil
                pmacs.command.invoke("pmacs-dired.parent")
                pmacs.async(function()
                    -- Yield a tick so the parent-command's nested
                    -- pmacs.async has time to settle. We rely on
                    -- pmacs.workers.sleep as the canonical "yield
                    -- one tick" helper.
                    pmacs.workers.sleep(20):await()
                    _G.DONE2 = true
                end)
            "#,
        )
        .unwrap_or_else(|e| panic!("parent command failed: {e}"));
    pump_until(&mut state, |s| {
        s.lua_host
            .lua()
            .globals()
            .get::<bool>("DONE2")
            .unwrap_or(false)
    });

    let after = active_buffer_text(&mut state);
    let root_str = root.path().display().to_string();
    assert!(
        after.starts_with(&format!("{root_str}:")),
        "after parent, header should be the root path; got: {}",
        after.lines().next().unwrap_or("")
    );
    assert!(
        after.contains("sibling.txt"),
        "parent listing should include sibling.txt"
    );
    assert!(
        after.contains(" child"),
        "parent listing should include the original child as an entry"
    );
}

// ---------------------------------------------------------------------------
// T M8.2 --- Enter on a subdirectory navigates into it
// ---------------------------------------------------------------------------
//
// Pins the spec bullet "Subdirectories navigate with Enter". The
// header advertises this acceptance, and the open-line command
// already implemented it; this test exercises the path end-to-end
// (cursor positioning + command invocation + buffer rebuild).

#[test]
fn dired_open_line_navigates_into_subdirectory_under_cursor() {
    // Parent has exactly one entry --- the subdirectory --- so we
    // know cursor line 1 is the subdir without depending on sort
    // semantics or file listing order.
    let root = tempfile::tempdir().expect("tempdir");
    let only = root.path().join("only_subdir");
    std::fs::create_dir(&only).expect("mkdir only_subdir");
    std::fs::write(only.join("inside.txt"), b"present").expect("write inside");

    let (mut state, _c, _u) = editor_with_dired();
    let root_str = root.path().display().to_string();

    // Open dired on the parent.
    let open = format!(
        r#"
        _G.DONE = nil
        pmacs.async(function()
            require("pmacs-dired").open("{root_str}")
            _G.DONE = true
        end)
    "#
    );
    state
        .lua_host
        .eval(Some("open-parent"), &open)
        .unwrap_or_else(|e| panic!("open parent failed: {e}"));
    pump_until(&mut state, |s| {
        s.lua_host
            .lua()
            .globals()
            .get::<bool>("DONE")
            .unwrap_or(false)
    });

    // Sanity: parent listing has only_subdir on line 1 (line 0 is
    // the header).
    let before = active_buffer_text(&mut state);
    let lines: Vec<&str> = before.lines().collect();
    assert_eq!(
        lines.len(),
        2,
        "parent should have 1 header + 1 entry; got {}: {before}",
        lines.len()
    );
    assert!(
        lines[1].contains("only_subdir"),
        "entry line 1 must be only_subdir: {}",
        lines[1]
    );

    // Position the cursor on entry line 1, then invoke open-line.
    // The pmacs.editor primitives are `move_line_start` + a loop of
    // `move_down`. We move to top via repeated move_up, then exactly
    // one move_down to land on line 1.
    state
        .lua_host
        .eval(
            Some("enter-on-subdir"),
            r#"
                while pmacs.editor.cursor_line() > 0 do
                    pmacs.editor.move_up()
                end
                pmacs.editor.move_line_start()
                pmacs.editor.move_down()
                _G.DONE2 = nil
                pmacs.command.invoke("pmacs-dired.open-line")
                pmacs.async(function()
                    -- The open-line dir branch dispatches another
                    -- pmacs.async; yield once so its read_dir reply
                    -- settles before we read the buffer.
                    pmacs.workers.sleep(20):await()
                    _G.DONE2 = true
                end)
            "#,
        )
        .unwrap_or_else(|e| panic!("enter-on-subdir failed: {e}"));
    pump_until(&mut state, |s| {
        s.lua_host
            .lua()
            .globals()
            .get::<bool>("DONE2")
            .unwrap_or(false)
    });

    // Buffer must now show the subdirectory's listing.
    let after = active_buffer_text(&mut state);
    let only_str = only.display().to_string();
    assert!(
        after.starts_with(&format!("{only_str}:")),
        "after Enter on subdir, header should be the subdir path; got: {}",
        after.lines().next().unwrap_or("")
    );
    assert!(
        after.contains("inside.txt"),
        "subdir listing should include inside.txt; got: {after}"
    );
}

// ---------------------------------------------------------------------------
// T M8.2 --- Enter on a file errors with a clear message (stub posture)
// ---------------------------------------------------------------------------
//
// The v0.1 dired package can't open files directly --- there's no
// buffer-from-file primitive yet --- so the open-line command for
// non-dir entries must error explicitly rather than silently no-op.
// Pins the stub-posture contract: error names the workaround.

#[test]
fn dired_open_line_on_file_errors_with_actionable_message() {
    let dir = tempfile::tempdir().expect("tempdir");
    std::fs::write(dir.path().join("a_file.txt"), b"content").expect("write file");

    let (mut state, _c, _u) = editor_with_dired();
    let path_str = dir.path().display().to_string();

    let open = format!(
        r#"
        _G.DONE = nil
        pmacs.async(function()
            require("pmacs-dired").open("{path_str}")
            _G.DONE = true
        end)
    "#
    );
    state
        .lua_host
        .eval(Some("open-files"), &open)
        .unwrap_or_else(|e| panic!("open failed: {e}"));
    pump_until(&mut state, |s| {
        s.lua_host
            .lua()
            .globals()
            .get::<bool>("DONE")
            .unwrap_or(false)
    });

    // Position the cursor on entry line 1 and invoke. open-line
    // should raise (caught via pcall here so we can inspect the
    // message).
    let msg: String = state
        .lua_host
        .lua()
        .load(
            r#"
                while pmacs.editor.cursor_line() > 0 do
                    pmacs.editor.move_up()
                end
                pmacs.editor.move_line_start()
                pmacs.editor.move_down()
                local ok, err = pcall(function()
                    pmacs.command.invoke("pmacs-dired.open-line")
                end)
                assert(not ok, "open-line on file should have errored")
                return tostring(err)
            "#,
        )
        .eval()
        .expect("file-branch error eval");
    assert!(
        msg.contains("buffer-from-file"),
        "error must name the missing capability: {msg}"
    );
    assert!(
        msg.contains("C-x C-f") || msg.contains("file-open"),
        "error must point at a workaround: {msg}"
    );
    assert!(
        msg.contains("a_file.txt"),
        "error must include the offending path: {msg}"
    );
}

// ---------------------------------------------------------------------------
// T M8.2 --- relative paths to dired.open are rejected up front
// ---------------------------------------------------------------------------

#[test]
fn dired_open_rejects_relative_paths() {
    let (state, _c, _u) = editor_with_dired();
    let msg: String = state
        .lua_host
        .lua()
        .load(
            r#"
                local ok, err = pcall(function()
                    require("pmacs-dired").open("./relative")
                end)
                assert(not ok, "relative path should reject")
                return tostring(err)
            "#,
        )
        .eval()
        .expect("relative-path eval");
    assert!(
        msg.contains("absolute"),
        "error must call out the absolute requirement: {msg}"
    );
}

// ---------------------------------------------------------------------------
// T M8.2 --- newlines in filenames don't break the line-per-entry invariant
// ---------------------------------------------------------------------------
//
// POSIX filenames may contain `\n` and `\r`. If render_entry passed
// such names through verbatim the buffer's one-line-per-entry
// contract --- which the wdired layer (M8.3) and cursor-line ->
// entry mapping both depend on --- would silently break. Verifies
// that `escape_displayable` keeps each entry on a single line.

#[test]
fn dired_escapes_newlines_in_filenames() {
    let dir = tempfile::tempdir().expect("tempdir");
    // Create a file with a literal newline in its name. POSIX
    // permits this; ext4 happily accepts it.
    let nasty_name = "evil\nname.txt";
    std::fs::write(dir.path().join(nasty_name), b"x").expect("write nasty");
    std::fs::write(dir.path().join("normal.txt"), b"y").expect("write normal");

    let (mut state, _c, _u) = editor_with_dired();
    let path_str = dir.path().display().to_string();

    let open = format!(
        r#"
        _G.DONE = nil
        pmacs.async(function()
            require("pmacs-dired").open("{path_str}")
            _G.DONE = true
        end)
    "#
    );
    state
        .lua_host
        .eval(Some("open-nasty"), &open)
        .unwrap_or_else(|e| panic!("open failed: {e}"));
    pump_until(&mut state, |s| {
        s.lua_host
            .lua()
            .globals()
            .get::<bool>("DONE")
            .unwrap_or(false)
    });

    let text = active_buffer_text(&mut state);
    let line_count = text.lines().count();
    // 1 header + 2 entries = 3 lines. Without escaping, the nasty
    // entry would render as two physical lines, giving 4.
    assert_eq!(
        line_count, 3,
        "newline-in-name must not split into multiple buffer lines; got {line_count} lines:\n{text}"
    );
    assert!(
        text.contains("evil\\nname.txt"),
        "newline must render as the escaped sequence \\n: {text}"
    );
}

// ---------------------------------------------------------------------------
// T M8.2 --- escape_displayable disambiguates literal `\` from real newlines
// ---------------------------------------------------------------------------
//
// Wdired (M8.3) will parse rendered entry text to detect line
// edits, so the rendering must be a one-to-one encoding: a
// filename containing the literal two-character sequence `\` `n`
// must not look identical to one containing an actual newline.
// We escape literal backslashes first, then the control chars,
// matching `ls --quoting=c`. This test exercises the test seam's
// `escape_displayable` directly because constructing a filename
// containing a literal backslash-n in a portable way and reading
// it back through pmacs.fs is brittle (some filesystems normalize
// surrogate pairs); the function-level test is the load-bearing
// guarantee.

#[test]
fn dired_escape_displayable_round_trip_is_unambiguous() {
    let (state, _c, _u) = editor_with_dired();
    let result: mlua::Table = state
        .lua_host
        .lua()
        .load(
            r#"
                local esc = require("pmacs-dired")._test.escape_displayable
                return {
                    -- Real newline -> two-char escape "\n"
                    real_newline = esc("a\nb"),
                    -- Literal "\n" (two chars) -> "\\n" (escaped)
                    literal_backslash_n = esc("a\\nb"),
                    -- Real CR
                    real_cr = esc("a\rb"),
                    -- Other C0 controls use visible hex escapes
                    c0_soh = esc("a" .. string.char(1) .. "b"),
                    c0_us = esc("a" .. string.char(31) .. "b"),
                    -- Literal "\\" -> "\\\\"
                    only_backslash = esc("\\"),
                    -- ASCII passes through
                    ascii_only = esc("plain.txt"),
                }
            "#,
        )
        .eval()
        .expect("escape eval");

    let real_newline: String = result.get("real_newline").unwrap();
    let literal_bs_n: String = result.get("literal_backslash_n").unwrap();
    let real_cr: String = result.get("real_cr").unwrap();
    let c0_soh: String = result.get("c0_soh").unwrap();
    let c0_us: String = result.get("c0_us").unwrap();
    let only_backslash: String = result.get("only_backslash").unwrap();
    let ascii: String = result.get("ascii_only").unwrap();

    // The whole point: the two encodings must differ.
    assert_ne!(
        real_newline, literal_bs_n,
        "real newline and literal \\n must encode distinctly; both -> {real_newline}"
    );
    assert_eq!(
        real_newline, "a\\nb",
        "real newline -> a\\nb; got {real_newline}"
    );
    assert_eq!(
        literal_bs_n, "a\\\\nb",
        "literal \\n -> a\\\\nb; got {literal_bs_n}"
    );
    assert_eq!(real_cr, "a\\rb");
    assert_eq!(c0_soh, "a\\x01b");
    assert_eq!(c0_us, "a\\x1Fb");
    assert_eq!(only_backslash, "\\\\");
    assert_eq!(ascii, "plain.txt");
}

// ---------------------------------------------------------------------------
// T M8.2 --- initial open is atomic on read failure
// ---------------------------------------------------------------------------
//
// `open(path)` must not create an empty dired buffer or switch windows
// before it knows `path` can be read. Navigation already has the
// read-then-commit guarantee; initial open needs the same property.

#[test]
fn dired_open_failure_leaves_editor_unchanged() {
    let root = tempfile::tempdir().expect("tempdir");
    let missing = root.path().join("__missing__");
    let missing_str = missing.display().to_string();

    let (mut state, _c, _u) = editor_with_dired();
    let before_text = active_buffer_text(&mut state);

    let open = format!(
        r#"
        _G.OPEN_FAIL_DONE = nil
        pmacs.async(function()
            local before_buf = pmacs.window.buffer()
            local before_count = #pmacs.buffer.list()
            local ok, err = pcall(function()
                require("pmacs-dired").open("{missing_str}")
            end)
            _G.OPEN_FAIL_OK = ok
            _G.OPEN_FAIL_ERR = tostring(err)
            _G.OPEN_FAIL_BUFFER_UNCHANGED = (pmacs.window.buffer() == before_buf)
            _G.OPEN_FAIL_COUNT_UNCHANGED = (#pmacs.buffer.list() == before_count)
            _G.OPEN_FAIL_DONE = true
        end)
    "#
    );
    state
        .lua_host
        .eval(Some("open-failure-atomic"), &open)
        .unwrap_or_else(|e| panic!("open failure eval failed: {e}"));
    pump_until(&mut state, |s| {
        s.lua_host
            .lua()
            .globals()
            .get::<bool>("OPEN_FAIL_DONE")
            .unwrap_or(false)
    });

    let ok: bool = state
        .lua_host
        .lua()
        .globals()
        .get("OPEN_FAIL_OK")
        .expect("OPEN_FAIL_OK");
    assert!(!ok, "open on missing path must fail");
    let buffer_unchanged: bool = state
        .lua_host
        .lua()
        .globals()
        .get("OPEN_FAIL_BUFFER_UNCHANGED")
        .expect("OPEN_FAIL_BUFFER_UNCHANGED");
    assert!(
        buffer_unchanged,
        "active buffer must not change on failed open"
    );
    let count_unchanged: bool = state
        .lua_host
        .lua()
        .globals()
        .get("OPEN_FAIL_COUNT_UNCHANGED")
        .expect("OPEN_FAIL_COUNT_UNCHANGED");
    assert!(count_unchanged, "failed open must not create a buffer");
    assert_eq!(
        before_text,
        active_buffer_text(&mut state),
        "failed open must leave active buffer bytes unchanged"
    );
}

// ---------------------------------------------------------------------------
// T M8.2 --- navigation is atomic on read failure (read-then-commit)
// ---------------------------------------------------------------------------
//
// If `pmacs.fs.read_dir(target):await()` fails for any reason
// (path doesn't exist, EACCES, etc.), the dired buffer must
// remain showing the previous listing rather than landing in a
// half-updated state where the header points at the new path but
// the body still shows the old entries. The wdired layer (M8.3)
// and the cursor-line -> entry mapping both rely on the
// header-and-body always agreeing.

#[test]
fn dired_navigation_failure_leaves_handle_unchanged() {
    let dir = tempfile::tempdir().expect("tempdir");
    std::fs::write(dir.path().join("alive.txt"), b"present").expect("write alive");

    let (mut state, _c, _u) = editor_with_dired();
    let path_str = dir.path().display().to_string();

    let open = format!(
        r#"
        _G.DONE = nil
        _G.D_HANDLE = nil
        pmacs.async(function()
            _G.D_HANDLE = require("pmacs-dired").open("{path_str}")
            _G.DONE = true
        end)
    "#
    );
    state
        .lua_host
        .eval(Some("open-atomic"), &open)
        .unwrap_or_else(|e| panic!("open failed: {e}"));
    pump_until(&mut state, |s| {
        s.lua_host
            .lua()
            .globals()
            .get::<bool>("DONE")
            .unwrap_or(false)
    });

    let before = active_buffer_text(&mut state);
    assert!(
        before.contains("alive.txt"),
        "pre-navigation buffer must show alive.txt; got: {before}"
    );

    // Try to navigate to a path that definitely doesn't exist.
    // Use a temp-tree subpath that hasn't been created --- read_dir
    // returns ENOENT, the await fails with the structured error,
    // and the test seam's navigate_to should leave handle.path /
    // .entries / the buffer unchanged.
    let bogus = format!("{path_str}/__no_such_dir__");
    let nav = format!(
        r#"
        _G.NAV_OK = nil
        _G.NAV_ERR = nil
        _G.NAV_DONE = nil
        local h = _G.D_HANDLE
        local path_before = h.path
        local entries_before = #h.entries
        local navigate_to = require("pmacs-dired")._test.navigate_to
        pmacs.async(function()
            local ok, err = pcall(navigate_to, h, "{bogus}")
            _G.NAV_OK = ok
            _G.NAV_ERR = tostring(err)
            -- After failure, handle must be unchanged.
            _G.NAV_PATH_UNCHANGED = (h.path == path_before)
            _G.NAV_ENTRIES_UNCHANGED = (#h.entries == entries_before)
            _G.NAV_DONE = true
        end)
    "#
    );
    state
        .lua_host
        .eval(Some("nav-atomic"), &nav)
        .unwrap_or_else(|e| panic!("nav eval failed: {e}"));
    pump_until(&mut state, |s| {
        s.lua_host
            .lua()
            .globals()
            .get::<bool>("NAV_DONE")
            .unwrap_or(false)
    });

    let nav_ok: bool = state
        .lua_host
        .lua()
        .globals()
        .get("NAV_OK")
        .expect("NAV_OK");
    assert!(
        !nav_ok,
        "navigate_to to non-existent path must error (got success)"
    );
    let path_ok: bool = state
        .lua_host
        .lua()
        .globals()
        .get("NAV_PATH_UNCHANGED")
        .expect("NAV_PATH_UNCHANGED");
    assert!(
        path_ok,
        "handle.path must be unchanged after failed navigation"
    );
    let entries_ok: bool = state
        .lua_host
        .lua()
        .globals()
        .get("NAV_ENTRIES_UNCHANGED")
        .expect("NAV_ENTRIES_UNCHANGED");
    assert!(
        entries_ok,
        "handle.entries must be unchanged after failed navigation"
    );

    let after = active_buffer_text(&mut state);
    assert_eq!(
        before, after,
        "buffer must be byte-identical after a failed navigation"
    );
}

// ---------------------------------------------------------------------------
// T M8.2 --- sort-mtime sorts newest-first
// ---------------------------------------------------------------------------
//
// The pmacs-dired.sort-mtime command was advertised but not
// directly tested in the first pass. Construct three files with
// controlled mtimes, invoke the command, verify the rendered
// order is newest-first.

#[test]
fn dired_sort_mtime_orders_newest_first() {
    let dir = tempfile::tempdir().expect("tempdir");
    // Create files in a known order, then explicitly stamp their
    // mtimes to make the test independent of filesystem timestamp
    // resolution / ordering during creation.
    let oldest = dir.path().join("c_oldest");
    let middle = dir.path().join("a_middle");
    let newest = dir.path().join("b_newest");
    for p in [&oldest, &middle, &newest] {
        std::fs::write(p, b"x").expect("write");
    }
    let now = SystemTime::now();
    std::fs::File::options()
        .write(true)
        .open(&oldest)
        .unwrap()
        .set_modified(now - Duration::from_mins(2))
        .expect("set mtime oldest");
    std::fs::File::options()
        .write(true)
        .open(&middle)
        .unwrap()
        .set_modified(now - Duration::from_mins(1))
        .expect("set mtime middle");
    std::fs::File::options()
        .write(true)
        .open(&newest)
        .unwrap()
        .set_modified(now)
        .expect("set mtime newest");

    let (mut state, _c, _u) = editor_with_dired();
    let path_str = dir.path().display().to_string();

    let open = format!(
        r#"
        _G.DONE = nil
        pmacs.async(function()
            require("pmacs-dired").open("{path_str}")
            _G.DONE = true
        end)
    "#
    );
    state
        .lua_host
        .eval(Some("mtime-open"), &open)
        .unwrap_or_else(|e| panic!("open failed: {e}"));
    pump_until(&mut state, |s| {
        s.lua_host
            .lua()
            .globals()
            .get::<bool>("DONE")
            .unwrap_or(false)
    });

    state
        .lua_host
        .eval(
            Some("sort-mtime"),
            r#"pmacs.command.invoke("pmacs-dired.sort-mtime")"#,
        )
        .unwrap_or_else(|e| panic!("sort-mtime failed: {e}"));
    let order = entry_filenames_in_buffer(&mut state);
    assert_eq!(
        order,
        vec!["b_newest", "a_middle", "c_oldest"],
        "sort-mtime must list newest first"
    );
}

// ---------------------------------------------------------------------------
// T M8.2 --- RET / Backspace key dispatch invokes the bound commands
// ---------------------------------------------------------------------------
//
// The other navigation tests invoke commands directly via
// pmacs.command.invoke, which masks regressions in the actual
// keybinding path (M.open's pmacs.keymap.bind calls). This test
// dispatches synthetic key events through EditorState::dispatch_key,
// the same path the running editor uses.

#[test]
fn dired_ret_and_backspace_keybindings_navigate() {
    let root = tempfile::tempdir().expect("tempdir");
    let only = root.path().join("only_subdir");
    std::fs::create_dir(&only).expect("mkdir");
    std::fs::write(only.join("inner.txt"), b"in").expect("write inner");

    let (mut state, _c, _u) = editor_with_dired();
    let root_str = root.path().display().to_string();
    let only_str = only.display().to_string();

    let open = format!(
        r#"
        _G.DONE = nil
        pmacs.async(function()
            require("pmacs-dired").open("{root_str}")
            _G.DONE = true
        end)
    "#
    );
    state
        .lua_host
        .eval(Some("open-keybind"), &open)
        .unwrap_or_else(|e| panic!("open failed: {e}"));
    pump_until(&mut state, |s| {
        s.lua_host
            .lua()
            .globals()
            .get::<bool>("DONE")
            .unwrap_or(false)
    });

    // Position cursor on the entry line (line 1).
    state
        .lua_host
        .eval(
            Some("cursor-to-entry"),
            r"
                while pmacs.editor.cursor_line() > 0 do
                    pmacs.editor.move_up()
                end
                pmacs.editor.move_line_start()
                pmacs.editor.move_down()
            ",
        )
        .expect("cursor positioning");

    // Press RET. The keybinding routes to pmacs-dired.open-line,
    // which dispatches a pmacs.async navigation. Wait for the
    // header to flip to the subdir path.
    state.dispatch_key(FrontendId::LOCAL, plain_key(KeyCode::Enter));
    let target_header = format!("{only_str}:");
    pump_until(&mut state, |s| {
        let lua = s.lua_host.lua();
        match lua
            .load(
                r"
                    local buf = pmacs.window.buffer()
                    return buf:slice(0, buf:len())
                ",
            )
            .eval::<String>()
        {
            Ok(text) => text.starts_with(&target_header),
            Err(_) => false,
        }
    });
    assert!(
        active_buffer_text(&mut state).contains("inner.txt"),
        "after RET the subdir's inner.txt should be visible"
    );

    // Press Backspace. Should navigate back to the root via the
    // pmacs-dired.parent binding.
    state.dispatch_key(FrontendId::LOCAL, plain_key(KeyCode::Backspace));
    let root_header = format!("{root_str}:");
    pump_until(&mut state, |s| {
        let lua = s.lua_host.lua();
        match lua
            .load(
                r"
                    local buf = pmacs.window.buffer()
                    return buf:slice(0, buf:len())
                ",
            )
            .eval::<String>()
        {
            Ok(text) => text.starts_with(&root_header),
            Err(_) => false,
        }
    });
    assert!(
        active_buffer_text(&mut state).contains("only_subdir"),
        "after Backspace the root's only_subdir entry should be visible"
    );
}

// ---------------------------------------------------------------------------
// T M8.2 --- pmacs-dired itself reloads post-init
// ---------------------------------------------------------------------------
//
// The package claims reload safety, and the runtime supports command
// unregistering from post-init unload hooks. Pin the actual fixture so
// a future package-local cleanup regression can't hide behind the
// generic M8.1d lifecycle tests.

#[test]
fn dired_package_reload_is_safe_after_init_complete() {
    let dir = tempfile::tempdir().expect("tempdir");
    std::fs::write(dir.path().join("after_reload.txt"), b"ok").expect("write");
    let path_str = dir.path().display().to_string();

    let (mut state, _c, _u) = editor_with_dired();
    state.lua_host.set_init_complete();

    state
        .lua_host
        .eval(
            Some("reload-dired"),
            r#"
                pmacs.packages.reload("pmacs-dired")
                assert(pmacs.command.exists("pmacs-dired.open-line"))
                assert(pmacs.command.exists("pmacs-dired.parent"))
                assert(pmacs.command.exists("pmacs-dired.sort-name"))
                assert(pmacs.command.exists("pmacs-dired.sort-mtime"))
                assert(pmacs.command.exists("pmacs-dired.sort-size"))
            "#,
        )
        .unwrap_or_else(|e| panic!("pmacs-dired reload failed: {e}"));

    let open = format!(
        r#"
        _G.RELOAD_OPEN_DONE = nil
        pmacs.async(function()
            require("pmacs-dired").open("{path_str}")
            _G.RELOAD_OPEN_DONE = true
        end)
    "#
    );
    state
        .lua_host
        .eval(Some("reload-dired-open"), &open)
        .unwrap_or_else(|e| panic!("post-reload open failed: {e}"));
    pump_until(&mut state, |s| {
        s.lua_host
            .lua()
            .globals()
            .get::<bool>("RELOAD_OPEN_DONE")
            .unwrap_or(false)
    });

    assert!(
        active_buffer_text(&mut state).contains("after_reload.txt"),
        "fresh dired buffer should still open after package reload"
    );
}

// ---------------------------------------------------------------------------
// T M8.2 --- pmacs-dired source size under 1500 lines
// ---------------------------------------------------------------------------

#[test]
fn dired_source_size_under_audit_ceiling() {
    let init_path = dired_package_path().join("init.lua");
    let src = std::fs::read_to_string(&init_path).expect("read init.lua");
    let lines = src.lines().count();
    assert!(
        lines < 1500,
        "M8.2 spec: dired source under 1500 lines; got {lines}"
    );
}
