// tests/journey_acceptance.rs --- the golden product journey.

//! The first cross-subsystem acceptance suite (`COHERENCE.md` §19,
//! `docs/journey-stage1a-framing.md` §5).
//!
//! Every other suite in the tree pins one subsystem's contract. This one
//! pins that the subsystems form a usable whole, walking `COHERENCE.md`
//! §2's twelve-step journey. Stage 1a seeds it with the steps that are
//! real today — 2 (launch unconfigured), 3 (open a real project), and 5
//! (edit immediately). Steps 6–12 join as later stages make them real.
//!
//! **This file is a ratchet: stages add rows, none removes them.**
//!
//! Two disciplines it must keep:
//!
//! * **Drive the real entry point.** A directory arm with no production
//!   caller passes every direct-call test, so step 3 goes through
//!   `EditorState::open` — the same function `pmacs FILE` calls — and
//!   not through `resolve_target_buffer`.
//! * **Pump to quiescence, never to a frame count.** Every listing is
//!   worker-dispatched; `tick_async` resuming a coroutine in the frame
//!   its result arrives does not bound when the worker finishes.
//!
//! Pins are labelled **N** (new behavior — must fail on full revert) or
//! **P** (preservation — legitimately green on the pre-image, falsified
//! by the named targeted mutation). See framing §6.0 for why the
//! distinction is load-bearing: an equivalence assertion between two
//! implementations that already agree proves nothing about structural
//! reuse.
//!
//! Two P pins here — P1 and P2 — *also* fail on full revert, since
//! `commit_to` does not exist on the pre-image. They are labelled P
//! because their discriminating falsifier is the named mutation: a
//! revert-only check cannot distinguish "validates" from "validates in
//! time", which is their entire claim. Each says so at its own site.

use std::path::Path;
use std::time::{Duration, Instant};

use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
use pmacs::buffer::BufferId;
use pmacs::editor::EditorState;
use pmacs::editor_core::normalize_buffer_path;
use pmacs::protocol::FrontendId;
use pmacs::window::{FrontendView, Layout, Window, WindowId};
use tempfile::TempDir;

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

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

/// Drive the async runtime to quiescence — no parked coroutine, no
/// pending worker job. The directory listing is invisible until this
/// returns, and how many frames it takes is not knowable in advance.
fn pump(s: &mut EditorState) {
    let deadline = Instant::now() + Duration::from_secs(10);
    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();
    }
}

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

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

/// The 0-based line an entry renders on, found by its trailing name
/// column -- the same shape `dired_acceptance` uses.
fn line_of(s: &EditorState, name: &str) -> usize {
    let text = active_text(s);
    for (index, line) in text.lines().enumerate() {
        if line.trim_end().ends_with(name) {
            return index;
        }
    }
    panic!("no listing line for {name:?} in:\n{text}");
}

/// A project a journey can plausibly be run against.
fn project() -> TempDir {
    let td = tempfile::tempdir().expect("tempdir");
    std::fs::write(td.path().join("alpha.txt"), b"alpha\n").expect("write alpha");
    std::fs::write(td.path().join("beta.txt"), b"beta\n").expect("write beta");
    td
}

fn canon(path: &Path) -> String {
    normalize_buffer_path(path.to_path_buf())
        .to_string_lossy()
        .into_owned()
}

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

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

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

fn buffer_count(s: &EditorState) -> usize {
    s.core.borrow().registry.borrow().ids().len()
}

/// The buffer a window currently shows, or `None` if it is not live.
fn buffer_in(s: &EditorState, window: WindowId) -> Option<BufferId> {
    s.core.borrow().windows.get(&window).map(|w| w.buffer_id)
}

/// The window `LOCAL` currently has selected.
fn local_window(s: &EditorState) -> WindowId {
    s.core
        .borrow()
        .views
        .get(&FrontendId::LOCAL)
        .expect("LOCAL view")
        .active
}

/// Register a second frontend with its own single-window layout,
/// mirroring `build_fresh_frontend_view` (the same helper shape
/// `bottom_panel_stage1_acceptance` uses).
fn attach_frontend(s: &EditorState, fid: FrontendId) -> WindowId {
    let mut core = s.core.borrow_mut();
    let buffer_id = core.active_buffer_id();
    let text_view = {
        let reg = core.registry.borrow();
        pmacs::text_view::TextView::new(reg.get(buffer_id).expect("buffer"))
    };
    let win = WindowId::next();
    core.windows
        .insert(win, Window::new(win, buffer_id, text_view));
    core.register_frontend_view(
        fid,
        FrontendView {
            layout: Layout::single(win),
            active: win,
            fold_projection: true,
            panel_capable: true,
            frame_geometry: None,
            panel_hidden: false,
        },
    );
    win
}

/// Drive the **real** chain far enough to obtain a genuine destination
/// and leave it in the Lua global `dest`.
///
/// The listener claims (returns `false`), so nothing is committed and no
/// fallback runs: what lands in `dest` is exactly the userdata dired
/// would have received, produced by the production capture rather than
/// fabricated. Nothing in the test suite can construct one — that is
/// N6b's whole subject.
fn capture_dest(s: &mut EditorState, dir: &Path) {
    exec(
        s,
        "dest = nil
         pmacs.hook.add('path.open-directory', function(_, d) dest = d return false end)",
    );
    s.open_directory_target(dir);
    pump(s);
    assert!(
        eval::<bool>(s, "return dest ~= nil"),
        "the chain must hand listeners a destination"
    );
}

/// Open through the **real** startup entry point, as `pmacs PATH` does.
fn launch(path: &Path) -> EditorState {
    let mut s = EditorState::open(path.to_path_buf()).expect("startup must not fail");
    exec(&s, "pmacs.lsp.config = {}");
    pump(&mut s);
    s
}

// ---------------------------------------------------------------------------
// Ambient-roots isolation
// ---------------------------------------------------------------------------
//
// This suite is the one place that must NOT take the
// `EditorState::new_with_roots` / `open_with_roots` seam
// (`docs/test-ambient-config-isolation-framing.md` §1.7). Its whole job
// is to prove the production entry point is wired: an `open` arm with no
// production caller passes every direct-call test, so the ratchet drives
// the same `EditorState::open` that `pmacs FILE` does — parameterless,
// ambient, exactly as production calls it.
//
// That leaves nothing in the process able to isolate it. Cargo launches
// each integration-test binary with the caller's environment, and a
// binary cannot re-point its own roots before its tests run:
// `std::env::set_var` is `unsafe` and pmacs is
// `#![forbid(unsafe_code)]`. "Isolated by the environment it is launched
// with" is not a mechanism — it is a hope that whoever typed `cargo
// test` wrapped it, which is the external workaround this lane exists to
// delete.
//
// So each test is a thin **parent** that re-execs this same binary for
// its own test name, with controlled roots in the child's environment.
// The child sees the marker, runs the real body, and calls the ambient
// entry point — production's call, against a controlled tree.

/// Marker the parent sets on the child. Its presence, not its value, is
/// the signal.
const ISOLATED_CHILD: &str = "PMACS_JOURNEY_ISOLATED_CHILD";

/// Re-exec this test binary for `name` under controlled bootstrap
/// storage roots.
///
/// Returns `true` in the **parent** — the child has already run and been
/// asserted, so the caller must return without doing anything itself —
/// and `false` in the **child**, whose job is to run the body.
#[must_use]
fn reexec_isolated(name: &str) -> bool {
    if std::env::var_os(ISOLATED_CHILD).is_some() {
        return false;
    }
    // Under `target/`, not `/tmp`: nothing unlinks this (libtest has no
    // teardown hook), so it belongs somewhere `cargo clean` owns.
    let base = Path::new(env!("CARGO_TARGET_TMPDIR"))
        .join("journey-isolation")
        .join(name);
    let _ = std::fs::remove_dir_all(&base);
    let roots = pmacs::bootstrap::BootstrapRoots::isolated_under(&base);
    let child_env = roots.child_env();
    for (_, dir) in &child_env {
        std::fs::create_dir_all(dir).expect("create controlled root");
    }
    let exe = std::env::current_exe().expect("current test binary");
    let output = std::process::Command::new(exe)
        .args(["--exact", name, "--nocapture", "--test-threads=1"])
        .env(ISOLATED_CHILD, "1")
        .envs(child_env)
        .output()
        .unwrap_or_else(|e| panic!("re-exec `{name}`: {e}"));
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        output.status.success(),
        "isolated child for `{name}` failed ({})\n--- child stdout ---\n{stdout}\
         \n--- child stderr ---\n{stderr}",
        output.status
    );
    // A filter that matches nothing exits 0. Without this the parent
    // would pass while running no test at all — the failure mode a
    // renamed test produces, and the one that would quietly hollow out
    // the whole ratchet.
    assert!(
        stdout.contains("1 passed"),
        "isolated child for `{name}` ran no test — the `--exact` filter went \
         stale against the function name\n--- child stdout ---\n{stdout}"
    );
    true
}

// ---------------------------------------------------------------------------
// Step 2 — launch unconfigured
// ---------------------------------------------------------------------------

/// **N** — the editor starts with no configuration and no arguments.
#[test]
fn journey_step2_launches_unconfigured_into_scratch() {
    if reexec_isolated("journey_step2_launches_unconfigured_into_scratch") {
        return;
    }
    let s = EditorState::new();
    assert_eq!(active_name(&s), "*scratch*");
    assert!(
        status(&s).is_empty(),
        "a clean launch reports no error; got {:?}",
        status(&s)
    );
}

// ---------------------------------------------------------------------------
// Step 3 — open a real project
// ---------------------------------------------------------------------------

/// **N1** — `pmacs .` opens the directory.
///
/// The headline of Stage 1a and of `COHERENCE.md` §2's "broken at step
/// 3" grade. Before the directory arm this construction returned
/// `Err(EISDIR)` and `main` exited 1.
#[test]
fn journey_step3_opening_a_directory_lists_it() {
    if reexec_isolated("journey_step3_opening_a_directory_lists_it") {
        return;
    }
    let td = project();
    let s = launch(td.path());

    let name = active_name(&s);
    assert_eq!(
        name,
        format!("*dired:{}*", canon(td.path())),
        "the active buffer must be the directory's dired buffer"
    );
    let text = active_text(&s);
    assert!(
        text.contains("alpha.txt") && text.contains("beta.txt"),
        "the listing must show the directory's entries; got {text:?}"
    );
}

/// **N1b** — and it is a *successful* startup, not a rescued failure.
///
/// Guards the specific regression shape: an implementation that opened
/// dired but still left an error on the status line would look right in
/// the assertion above while `pmacs .` still printed a diagnostic.
#[test]
fn journey_step3_directory_startup_reports_no_error() {
    if reexec_isolated("journey_step3_directory_startup_reports_no_error") {
        return;
    }
    let td = project();
    let s = launch(td.path());
    assert!(
        !status(&s).contains("cannot open"),
        "a successful directory open must not leave an error status; got {:?}",
        status(&s)
    );
}

/// **N3** — an unreadable directory reports and leaves the session
/// running, rather than failing startup.
#[cfg(target_os = "linux")]
#[test]
fn journey_step3_unreadable_directory_reports_without_failing_startup() {
    use std::os::unix::fs::PermissionsExt;
    if reexec_isolated("journey_step3_unreadable_directory_reports_without_failing_startup") {
        return;
    }
    let td = tempfile::tempdir().expect("tempdir");
    let locked = td.path().join("locked");
    std::fs::create_dir(&locked).expect("mkdir");
    std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).expect("chmod");

    // Startup itself must succeed: the failure is the *listing*, which
    // happens a tick later and belongs on the status line.
    let s = launch(&locked);
    assert!(
        !status(&s).is_empty(),
        "a failed listing must report through the status line"
    );
    assert!(
        !active_name(&s).starts_with("*dired:"),
        "a failed listing must leave no dired buffer behind"
    );

    std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o700)).expect("restore");
}

/// **N9** — the resolver receives a canonical absolute path.
///
/// Falsified by dropping the normalization in
/// `ResolvedTarget::Directory`: nothing else normalizes on that arm,
/// because no buffer is created and `set_buffer_path` never runs.
#[test]
fn journey_directory_resolver_receives_a_canonical_path() {
    if reexec_isolated("journey_directory_resolver_receives_a_canonical_path") {
        return;
    }
    let td = project();
    let mut s = EditorState::new();
    exec(&s, "pmacs.lsp.config = {}");
    exec(
        &s,
        "seen = nil
         pmacs.hook.add('path.open-directory', function(path) seen = path return false end)",
    );

    // A path with a redundant component, which only canonicalization removes.
    let noisy = td.path().join("subdir").join("..");
    std::fs::create_dir_all(td.path().join("subdir")).expect("mkdir");
    s.open_directory_target(&noisy);
    pump(&mut s);

    let seen: String = eval(&s, "return seen");
    assert_eq!(
        seen,
        canon(td.path()),
        "the resolver must receive the canonical path, not the literal argument"
    );
}

/// **N10** — with the handler cleared and nothing claiming, a directory
/// argument still starts successfully.
///
/// The regression path back to exit 1. Reachable only because the
/// fallback is a clearable slot rather than a builtin hook subscription.
#[test]
fn journey_unclaimed_directory_starts_successfully_with_a_status() {
    if reexec_isolated("journey_unclaimed_directory_starts_successfully_with_a_status") {
        return;
    }
    let td = project();
    let mut s = EditorState::new();
    exec(&s, "pmacs.lsp.config = {}");
    exec(&s, "pmacs.path.set_directory_handler(nil)");

    let before = active_name(&s);
    s.open_directory_target(td.path());
    pump(&mut s);

    assert_eq!(
        active_name(&s),
        before,
        "with no handler the window keeps the buffer it had"
    );
    assert!(
        status(&s).contains(&canon(td.path())),
        "the status must name the directory nothing surfaced; got {:?}",
        status(&s)
    );
}

// ---------------------------------------------------------------------------
// The resolver chain
// ---------------------------------------------------------------------------

/// **N7** — first claimant wins, through an ordinary user listener, and
/// a claim suppresses the fallback.
#[test]
fn journey_resolver_chain_is_first_claimant_wins() {
    if reexec_isolated("journey_resolver_chain_is_first_claimant_wins") {
        return;
    }
    let td = project();
    let mut s = EditorState::new();
    exec(&s, "pmacs.lsp.config = {}");
    exec(
        &s,
        "first, second, fallback_ran = false, false, false
         pmacs.path.set_directory_handler(function() fallback_ran = true end)
         pmacs.hook.add('path.open-directory', function() first = true return false end)
         pmacs.hook.add('path.open-directory', function() second = true return false end)",
    );

    s.open_directory_target(td.path());
    pump(&mut s);

    assert!(eval::<bool>(&s, "return first"), "the first listener runs");
    assert!(
        !eval::<bool>(&s, "return second"),
        "a claim stops the fan-out before the second listener"
    );
    assert!(
        !eval::<bool>(&s, "return fallback_ran"),
        "a claim suppresses the fallback"
    );
}

/// **N8** — a raising listener suppresses the fallback *and* is
/// reported.
///
/// Falsified by running the fallback when `errors` is non-empty (i.e.
/// treating a raise as a decline), or by making a raise yield
/// `proceed = true`. NOT falsified by keying suppression on `proceed`
/// alone — that is already correct, since a raise and a claim both give
/// `proceed == false`.
#[test]
fn journey_a_raising_resolver_suppresses_the_fallback_and_reports() {
    if reexec_isolated("journey_a_raising_resolver_suppresses_the_fallback_and_reports") {
        return;
    }
    let td = project();
    let mut s = EditorState::new();
    exec(&s, "pmacs.lsp.config = {}");
    exec(
        &s,
        "fallback_ran = false
         pmacs.path.set_directory_handler(function() fallback_ran = true end)
         pmacs.hook.add('path.open-directory', function() error('resolver exploded') end)",
    );

    s.open_directory_target(td.path());
    pump(&mut s);

    assert!(
        !eval::<bool>(&s, "return fallback_ran"),
        "a crashed resolver must not fall through to the default surface"
    );
    assert!(
        !status(&s).is_empty(),
        "the failure must reach the status line, not only *errors*"
    );
    let errors: String = eval(
        &s,
        "for _, id in ipairs(pmacs.buffer.list()) do
           local ok, d = pcall(pmacs.describe.buffer, id)
           if ok and d and d.name == '*errors*' then
             return id:slice(0, id:len())
           end
         end
         return ''",
    );
    assert!(
        errors.contains("resolver exploded"),
        "the failure must also reach the *errors* buffer; got {errors:?}"
    );
}

// ---------------------------------------------------------------------------
// The destination commit (`pmacs.window.commit_to`)
// ---------------------------------------------------------------------------
//
// The substrate half of Stage 1a. A directory listing settles a tick or
// more after the request, by which time the ambient frontend, selected
// window, and active buffer may all name something else — so the whole
// post-await commit runs against a destination captured at request time.
//
// `LOCAL` is the requesting frontend throughout, because
// `open_directory_target` is the local-startup seam; the daemon's
// non-`LOCAL` capture is pinned in `src/daemon.rs`, where the production
// caller lives. What varies here is what the *ambient* frontend is doing
// while the commit runs, which is exactly the misrouting the scope
// exists to prevent.

/// The frontend that competes for ambient authority in these tests.
const COMPETITOR: FrontendId = FrontendId(7);

/// **N4** — the commit lands in the *requesting* frontend's window even
/// though another frontend is the one dispatching.
///
/// The blocker's positive half. Falsified by reverting `commit_to` to an
/// ambient display: the file then appears in the competitor's window.
#[test]
fn commit_to_delivers_to_the_requesting_frontend_not_the_ambient_one() {
    if reexec_isolated("commit_to_delivers_to_the_requesting_frontend_not_the_ambient_one") {
        return;
    }
    let td = project();
    let mut s = EditorState::new();
    exec(&s, "pmacs.lsp.config = {}");
    capture_dest(&mut s, td.path());

    let local_win = local_window(&s);
    let other_win = attach_frontend(&s, COMPETITOR);
    let other_before = buffer_in(&s, other_win);

    // The competitor becomes the dispatching frontend while the work is
    // "in flight" — the state a worker completion actually returns to.
    s.core.borrow_mut().active_frontend = COMPETITOR;

    let alpha = td.path().join("alpha.txt").display().to_string();
    exec(
        &s,
        &format!(
            "assert(pmacs.window.commit_to(dest, function()
               pmacs.window.display_file({alpha:?})
             end))"
        ),
    );

    assert_eq!(
        buffer_in(&s, other_win),
        other_before,
        "the competing frontend's window must be untouched"
    );
    s.core.borrow_mut().active_frontend = FrontendId::LOCAL;
    assert_eq!(
        active_name(&s),
        alpha,
        "the commit must land in the requesting frontend's captured window"
    );
    assert_eq!(
        local_window(&s),
        local_win,
        "and in that window, not a new one"
    );
}

/// **N4b** — the scope beats an *interactive origin*, not merely the
/// ambient frontend.
///
/// Found by bite-testing N4: with the `ScopedFrontend` arm deleted from
/// `acting_frontend`, N4 still passed, because `ScopedFrontend::enter`
/// also swaps `core.active_frontend` and the ambient fallback then
/// answers correctly on its own. The arm is load-bearing in exactly one
/// situation — a commit reached from inside an interactive command,
/// where the origin sits *between* the override and the ambient value
/// and would otherwise win. `acting_frontend`'s comment claims that
/// ordering; nothing pinned it.
///
/// Driven through `dispatch_key`, because the interactive origin is
/// established by dispatch and by nothing else — `invoke_interactive`
/// requires a context rather than creating one.
///
/// Falsified by deleting the `ScopedFrontend` arm from
/// `acting_frontend`, or by reordering it after the interactive origin.
#[test]
fn commit_to_outranks_an_interactive_origin() {
    if reexec_isolated("commit_to_outranks_an_interactive_origin") {
        return;
    }
    let td = project();
    let mut s = EditorState::new();
    exec(&s, "pmacs.lsp.config = {}");
    capture_dest(&mut s, td.path());

    let local_win = local_window(&s);
    let other_win = attach_frontend(&s, COMPETITOR);
    let other_before = buffer_in(&s, other_win);

    let alpha = td.path().join("alpha.txt").display().to_string();
    exec(
        &s,
        &format!(
            "pmacs.command.define {{
               name = 'test.journey-commit',
               description = 'commit to a captured destination from inside a command',
               fn = function()
                 committed = pmacs.window.commit_to(dest, function()
                   pmacs.window.display_file({alpha:?})
                 end)
               end,
             }}
             pmacs.keymap.bind {{ scope = 'global', sequence = 'C-c j',
                                  command = 'test.journey-commit' }}"
        ),
    );

    // The COMPETITOR runs the command, so ITS id is the interactive
    // origin for the whole invocation.
    s.dispatch_key(COMPETITOR, key(KeyCode::Char('c'), KeyModifiers::CONTROL));
    s.dispatch_key(COMPETITOR, key(KeyCode::Char('j'), KeyModifiers::NONE));

    assert!(
        eval::<bool>(&s, "return committed"),
        "the commit must be accepted"
    );
    assert_eq!(
        buffer_in(&s, other_win),
        other_before,
        "the invoking frontend's own window must be untouched"
    );
    s.core.borrow_mut().active_frontend = FrontendId::LOCAL;
    assert_eq!(
        active_name(&s),
        alpha,
        "the commit must land in the captured destination, not the \
         interactive origin's window"
    );
    assert_eq!(local_window(&s), local_win);
}

/// **N4c** — the commit lands in the *captured window*, not merely in
/// the captured frontend's currently selected one.
///
/// Review finding on PR #182. Every other routing pin here varies
/// frontend identity; none varied the selected window *within* one
/// frontend, and dired's commit still ended in `switch_buffer`, which
/// targets whatever window the scoped frontend has active. The preflight
/// cannot catch this — the captured window is still live and still holds
/// its captured buffer — so a split that took focus while `read_dir` was
/// pending got the listing, and `prev` was captured from it too.
///
/// Both halves are asserted: where the listing lands, and where `q`
/// goes. Falsified by restoring `pmacs.window.switch_buffer` in dired's
/// `display`, or by reading `prev` from the ambient window.
#[test]
fn a_background_open_uses_the_captured_window_not_the_selected_one() {
    if reexec_isolated("a_background_open_uses_the_captured_window_not_the_selected_one") {
        return;
    }
    let td = project();
    let mut s = EditorState::new();
    exec(&s, "pmacs.lsp.config = {}");
    capture_dest(&mut s, td.path());

    let target = local_window(&s);
    let origin = buffer_in(&s, target).expect("the captured window's buffer");

    // Split, move focus to the OTHER window, and give it a buffer of its
    // own. The captured window is untouched, so every preflight check
    // still passes -- which is exactly why this needs its own pin.
    exec(
        &s,
        "local captured = dest:window()
         pmacs.window.split_horizontal()
         while pmacs.window.current() == captured do pmacs.window.focus_next() end
         pmacs.window.switch_buffer(pmacs.buffer.create('*elsewhere*'))",
    );
    let elsewhere = local_window(&s);
    assert_ne!(elsewhere, target, "focus must have moved to another window");
    let elsewhere_buffer = buffer_in(&s, elsewhere);

    // dired's real handler path, with the captured destination.
    exec(
        &s,
        &format!(
            "pmacs.async(function()
               pmacs.dired.open({:?}, {{ dest = dest }})
             end)",
            canon(td.path())
        ),
    );
    pump(&mut s);

    assert_eq!(
        buffer_in(&s, elsewhere),
        elsewhere_buffer,
        "the window that took focus mid-listing must be untouched"
    );
    assert_eq!(
        local_window(&s),
        target,
        "the commit must select the captured window"
    );
    assert!(
        active_name(&s).starts_with("*dired:"),
        "and the listing must be in it; got {:?}",
        active_name(&s)
    );

    // `prev` came from the captured window too, not from `*elsewhere*`.
    type_char(&mut s, 'q');
    assert_eq!(
        buffer_in(&s, target),
        Some(origin),
        "`q` must return to the buffer the CAPTURED window showed"
    );
}

/// **N6a** — the scope is restored when the callback returns normally.
///
/// Falsified by dropping the guard's restore, or by never swapping
/// `core.active_frontend` in the first place (then `inside` reads the
/// competitor and the assertion fails from the other direction).
#[test]
fn commit_to_scopes_and_restores_on_a_normal_return() {
    if reexec_isolated("commit_to_scopes_and_restores_on_a_normal_return") {
        return;
    }
    let td = project();
    let mut s = EditorState::new();
    exec(&s, "pmacs.lsp.config = {}");
    capture_dest(&mut s, td.path());
    attach_frontend(&s, COMPETITOR);
    s.core.borrow_mut().active_frontend = COMPETITOR;

    exec(
        &s,
        "inside, scoped = nil, nil
         assert(pmacs.window.commit_to(dest, function()
           inside = pmacs.frontend.id()
           scoped = pmacs._async._in_commit_scope()
         end))",
    );

    assert_eq!(
        eval::<i64>(&s, "return inside"),
        i64::try_from(FrontendId::LOCAL.0).expect("frontend id"),
        "inside the commit the acting frontend is the requesting one"
    );
    assert!(
        eval::<bool>(&s, "return scoped"),
        "and the commit-scope flag is set while the callback runs"
    );
    assert_eq!(
        s.core.borrow().active_frontend,
        COMPETITOR,
        "the ambient frontend must be restored on return"
    );
    assert!(
        !eval::<bool>(&s, "return pmacs._async._in_commit_scope()"),
        "and the commit-scope flag cleared"
    );
    assert_eq!(
        eval::<i64>(&s, "return pmacs.frontend.id()"),
        i64::try_from(COMPETITOR.0).expect("frontend id"),
        "the Lua-visible frontend must be restored too"
    );
}

/// **N6b (part of N6)** — a raising callback still restores.
///
/// The path that makes the guard RAII rather than a pair of statements:
/// `commit_to` captures the call's result and lets the guard drop before
/// propagating it. Falsified by `?`-propagating the callback's error
/// through the scope, or by restoring on the success path only.
#[test]
fn commit_to_restores_when_the_callback_raises() {
    if reexec_isolated("commit_to_restores_when_the_callback_raises") {
        return;
    }
    let td = project();
    let mut s = EditorState::new();
    exec(&s, "pmacs.lsp.config = {}");
    capture_dest(&mut s, td.path());
    attach_frontend(&s, COMPETITOR);
    s.core.borrow_mut().active_frontend = COMPETITOR;

    exec(
        &s,
        "local ok, err = pcall(pmacs.window.commit_to, dest, function()
           error('commit exploded')
         end)
         raised = (not ok) and tostring(err) or '<no raise>'",
    );

    assert!(
        eval::<String>(&s, "return raised").contains("commit exploded"),
        "the callback's error must propagate"
    );
    assert_eq!(
        s.core.borrow().active_frontend,
        COMPETITOR,
        "a raising callback must still restore the ambient frontend"
    );
    assert!(
        !eval::<bool>(&s, "return pmacs._async._in_commit_scope()"),
        "and must still clear the commit-scope flag"
    );
}

/// **N6c (part of N6)** — awaiting inside a commit is refused, the
/// refusal names the rule, and the scope is restored anyway.
///
/// A yield would restore the scope while the coroutine is still parked,
/// so the rest of the commit would resume ambient — silently
/// reintroducing exactly the misrouting N4 pins against. Driven inside
/// `pmacs.async`, which is where a real await lives.
///
/// Falsified by dropping the `_in_commit_scope` check from
/// `Handle:await`: the await then succeeds and `refusal` reads
/// `<no raise>`.
#[test]
fn commit_to_refuses_an_await_and_restores() {
    if reexec_isolated("commit_to_refuses_an_await_and_restores") {
        return;
    }
    let td = project();
    let mut s = EditorState::new();
    exec(&s, "pmacs.lsp.config = {}");
    capture_dest(&mut s, td.path());
    attach_frontend(&s, COMPETITOR);
    s.core.borrow_mut().active_frontend = COMPETITOR;

    exec(
        &s,
        &format!(
            "refusal = nil
             pmacs.async(function()
               local handle = pmacs.fs.read_dir({:?})
               local ok, err = pcall(pmacs.window.commit_to, dest, function()
                 return handle:await()
               end)
               refusal = (not ok) and tostring(err) or '<no raise>'
               -- Drain it OUTSIDE the commit, which is where the refusal
               -- says the await belongs -- and which also settles the job
               -- so the pump can reach quiescence.
               handle:await()
             end)",
            td.path().display().to_string()
        ),
    );
    pump(&mut s);

    let refusal: String = eval(&s, "return refusal");
    assert!(
        refusal.contains("cannot await inside") && refusal.contains("commit_to"),
        "the refusal must name the rule it enforces; got {refusal:?}"
    );
    assert_eq!(
        s.core.borrow().active_frontend,
        COMPETITOR,
        "a refused await must still restore the ambient frontend"
    );
    assert!(
        !eval::<bool>(&s, "return pmacs._async._in_commit_scope()"),
        "and must still clear the commit-scope flag"
    );
}

/// **N6b** — a forged destination is rejected, and the callback never
/// runs.
///
/// A plausible `{frontend, window, buffer}` table is what any Lua could
/// fabricate. Falsified by accepting a table, or by borrowing the
/// userdata after invoking the callback.
#[test]
fn commit_to_refuses_a_forged_destination() {
    if reexec_isolated("commit_to_refuses_a_forged_destination") {
        return;
    }
    let td = project();
    let mut s = EditorState::new();
    exec(&s, "pmacs.lsp.config = {}");
    capture_dest(&mut s, td.path());

    let win = eval::<i64>(&s, "return dest:window()");
    exec(
        &s,
        &format!(
            "ran = false
             local ok, err = pcall(pmacs.window.commit_to,
               {{ frontend = 0, window = {win}, buffer = 0 }},
               function() ran = true end)
             rejected = (not ok) and tostring(err) or '<accepted>'"
        ),
    );

    let rejected: String = eval(&s, "return rejected");
    assert!(
        rejected.contains("cannot be constructed from Lua"),
        "a forged table must be rejected by type, not merely fail later; got {rejected:?}"
    );
    assert!(
        !eval::<bool>(&s, "return ran"),
        "a rejected destination must not reach the callback"
    );
}

/// **N6c** — a declining listener cannot redirect the destination.
///
/// The same userdata is handed to every listener in turn. As a table, an
/// earlier listener could rewrite the window and then decline, sending
/// the fallback somewhere the user never asked for. Falsified by passing
/// a shared mutable table.
#[test]
fn a_declining_listener_cannot_redirect_the_destination() {
    if reexec_isolated("a_declining_listener_cannot_redirect_the_destination") {
        return;
    }
    let td = project();
    let mut s = EditorState::new();
    exec(&s, "pmacs.lsp.config = {}");
    let target = local_window(&s);

    exec(
        &s,
        "seen_first, seen_second, mutation = nil, nil, nil
         pmacs.hook.add('path.open-directory', function(_, d)
           seen_first = d:window()
           -- Try to redirect, then decline. Both halves matter: a
           -- successful mutation with a decline is the attack.
           local ok, err = pcall(function() d.window = 999 end)
           mutation = (not ok) and tostring(err) or '<mutated>'
         end)
         pmacs.hook.add('path.open-directory', function(_, d)
           seen_second = d:window()
         end)",
    );

    s.open_directory_target(td.path());
    pump(&mut s);

    let mutation: String = eval(&s, "return mutation");
    assert!(
        !mutation.contains("<mutated>"),
        "the destination must be read-only; got {mutation:?}"
    );
    let first = eval::<i64>(&s, "return seen_first");
    let second = eval::<i64>(&s, "return seen_second");
    assert_eq!(
        first, second,
        "every listener must see the same, unaltered destination"
    );
    assert_eq!(
        u64::try_from(second).expect("window id"),
        target.raw(),
        "and it must still name the window the editor captured"
    );
    // And the fallback commits THERE, not to whatever the first listener
    // wanted -- the observable the attack was aiming at.
    assert!(
        active_name(&s).starts_with("*dired:"),
        "the declined chain must still fall back to dired"
    );
    assert_eq!(
        buffer_in(&s, target),
        Some(eval::<pmacs::lua_bindings::BufferIdLua>(&s, "return pmacs.window.buffer()").0),
        "in the captured window"
    );
}

// --- the commit's preservation pins ---------------------------------------

/// **P1** — every destination precondition is checked *before* the
/// callback runs, so a failure mutates nothing.
///
/// Four refusals, each asserted the same way: `commit_to` returns
/// `(false, reason)`, the callback never ran, and no buffer was created.
/// Table-driven deliberately — the failure message names which
/// precondition regressed, which four separate near-identical tests
/// would give up in exchange for nothing.
///
/// *Mutation:* move the preflight from before the callback to after it
/// (rev 2's design, which validated at display time). All four fail.
/// *Second mutation, for the dedicated case:* pass `Some(dest.buffer)`
/// instead of `None` to `window_accepts_buffer`. Only that case fails —
/// which is why it is listed separately from the stale-buffer case it
/// otherwise resembles.
///
/// **Also fails on full revert**, since `commit_to` does not exist on the
/// pre-image. It is listed as a P because the discriminating falsifier is
/// the named mutation, not the revert: a revert-only check would not
/// distinguish "validates" from "validates in time".
#[test]
fn preservation_a_failed_precondition_never_reaches_the_callback() {
    if reexec_isolated("preservation_a_failed_precondition_never_reaches_the_callback") {
        return;
    }
    // (label, Lua that breaks the precondition, expected reason fragment)
    let cases: [(&str, &str, &str); 4] = [
        (
            "frontend gone",
            // Handled in Rust below: unregistering a view has no Lua surface.
            "",
            "requesting frontend is gone",
        ),
        (
            "window gone",
            "local doomed = dest:window()
             pmacs.window.split_horizontal()
             while pmacs.window.current() == doomed do pmacs.window.focus_next() end
             pmacs.window.close_others()",
            "is gone",
        ),
        (
            "stale buffer",
            "pmacs.window.switch_buffer(pmacs.buffer.create('*usurper*'))",
            "now shows another buffer",
        ),
        (
            "dedicated",
            "pmacs.window.set_params(dest:window(), { dedicated = true })",
            "is dedicated",
        ),
    ];

    for (label, break_it, expected) in cases {
        let td = project();
        let mut s = EditorState::new();
        exec(&s, "pmacs.lsp.config = {}");
        capture_dest(&mut s, td.path());

        if label == "frontend gone" {
            s.core
                .borrow_mut()
                .unregister_frontend_view(FrontendId::LOCAL);
        } else {
            exec(&s, break_it);
        }
        let before = buffer_count(&s);

        exec(
            &s,
            "ran = false
             ok, reason = pmacs.window.commit_to(dest, function() ran = true end)",
        );

        assert!(
            !eval::<bool>(&s, "return ok"),
            "{label}: commit_to must refuse"
        );
        let reason: String = eval(&s, "return tostring(reason)");
        assert!(
            reason.contains(expected),
            "{label}: reason must say why; wanted {expected:?}, got {reason:?}"
        );
        assert!(
            !eval::<bool>(&s, "return ran"),
            "{label}: the callback must not run at all -- validating after it \
             is four mutations too late"
        );
        assert_eq!(
            buffer_count(&s),
            before,
            "{label}: a refused commit must create no buffer"
        );
    }
}

/// **P2 — stale intent loses**, through dired's real commit path.
///
/// The user replaced the destination window's buffer while the listing
/// was in flight. Their action is newer information than the request, so
/// the request loses: dired refuses, their buffer survives, and no dired
/// buffer or handle is left behind for that path.
///
/// P1 pins the preflight in isolation; this drives `pmacs.dired.open`
/// with a captured destination — the same call the handler makes — so
/// the atomicity claim is asserted where the four mutations actually
/// live.
///
/// *Mutation:* drop the `dest.buffer` comparison from the preflight
/// (window-only validation). The dired buffer then replaces the user's.
#[test]
fn preservation_a_stale_destination_loses_to_the_users_newer_buffer() {
    if reexec_isolated("preservation_a_stale_destination_loses_to_the_users_newer_buffer") {
        return;
    }
    let td = project();
    let mut s = EditorState::new();
    exec(&s, "pmacs.lsp.config = {}");
    capture_dest(&mut s, td.path());
    let target = local_window(&s);

    // The user switches the destination window while the work is in flight.
    exec(
        &s,
        "usurper = pmacs.buffer.create('*usurper*')
         pmacs.window.switch_buffer(usurper)",
    );
    let usurper = buffer_in(&s, target);
    let before = buffer_count(&s);

    exec(
        &s,
        &format!(
            "failure = nil
             pmacs.async(function()
               local ok, err = pcall(pmacs.dired.open, {:?}, {{ dest = dest }})
               failure = (not ok) and tostring(err) or '<committed>'
             end)",
            canon(td.path())
        ),
    );
    pump(&mut s);

    let failure: String = eval(&s, "return failure");
    assert!(
        failure.contains("destination is gone"),
        "dired must report the refusal rather than commit; got {failure:?}"
    );
    assert_eq!(
        buffer_in(&s, target),
        usurper,
        "the user's newer buffer must survive"
    );
    assert_eq!(
        buffer_count(&s),
        before,
        "and no dired buffer may be left behind"
    );
    assert_eq!(
        active_name(&s),
        "*usurper*",
        "nor may the refusal change what is displayed"
    );
}

/// **P3** — dired reads its `prev` inside the scope, so `q` returns to
/// the *destination* window's buffer, not the ambient frontend's.
///
/// `handle.prev` is captured with `pmacs.window.buffer()`, whose no-arg
/// arm reads the core's ambient `active_buffer_id()`. That is precisely
/// why the scope swaps `core.active_frontend` and not only the override:
/// a scope that swapped the override alone would leave this one line
/// reading the competitor's buffer, and `q` would drop the user into a
/// buffer from another frontend's window.
///
/// Asserted through `q` rather than by reaching into dired's handle
/// table — `prev`'s entire meaning is where `q` lands.
///
/// *Mutation:* stop swapping `core.active_frontend` in
/// `ScopedFrontend::enter` (keep the override). `q` then lands in
/// `*competitor*`.
#[test]
fn preservation_dired_captures_prev_from_the_destination_not_the_ambient_frontend() {
    if reexec_isolated(
        "preservation_dired_captures_prev_from_the_destination_not_the_ambient_frontend",
    ) {
        return;
    }
    let td = project();
    let mut s = EditorState::new();
    exec(&s, "pmacs.lsp.config = {}");

    let target = local_window(&s);
    let origin = buffer_in(&s, target).expect("the startup buffer");

    // A competitor whose window shows a buffer of its own, ambient while
    // the listing settles.
    let other_win = attach_frontend(&s, COMPETITOR);
    let competitor_buffer =
        eval::<pmacs::lua_bindings::BufferIdLua>(&s, "return pmacs.buffer.create('*competitor*')")
            .0;
    s.core
        .borrow_mut()
        .install_buffer_in_window(other_win, competitor_buffer)
        .expect("install");
    s.core.borrow_mut().active_frontend = COMPETITOR;

    s.open_directory_target(td.path());
    pump(&mut s);
    s.core.borrow_mut().active_frontend = FrontendId::LOCAL;
    assert!(
        active_name(&s).starts_with("*dired:"),
        "the listing must have committed"
    );

    type_char(&mut s, 'q');
    assert_eq!(
        buffer_in(&s, target),
        Some(origin),
        "`q` must return to the buffer the DESTINATION window showed, not \
         the ambient frontend's"
    );
}

// ---------------------------------------------------------------------------
// Step 5 — edit immediately
// ---------------------------------------------------------------------------

/// **N11** — the journey's step-3-into-step-5 path, through the real
/// input path at every step: start on a directory, press `RET` on a
/// listed file, then type a character into it.
///
/// Rev 6 correction: this previously called `display_file` and
/// `buf:insert` directly, so it stayed green with dired's `RET` binding,
/// its entry dispatch, or the editor's self-insert path all broken —
/// which is most of what "the journey works" is supposed to mean. Both
/// gestures are now dispatched as keys.
///
/// Deliberately not a self-insert into the dired buffer, whose intercept
/// rejects every edit: asserting an edit lands there would contradict
/// the read-only contract rather than pin the journey.
#[test]
fn journey_step5_editing_a_file_reached_through_the_directory() {
    if reexec_isolated("journey_step5_editing_a_file_reached_through_the_directory") {
        return;
    }
    let td = project();
    let mut s = launch(td.path());
    assert!(active_name(&s).starts_with("*dired:"));

    // Seat on the entry, then VISIT it with the real key.
    let line = line_of(&s, "alpha.txt");
    exec(&s, &format!("pmacs.editor.move_to_line({line})"));
    press(&mut s, KeyCode::Enter);
    pump(&mut s);

    assert_eq!(
        active_name(&s),
        td.path().join("alpha.txt").display().to_string(),
        "RET on a listed file must visit it"
    );

    // And type into it with the real key.
    type_char(&mut s, 'X');
    let text = active_text(&s);
    assert!(
        text.starts_with('X'),
        "a self-insert must land in the visited file's buffer; got {text:?}"
    );
    assert!(
        buffer_count(&s) >= 2,
        "the dired buffer and the visited file both exist"
    );
}

// ---------------------------------------------------------------------------
// Preservation pins (P) — green on the pre-image; see the named mutation
// ---------------------------------------------------------------------------

/// **P4** — startup shows the file in the *active* window.
///
/// *Mutation:* replace `replace_active_buffer` with a bare
/// `install_buffer_in_window` into some other window in
/// `EditorState::open`.
///
/// **Note, found during implementation:** this does NOT assert that the
/// initial scratch buffer is destroyed, because it is not.
/// `replace_active_buffer`'s doc comment claims it drops "any old
/// scratch buffer if the active window's previous buffer has no other
/// windows referencing it", but all it does is call
/// `switch_active_buffer`, which reassigns the window's `buffer_id` and
/// never removes anything. The stale scratch survives in the registry
/// today, on `main`, unrelated to this stage — so asserting otherwise
/// would have pinned a guarantee the editor does not make and failed on
/// the pre-image for the wrong reason. What the unification must
/// preserve is which window shows the file, and that is what this pins.
#[test]
fn preservation_opening_a_file_shows_it_in_the_active_window() {
    if reexec_isolated("preservation_opening_a_file_shows_it_in_the_active_window") {
        return;
    }
    let td = project();
    let target = td.path().join("alpha.txt");
    let s = EditorState::open(target.clone()).expect("open");

    // The displayed name is the argument as given (`path.display()`),
    // which both implementations have always produced -- the *stored*
    // path is what gets normalized, inside `set_buffer_path`.
    assert_eq!(
        active_name(&s),
        target.display().to_string(),
        "the file must be in the active window, not merely loaded"
    );
    let scratch_displayed: bool = eval(
        &s,
        "for _, id in ipairs(pmacs.buffer.list()) do
           local ok, d = pcall(pmacs.describe.buffer, id)
           if ok and d and d.name == '*scratch*' and pmacs.window.buffer() == id then
             return true
           end
         end
         return false",
    );
    assert!(
        !scratch_displayed,
        "no window may still be showing the startup scratch buffer"
    );
}

/// **P5** — the `NotFound` arm survives the unification.
///
/// *Mutation:* delete the `NotFound` arm from `resolve_target_buffer`.
/// The arm most likely to be lost in a wholesale refactor, because its
/// failure mode is a hard error on a perfectly ordinary gesture.
#[test]
fn preservation_a_missing_path_becomes_a_new_file_buffer() {
    if reexec_isolated("preservation_a_missing_path_becomes_a_new_file_buffer") {
        return;
    }
    let td = project();
    let fresh = td.path().join("not-yet.txt");
    let s = EditorState::open(fresh.clone()).expect("a missing path is not an error");

    assert_eq!(status(&s), "[new file]");
    let len: usize = eval(&s, "return pmacs.window.buffer():len()");
    assert_eq!(len, 0, "a new-file buffer starts empty");
    assert!(!fresh.exists(), "nothing is written until save");
}

/// **P8** — a startup failure names the file.
///
/// The message gained a `cannot open {path}: ` prefix in Stage 1a; the
/// *failure* is preserved, only its wording improved. Before, the bare
/// `io::Error` never named the path.
#[cfg(target_os = "linux")]
#[test]
fn preservation_an_unreadable_file_reports_with_its_path() {
    use std::os::unix::fs::PermissionsExt;
    if reexec_isolated("preservation_an_unreadable_file_reports_with_its_path") {
        return;
    }
    let td = project();
    let locked = td.path().join("locked.txt");
    std::fs::write(&locked, b"secret\n").expect("write");
    std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).expect("chmod");

    let rendered = match EditorState::open(locked.clone()) {
        Ok(_) => panic!("an unreadable file must fail"),
        Err(error) => error.to_string(),
    };

    std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o600)).expect("restore");

    assert!(
        rendered.contains("cannot open"),
        "the message must say what failed; got {rendered:?}"
    );
    assert!(
        rendered.contains(&locked.display().to_string()),
        "the message must name the file; got {rendered:?}"
    );
}

// **P7 — removed in rev 6, not weakened.**
//
// Q#JR12 said a directory argument must suppress desktop restore, and
// rev 5 carried a pin for it. There is nothing to pin. `run` computes
// `had_file = file.is_some()` (`editor.rs:3152`) and a directory path is
// `Some` like any other, so the suppression is structural: no
// directory-specific branch exists that could get it wrong, and the
// named mutation ("pass false for `had_file` on the directory path")
// would require inventing the branch first.
//
// The rev 5 test also never armed desktop restore and hard-coded
// `had_file = true` after startup, so it asserted nothing about `run`'s
// decision and would have passed against any implementation. Keeping a
// green test that cannot fail is worse than having none: it reads as
// coverage. Q#JR12 is downgraded to an observation in the framing.

/// **P6** — `display_file` keeps its directory-is-an-error contract and
/// does not enter the resolver chain.
///
/// *Mutation:* route `display_file` into the directory resolver.
/// `find_file_accepting_a_directory_reports_instead_of_raising` in
/// `find_file_acceptance.rs` is the companion pin through find-file's
/// real accept path; this one pins the primitive and the window state.
#[test]
fn preservation_display_file_still_refuses_a_directory() {
    if reexec_isolated("preservation_display_file_still_refuses_a_directory") {
        return;
    }
    let td = project();
    let mut s = EditorState::open(td.path().join("alpha.txt")).expect("open");
    exec(&s, "pmacs.lsp.config = {}");
    let before_name = active_name(&s);
    let before_count = buffer_count(&s);

    let raised: bool = eval(
        &s,
        &format!(
            "local ok = pcall(pmacs.window.display_file, {:?}) return not ok",
            td.path().display().to_string()
        ),
    );
    pump(&mut s);

    assert!(raised, "display_file on a directory must raise");
    assert_eq!(
        active_name(&s),
        before_name,
        "a refused display_file must not change the active buffer"
    );
    assert_eq!(
        buffer_count(&s),
        before_count,
        "a refused display_file must not create a buffer"
    );
}

// ---------------------------------------------------------------------------
// Step 9 — build or test the project (Journey Stage 1b-1)
//
// `COHERENCE.md` §2 graded this **Partial**: `M-x compile.run` worked but
// had no keybinding, an empty first prompt, and no `cargo build`
// suggestion. These rows are the ratchet for the three.
//
// The trap these fixtures are built around: compile's last-resort cwd is
// `std::env::current_dir()` evaluated at call time, which under `cargo
// test` is the **pmacs repo root — itself a Cargo project**
// (`compile_mode_acceptance.rs` pins exactly that). So any pin asserting
// the *absence* of a Cargo suggestion must be shaped so the fallback is
// never consulted: each fixture carries its own marker, and
// `project_root_of_active()` therefore answers from inside the fixture.
// `set_search_boundary` is not the defence — it clamps only a walk that
// starts below the boundary, and the fallback's walk starts at the repo.
// ---------------------------------------------------------------------------

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

/// Open the compile prompt through the real `C-c c` binding.
fn press_compile_chord(s: &mut EditorState) {
    ctrl(s, 'c');
    type_char(s, 'c');
}

/// A Cargo project a journey can plausibly be run against.
fn cargo_project() -> TempDir {
    let td = tempfile::tempdir().expect("tempdir");
    std::fs::write(
        td.path().join("Cargo.toml"),
        b"[package]\nname = \"journey-fixture\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
    )
    .expect("write Cargo.toml");
    std::fs::create_dir_all(td.path().join("src")).expect("mkdir src");
    std::fs::write(td.path().join("src/main.rs"), b"fn main() {}\n").expect("write main.rs");
    td
}

/// Clamp detection to the fixture so a stray marker above the tempdir
/// (a developer's `/tmp/.git`) cannot leak in.
fn bound_detection_to(s: &EditorState, dir: &Path) {
    exec(
        s,
        &format!(
            "pmacs.project.set_search_boundary({:?})",
            dir.display().to_string()
        ),
    );
}

/// The project root as **detection** reports it.
///
/// `pmacs.project.detect` canonicalizes before walking
/// (`canonicalize_or_passthrough`, `src/project.rs:509-511`), so a
/// `/var/folders/...` tempdir on macOS comes back as
/// `/private/var/folders/...`. `canon()` is *lexical* — it never
/// resolves symlinks — so it is the wrong expectation for any value
/// that has passed through detection, which is exactly what the compile
/// cwd is. Using it here failed both macOS CI legs while Ubuntu (where
/// `/tmp` is not a symlink) stayed green.
fn detected_root(path: &Path) -> String {
    std::fs::canonicalize(path)
        .unwrap_or_else(|_| path.to_path_buf())
        .to_string_lossy()
        .into_owned()
}

fn minibuffer_active(s: &EditorState) -> bool {
    eval(s, "return pmacs.minibuffer.is_active()")
}

fn minibuffer_contents(s: &EditorState) -> String {
    eval(s, "return pmacs.minibuffer.contents()")
}

fn named_text(s: &EditorState, name: &str) -> String {
    eval(
        s,
        &format!(
            r#"
            for _, id in ipairs(pmacs.buffer.list()) do
                if pmacs.describe.buffer(id).name == {name:?} then
                    return id:slice(0, id:len())
                end
            end
            return ""
            "#
        ),
    )
}

/// Drive frames until `pred` holds, pumping the process supervisor as
/// well as the async runtime — a compile run is a spawned child.
fn pump_processes_until(
    s: &mut EditorState,
    timeout_ms: u64,
    mut pred: impl FnMut(&EditorState) -> bool,
) -> bool {
    let stop = Instant::now() + Duration::from_millis(timeout_ms);
    loop {
        if pred(s) {
            return true;
        }
        if Instant::now() >= stop {
            return false;
        }
        s.tick_processes();
        s.tick_async();
        std::thread::sleep(Duration::from_millis(5));
    }
}

/// Walk the journey to the point where a build would be run: launch on
/// the directory, then `RET` on `name` to open a file.
fn walk_to_open_file(dir: &Path, name: &str) -> EditorState {
    let mut s = launch(dir);
    bound_detection_to(&s, dir);
    let line = line_of(&s, name);
    exec(&s, &format!("pmacs.editor.move_to_line({line})"));
    press(&mut s, KeyCode::Enter);
    pump(&mut s);
    assert_eq!(
        active_name(&s),
        dir.join(name).display().to_string(),
        "precondition: the walk must actually open the file, or every pin \
         below is really testing the dired buffer"
    );
    s
}

/// **N** — `C-c c` reaches `compile.run`. Separate from N2 because a
/// prefill assertion alone stays green if the binding is removed and the
/// prompt is opened another way — and the binding is the thing
/// `COHERENCE.md` says is missing.
#[test]
fn journey_step9_the_compile_chord_opens_the_prompt() {
    if reexec_isolated("journey_step9_the_compile_chord_opens_the_prompt") {
        return;
    }
    let td = cargo_project();
    let mut s = walk_to_open_file(td.path(), "Cargo.toml");
    press_compile_chord(&mut s);
    assert!(
        minibuffer_active(&s),
        "C-c c must open the compile prompt; active buffer is {}",
        active_name(&s)
    );
}

/// **N** — the prompt is prefilled from the detected project kind.
#[test]
fn journey_step9_the_prompt_is_prefilled_for_a_cargo_project() {
    if reexec_isolated("journey_step9_the_prompt_is_prefilled_for_a_cargo_project") {
        return;
    }
    let td = cargo_project();
    let mut s = walk_to_open_file(td.path(), "Cargo.toml");
    press_compile_chord(&mut s);
    assert_eq!(
        minibuffer_contents(&s),
        "cargo build",
        "a Rust project's first compile prompt must offer its build command"
    );
}

/// **N** — the directory the prompt captured survives a window switch.
///
/// The prompt is opened against A, the active buffer then moves to an
/// unrelated directory B, and only then is the command accepted. Both
/// readings — the header pmacs writes and the shell's own `pwd` — must
/// say A. Falsified by dropping `{ cwd = ctx.cwd }` from `on_accept`,
/// which is what re-resolving at accept time looks like.
#[test]
fn journey_step9_the_prompt_runs_in_the_directory_it_captured() {
    if reexec_isolated("journey_step9_the_prompt_runs_in_the_directory_it_captured") {
        return;
    }
    let a = cargo_project();
    let b = project();
    let mut s = walk_to_open_file(a.path(), "Cargo.toml");
    press_compile_chord(&mut s);
    assert!(minibuffer_active(&s), "prompt must be open against A");

    // Move the active buffer to B while the prompt waits for input.
    exec(
        &s,
        &format!(
            "pmacs.window.display_file({:?})",
            b.path().join("alpha.txt").display().to_string()
        ),
    );
    pump(&mut s);

    // Accept a cheap command: the subject is the directory, so a fast
    // one keeps the failure message about the directory. Only the
    // editing of an already-open prompt is short-circuited; the prompt
    // itself was opened through the real chord.
    exec(&s, "pmacs.minibuffer.set_contents('pwd')");
    press(&mut s, KeyCode::Enter);
    let finished = pump_processes_until(&mut s, 10_000, |s| {
        named_text(s, "*compilation*").contains("exited")
    });
    assert!(finished, "the accepted run must finish");

    let text = named_text(&s, "*compilation*");
    // Detection-canonical, not lexical: the compile cwd came from
    // `pmacs.project.detect`, and `pwd` reports the physical directory.
    let a_path = detected_root(a.path());
    assert!(
        text.contains(&format!("Directory: {a_path}")),
        "the header must name the directory the prompt captured, not the newly active one;\n{text}"
    );
    assert!(
        text.contains(&a_path),
        "and `pwd` must agree with the header;\n{text}"
    );
}

/// **N** — the offered command runs, in the offered directory.
///
/// Every other pin here compares values the prompt and the resolver
/// already agree on. A wrong directory inside `on_accept` passes all of
/// them. This one accepts what was offered, unedited, and observes a
/// real process.
#[test]
fn journey_step9_the_offered_command_builds_the_project() {
    if reexec_isolated("journey_step9_the_offered_command_builds_the_project") {
        return;
    }
    if !binary_available("cargo") {
        assert!(
            std::env::var_os("PMACS_REQUIRE_CARGO_BUILD").is_none(),
            "PMACS_REQUIRE_CARGO_BUILD is set but `cargo` is not on PATH"
        );
        eprintln!("skipping: `cargo` is not on PATH");
        return;
    }
    let td = cargo_project();
    let mut s = walk_to_open_file(td.path(), "Cargo.toml");
    press_compile_chord(&mut s);
    assert_eq!(
        minibuffer_contents(&s),
        "cargo build",
        "precondition: the prompt offers the build command"
    );

    // Accept exactly what was offered — no editing.
    press(&mut s, KeyCode::Enter);
    let finished = pump_processes_until(&mut s, 120_000, |s| {
        named_text(s, "*compilation*").contains("exited")
    });
    assert!(finished, "the offered build must finish");

    let text = named_text(&s, "*compilation*");
    assert!(
        text.contains(&format!("Directory: {}", detected_root(td.path()))),
        "the build runs in the detected project root;\n{text}"
    );
    assert!(
        text.contains("[compile exited with code 0]"),
        "and the command that was offered is one that actually runs;\n{text}"
    );
    // Positive control on the run itself. A clean exit alone does not
    // prove cargo compiled anything — this names the fixture crate, so
    // the pin fails if the offered command ever stops being a build.
    assert!(
        text.contains("journey-fixture"),
        "cargo must actually have built the fixture crate;\n{text}"
    );
}

/// **N** — the suggestion follows the directory the run will use.
///
/// A Node project nested inside a Cargo one, opened at the *inner* file.
/// Falsified by deriving the kind from the launch directory (the Cargo
/// root here) or from the process cwd — both yield `cargo build`.
///
/// It does **not** catch re-detecting from `project_root_of_active()`'s
/// answer: that helper already returns the innermost root, so detecting
/// from it yields `node` again and this pin stays green.
#[test]
fn journey_step9_a_nested_project_gets_its_own_kind_not_the_outer_one() {
    if reexec_isolated("journey_step9_a_nested_project_gets_its_own_kind_not_the_outer_one") {
        return;
    }
    let outer = cargo_project();
    let sub = outer.path().join("sub");
    std::fs::create_dir_all(&sub).expect("mkdir sub");
    std::fs::write(sub.join("package.json"), b"{ \"name\": \"inner\" }\n").expect("write pkg");
    std::fs::write(sub.join("index.js"), b"console.log(1)\n").expect("write index.js");

    let mut s = walk_to_open_file(&sub, "index.js");
    press_compile_chord(&mut s);
    // `minibuffer_contents` is "" both for an empty prefill and for no
    // minibuffer at all, so the emptiness assertion below is vacuous
    // without this. Bite A caught it: with the binding removed this pin
    // stayed green while six others failed.
    assert!(
        minibuffer_active(&s),
        "precondition: the prompt is actually open"
    );
    assert_eq!(
        eval::<String>(&s, "return pmacs.compile.context().kind"),
        "node",
        "the kind is detected from the resolved cwd, not the launch directory"
    );
    assert_eq!(
        minibuffer_contents(&s),
        "",
        "and `node` has no seeded default, so nothing is offered"
    );
}

/// **N** — `context()` is total in a launched session.
///
/// Asserted with the pathless dired buffer active, so the fallback
/// branch is the one under test. It pins a *property*, deliberately not
/// a value: which directory the fallback resolves to is the test
/// runner's cwd and pinning it would pin the environment.
#[test]
fn journey_step9_the_compile_context_is_total_even_with_no_file_open() {
    if reexec_isolated("journey_step9_the_compile_context_is_total_even_with_no_file_open") {
        return;
    }
    let td = cargo_project();
    let s = launch(td.path());
    assert!(
        active_name(&s).starts_with("*dired:"),
        "precondition: the pathless dired buffer is active"
    );
    assert!(
        eval::<bool>(&s, "return pmacs.compile.context().cwd ~= nil"),
        "a launched session always has somewhere to run a build"
    );
    assert!(
        eval::<bool>(
            &s,
            "local c = pmacs.compile.context()
             local ok, p = pcall(pmacs.project.detect, c.cwd)
             local detected = (ok and p) and p.kind or nil
             return c.kind == detected"
        ),
        "and its kind is exactly what detection answers for that cwd"
    );
}

/// **P** — `_last` still outranks the kind default.
///
/// Green on the pre-image (there was no default). The targeted mutation
/// is reordering the precedence chain to put `defaults[kind]` first.
#[test]
fn journey_step9_preservation_the_last_command_outranks_the_default() {
    if reexec_isolated("journey_step9_preservation_the_last_command_outranks_the_default") {
        return;
    }
    let td = cargo_project();
    let mut s = walk_to_open_file(td.path(), "Cargo.toml");
    exec(&s, "pmacs.compile.run('true')");
    let finished = pump_processes_until(&mut s, 10_000, |s| {
        named_text(s, "*compilation*").contains("exited")
    });
    assert!(finished, "precondition: a run completed and set _last");

    press_compile_chord(&mut s);
    assert_eq!(
        minibuffer_contents(&s),
        "true",
        "a session that has compiled keeps its own command, not the project default"
    );
}

/// **P** — the compile keys that already existed still dispatch.
///
/// Targeted mutation: writing the new binding as an unbind+bind pair
/// over one of these sequences.
#[test]
fn journey_step9_preservation_the_existing_compile_bindings_survive() {
    if reexec_isolated("journey_step9_preservation_the_existing_compile_bindings_survive") {
        return;
    }
    let td = cargo_project();
    let s = walk_to_open_file(td.path(), "Cargo.toml");
    for (sequence, command) in [
        ("M-g n", "error.next"),
        ("M-g p", "error.previous"),
        ("C-x `", "error.next"),
        ("M-!", "shell.command"),
        ("C-c c", "compile.run"),
    ] {
        let bound: String = eval(
            &s,
            &format!("return pmacs.keymap.lookup({sequence:?}).command"),
        );
        assert_eq!(
            bound, command,
            "{sequence} must still dispatch to {command}"
        );
    }
}

/// True when `name` resolves on PATH — the fixture-dependency gate the
/// process suite uses, so a missing binary skips rather than fails.
fn binary_available(name: &str) -> bool {
    std::process::Command::new("which")
        .arg(name)
        .output()
        .is_ok_and(|o| o.status.success())
}

/// **N** — the compile directory is *detection*-canonical, not lexical.
///
/// This pin exists because its absence broke both macOS CI legs while
/// Ubuntu stayed green: `/var` is a symlink to `/private/var` there, so
/// a tempdir's lexical and canonical paths differ, and the original
/// assertions used the lexical one.
///
/// Reproducing it on Linux needs an explicit symlink — which is also a
/// real configuration `Workspace::detect`'s own doc comment names
/// ("`/tmp/sandbox/foo` symlinked to `/home/user/code/foo`"). Launching
/// through the link makes the two paths disagree on every platform, so
/// the regression can no longer hide behind a filesystem that happens
/// not to use symlinks.
#[cfg(unix)]
#[test]
fn journey_step9_the_compile_directory_is_detection_canonical() {
    if reexec_isolated("journey_step9_the_compile_directory_is_detection_canonical") {
        return;
    }
    let parent = tempfile::tempdir().expect("tempdir");
    let real = parent.path().join("real");
    std::fs::create_dir_all(real.join("src")).expect("mkdir real");
    std::fs::write(
        real.join("Cargo.toml"),
        b"[package]\nname = \"journey-fixture\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
    )
    .expect("write Cargo.toml");
    std::fs::write(real.join("src/main.rs"), b"fn main() {}\n").expect("write main.rs");

    let link = parent.path().join("link");
    std::os::unix::fs::symlink(&real, &link).expect("symlink");

    // Precondition, or the pin is vacuous: the two spellings must
    // actually differ, which is the whole condition macOS creates for
    // free and Linux does not.
    assert_ne!(
        canon(&link),
        detected_root(&link),
        "the fixture must make lexical and canonical paths disagree"
    );

    let mut s = walk_to_open_file(&link, "Cargo.toml");
    press_compile_chord(&mut s);
    exec(&s, "pmacs.minibuffer.set_contents('pwd')");
    press(&mut s, KeyCode::Enter);
    let finished = pump_processes_until(&mut s, 10_000, |s| {
        named_text(s, "*compilation*").contains("exited")
    });
    assert!(finished, "the accepted run must finish");

    let text = named_text(&s, "*compilation*");
    assert!(
        text.contains(&format!("Directory: {}", detected_root(&link))),
        "the header must name the directory detection resolved to;\n{text}"
    );
}

// ---------------------------------------------------------------------------
// Step 4 — understand the visible interface (Journey Stage 1b-3)
//
// `COHERENCE.md` §18 graded onboarding "missing entirely": no welcome,
// no cheat sheet reachable from inside the editor, and `M-x` the only
// door in — which a new user has no way to learn about.
//
// These rows drive `prepare_startup`, the production call `run()` makes.
// Calling `finalize_local_launch` by hand instead would leave the wiring
// unpinned: deleting the one call inside `prepare_startup` would keep
// every other assertion here green while shipping no welcome at all.
// ---------------------------------------------------------------------------

/// The `*scratch*` buffer's text, wherever it currently sits.
fn scratch_text(s: &EditorState) -> String {
    eval(
        s,
        r#"
        for _, id in ipairs(pmacs.buffer.list()) do
            if pmacs.describe.buffer(id).name == "*scratch*" then
                return id:slice(0, id:len())
            end
        end
        return ""
        "#,
    )
}

fn welcome_entries(s: &EditorState) -> Vec<(String, String)> {
    let raw: Vec<String> = eval(
        s,
        "local out = {}
         for _, e in ipairs(pmacs.welcome.entries) do
           out[#out + 1] = e.keys .. '\\1' .. e.label
         end
         return out",
    );
    raw.into_iter()
        .map(|row| {
            let (keys, label) = row
                .split_once('\u{1}')
                .expect("entry encodes keys and label");
            (keys.to_owned(), label.to_owned())
        })
        .collect()
}

/// Drive the production startup path with no target, as `pmacs` does.
fn start_local() -> EditorState {
    match pmacs::editor::prepare_startup(None).expect("startup must not fail") {
        pmacs::editor::Startup::Local(state) => *state,
        pmacs::editor::Startup::HandOff(_) => panic!("no init.lua attach request in a test"),
    }
}

/// **N** — journey step 4: a no-target local launch greets.
///
/// Falsified by deleting the `finalize_local_launch` call inside
/// `prepare_startup` — the mutation every by-hand pin would survive.
#[test]
fn journey_step4_a_no_target_launch_greets_in_scratch() {
    if reexec_isolated("journey_step4_a_no_target_launch_greets_in_scratch") {
        return;
    }
    let s = start_local();

    // Preconditions asserted, not assumed (framing §3.2b): a developer
    // whose real init.lua arms desktop mode would otherwise get a
    // restored scratch and a silently different result.
    assert!(
        eval::<bool>(
            &s,
            "return pmacs.session == nil or pmacs.session.desktop_armed ~= true"
        ),
        "precondition: desktop restore must be unarmed for this pin to mean anything"
    );
    assert_eq!(active_name(&s), "*scratch*");

    let text = active_text(&s);
    assert!(
        !text.is_empty(),
        "an unconfigured launch must say something"
    );
    assert!(
        text.contains("M-x"),
        "and must name the one key that opens everything; got {text:?}"
    );
}

/// **N** — every entry the welcome names is actually bound.
///
/// A property over the structured list, not a scrape of prose: `C-c c`
/// is two chords and nothing in the rendered text marks the boundary.
#[test]
fn journey_step4_every_advertised_key_is_bound() {
    if reexec_isolated("journey_step4_every_advertised_key_is_bound") {
        return;
    }
    let s = start_local();
    let entries = welcome_entries(&s);
    assert!(
        !entries.is_empty(),
        "precondition: the entry list must be non-empty or this loop is vacuous"
    );
    for (keys, label) in entries {
        let bound: Option<String> = eval(
            &s,
            &format!("local b = pmacs.keymap.lookup({keys:?}) return b and b.command"),
        );
        assert!(
            bound.is_some(),
            "the welcome advertises {keys:?} ({label}) but nothing is bound to it"
        );
    }
}

/// **N** — the rendered text contains every entry.
///
/// Pin 2 alone would pass if rendering silently dropped one.
#[test]
fn journey_step4_the_rendered_welcome_contains_every_entry() {
    if reexec_isolated("journey_step4_the_rendered_welcome_contains_every_entry") {
        return;
    }
    let s = start_local();
    let text = active_text(&s);
    for (keys, label) in welcome_entries(&s) {
        assert!(
            text.contains(&keys),
            "welcome text omits the key {keys:?}; got {text:?}"
        );
        assert!(
            text.contains(&label),
            "welcome text omits the label {label:?}; got {text:?}"
        );
    }
}

/// **N** — `M-x help` renders the cheat sheet, reached the way a user
/// reaches it.
///
/// `pmacs.command.invoke` is the *programmatic* API; M-x is
/// `editor.execute-command`, a minibuffer with the `commands` completion
/// source that calls `invoke_interactive` only on accept.
///
/// The selection is asserted **before** RET: a selected candidate
/// shadows typed text, and `Minibuffer::accept` does `session.take()`,
/// so nothing about the accepted value survives afterwards.
#[test]
fn journey_step4_m_x_help_renders_the_cheat_sheet() {
    if reexec_isolated("journey_step4_m_x_help_renders_the_cheat_sheet") {
        return;
    }
    let mut s = start_local();

    s.dispatch_key(
        FrontendId::LOCAL,
        key(KeyCode::Char('x'), KeyModifiers::ALT),
    );
    assert!(
        eval::<bool>(&s, "return pmacs.minibuffer.is_active()"),
        "M-x must open the command palette"
    );
    for ch in "help".chars() {
        type_char(&mut s, ch);
    }
    assert_eq!(
        eval::<Option<String>>(&s, "return pmacs.minibuffer.selected()").as_deref(),
        Some("help"),
        "the completion source must have `help` selected; a different \
         candidate would accept a different command"
    );

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

    let help = named_text(&s, "*help*");
    for (keys, _) in welcome_entries(&s) {
        assert!(
            help.contains(&keys),
            "the cheat sheet omits {keys:?}; got:\n{help}"
        );
    }
}

/// **P** — the greeted buffer is editable and unmodified.
///
/// Targeted mutation: rendering through `set_generated_contents`, which
/// would lift read-only, discard history, and fail the insert.
#[test]
fn journey_step4_preservation_the_greeted_scratch_is_editable_and_clean() {
    if reexec_isolated("journey_step4_preservation_the_greeted_scratch_is_editable_and_clean") {
        return;
    }
    let mut s = start_local();
    assert!(
        eval::<bool>(
            &s,
            "return pmacs.describe.buffer(pmacs.window.buffer()).modified == false"
        ),
        "a greeting must not look like unsaved work"
    );

    let before = active_text(&s).len();
    type_char(&mut s, 'X');
    assert!(
        active_text(&s).len() > before,
        "step 5 must still work from the first frame: typing inserts"
    );
}

/// **P** — a file target does not greet.
#[test]
fn journey_step4_preservation_a_file_target_does_not_greet() {
    if reexec_isolated("journey_step4_preservation_a_file_target_does_not_greet") {
        return;
    }
    let td = project();
    let path = td.path().join("alpha.txt");
    let s = match pmacs::editor::prepare_startup(Some(path.clone())).expect("startup") {
        pmacs::editor::Startup::Local(state) => *state,
        pmacs::editor::Startup::HandOff(_) => panic!("no attach request"),
    };
    assert_eq!(active_name(&s), path.display().to_string());
    assert_eq!(
        scratch_text(&s),
        "",
        "a positional argument means \"open this\", not \"greet me\""
    );
}

/// **P** — a directory target does not greet either, so Stage 1a's
/// dired listing is what the user sees.
///
/// Separate from the file pin because the directory path reaches
/// `*scratch*` differently: the bootstrap replaces the window's buffer
/// and `replace_active_buffer` removes nothing, so the scratch buffer
/// still exists to be wrongly greeted.
#[test]
fn journey_step4_preservation_a_directory_target_does_not_greet() {
    if reexec_isolated("journey_step4_preservation_a_directory_target_does_not_greet") {
        return;
    }
    let td = project();
    let mut s =
        match pmacs::editor::prepare_startup(Some(td.path().to_path_buf())).expect("startup") {
            pmacs::editor::Startup::Local(state) => *state,
            pmacs::editor::Startup::HandOff(_) => panic!("no attach request"),
        };
    pump(&mut s);
    assert!(active_name(&s).starts_with("*dired:"));
    assert_eq!(scratch_text(&s), "", "the dired listing is the greeting");
}

/// **P** — a non-empty `*scratch*` is never overwritten.
#[test]
fn journey_step4_preservation_existing_scratch_content_survives() {
    if reexec_isolated("journey_step4_preservation_existing_scratch_content_survives") {
        return;
    }
    let mut s = EditorState::new();
    exec(&s, "pmacs.window.buffer():insert(0, 'user content')");
    s.finalize_local_launch(false);
    assert_eq!(
        active_text(&s),
        "user content",
        "config or a restored desktop owns whatever is already there"
    );
}

/// **P** — a non-active `*scratch*` is not greeted. Stands in for a
/// desktop restore having put something else in front.
#[test]
fn journey_step4_preservation_a_backgrounded_scratch_is_not_greeted() {
    if reexec_isolated("journey_step4_preservation_a_backgrounded_scratch_is_not_greeted") {
        return;
    }
    let td = project();
    let mut s = EditorState::new();
    exec(
        &s,
        &format!(
            "pmacs.window.display_file({:?})",
            td.path().join("alpha.txt").display().to_string()
        ),
    );
    pump(&mut s);
    s.finalize_local_launch(false);
    assert_eq!(
        scratch_text(&s),
        "",
        "only the buffer actually greeting the user is written to"
    );
}

/// **P** — the constructors greet nothing on their own.
///
/// This is what makes the seam the only writer, and would catch a
/// greeting smuggled back into a constructor — including the daemon's.
/// Necessary but not sufficient: pin 1 is what proves the seam is
/// reached in production.
#[test]
fn journey_step4_preservation_constructors_never_greet() {
    if reexec_isolated("journey_step4_preservation_constructors_never_greet") {
        return;
    }
    let bare = EditorState::new();
    assert_eq!(scratch_text(&bare), "", "EditorState::new must not greet");

    let td = project();
    let file = EditorState::open(td.path().join("alpha.txt")).expect("open file");
    assert_eq!(
        scratch_text(&file),
        "",
        "EditorState::open(file) must not greet"
    );

    let mut dir = EditorState::open(td.path().to_path_buf()).expect("open dir");
    pump(&mut dir);
    assert_eq!(
        scratch_text(&dir),
        "",
        "EditorState::open(dir) must not greet"
    );
}

// Step 6 — receive language intelligence (Journey Stage 1b-2)
//
// `COHERENCE.md` §2 graded this **Partial**: a preconfigured server that
// is not installed failed silently, and working tree-sitter highlighting
// masked it. This row pins the failure being *told*, end to end, through
// the same walk a user takes.
// ---------------------------------------------------------------------------

/// The `lsp` modeline segment for the active buffer, found by face —
/// `EvaluatedStatuslineSegment` carries `provider_id`, not the
/// registration's name.
fn lsp_segment(s: &EditorState) -> Option<String> {
    let outcome = pmacs::statusline::evaluate_statusline(
        s.lua_host.lua(),
        &s.core,
        &s.statusline_registry,
        pmacs::statusline::StatuslineEvaluationTarget::Grid {
            frontend_id: FrontendId::LOCAL,
        },
    );
    let pmacs::statusline::StatuslineEvaluationOutcome::Ready(windows) = outcome.outcome else {
        return None;
    };
    windows
        .into_iter()
        .flat_map(|w| w.right)
        .find(|seg| seg.face == "ui.modeline.lsp")
        .map(|seg| seg.text)
}

/// **N** — step 6: when language intelligence cannot start, the user is
/// told, on the path `pmacs .` actually takes.
///
/// The configured command is one that cannot exist, so a developer with
/// `rust-analyzer` installed gets the same result as CI — and the
/// fixture asserts that precondition, or every assertion here would be
/// vacuous.
#[test]
fn journey_step6_a_missing_language_server_is_reported_not_swallowed() {
    if reexec_isolated("journey_step6_a_missing_language_server_is_reported_not_swallowed") {
        return;
    }
    let td = tempfile::tempdir().expect("tempdir");
    std::fs::write(td.path().join("Cargo.toml"), b"[package]\nname=\"x\"\n").expect("write toml");
    std::fs::write(td.path().join("main.rs"), b"fn main() {}\n").expect("write rs");

    let absent = td.path().join("no-such-bin").join("rust-analyzer");
    assert!(
        !absent.exists(),
        "fixture precondition: the configured server must not exist"
    );

    // Launch as `pmacs .` does — this lists the directory in dired.
    let mut s = launch(td.path());
    // `launch` clears `pmacs.lsp.config`, so configure after it.
    exec(
        &s,
        &format!(
            "pmacs.project.set_search_boundary({:?})
             pmacs.lsp.config.rust = {{ command = {:?} }}",
            td.path().display().to_string(),
            absent.display().to_string()
        ),
    );

    // Visit the source file with the real key, as step 5 does.
    let line = line_of(&s, "main.rs");
    exec(&s, &format!("pmacs.editor.move_to_line({line})"));
    press(&mut s, KeyCode::Enter);
    pump(&mut s);
    assert_eq!(
        active_name(&s),
        td.path().join("main.rs").display().to_string(),
        "precondition: the walk must actually open the source file"
    );

    let msg = status(&s);
    assert!(
        msg.contains(&absent.display().to_string()),
        "the user is told which command did not start; got {msg:?}"
    );
    assert_eq!(
        lsp_segment(&s).as_deref(),
        Some("LSP:!"),
        "and the modeline says so, rather than rendering nothing — which \
         is what made highlighting able to mask this"
    );
}

/// **N** — the greeting renders as separate rows on the **first frame**.
///
/// Writing to the registry without `notify_buffer_edit` leaves the
/// window's `TextView` indexed against the empty `*scratch*` it was
/// built for. Newlines are zero-width to a painter working from a stale
/// line index, so the whole three-line greeting collapses onto row 0 —
/// visible to a user, invisible to every buffer-text assertion above.
///
/// Falsified by dropping the `notify_buffer_edit` call in
/// `finalize_local_launch`.
#[test]
fn journey_step4_the_welcome_paints_as_multiple_rows_on_the_first_frame() {
    use pmacs::cell::{Cell, CellGrid, CellSize, Glyph};
    if reexec_isolated("journey_step4_the_welcome_paints_as_multiple_rows_on_the_first_frame") {
        return;
    }

    let s = start_local();
    let (rows, cols) = (12u32, 100u32);
    let mut cells = vec![Cell::default(); (rows * cols) as usize];
    let mut grid = CellGrid {
        cells: &mut cells,
        stride: cols,
        size: CellSize::new(rows, cols),
    };
    let _ = pmacs::editor::paint_frame(
        &s,
        FrontendId::LOCAL,
        &std::collections::HashMap::new(),
        &mut grid,
        CellSize::new(rows, cols),
    );

    let row_text = |row: u32| -> String {
        (0..cols)
            .map(
                |column| match &cells[(row * cols + column) as usize].glyph {
                    Glyph::Char(ch) => *ch,
                    Glyph::Cluster(bytes) => std::str::from_utf8(bytes)
                        .ok()
                        .and_then(|t| t.chars().next())
                        .unwrap_or(' '),
                    Glyph::Continuation => ' ',
                },
            )
            .collect::<String>()
            .trim_end()
            .to_owned()
    };

    assert!(
        row_text(0).contains("Welcome to pmacs"),
        "row 0 is the greeting's first line; got {:?}",
        row_text(0)
    );
    // The discriminating half: with a stale TextView these land on row 0
    // too, and row 1 is blank.
    assert!(
        row_text(1).contains("C-x C-f"),
        "the second line must occupy its own row, not collapse into the \
         first; row 1 = {:?}, row 0 = {:?}",
        row_text(1),
        row_text(0)
    );
    assert!(
        !row_text(0).contains("C-x C-f"),
        "and must not have been folded into row 0; got {:?}",
        row_text(0)
    );
}

// ---------------------------------------------------------------------------
// The isolation mechanism itself
// ---------------------------------------------------------------------------

/// **N** — the re-exec of §1.10 actually redirects the child's roots.
///
/// Every other test in this file is *protected* by the mechanism; none
/// of them fails if it silently stops working, because they assert about
/// editor behaviour and a developer's `init.lua` usually leaves that
/// alone. This one asserts the mechanism's own output: the roots the
/// child resolves, from inside the child.
///
/// Falsified by dropping `.envs(child_env)` from `reexec_isolated` — the
/// child then resolves the developer's real `~/.config/pmacs` and
/// `~/.local/share`, and all four assertions below fail.
#[test]
fn journey_isolated_child_resolves_the_controlled_roots() {
    if reexec_isolated("journey_isolated_child_resolves_the_controlled_roots") {
        return;
    }
    let base = Path::new(env!("CARGO_TARGET_TMPDIR"))
        .join("journey-isolation")
        .join("journey_isolated_child_resolves_the_controlled_roots");
    let under = |p: Option<std::path::PathBuf>, what: &str| {
        let p = p.unwrap_or_else(|| panic!("{what} must resolve in the child"));
        assert!(
            p.starts_with(&base),
            "{what} resolved to {} — outside the controlled base {}",
            p.display(),
            base.display()
        );
    };
    under(pmacs::config::user_config_dir(), "the config dir");
    under(
        Some(pmacs::builtin_packages::bundled_runtime_dir()),
        "the bundled-package dir",
    );
    // `PMACS_STATE_HOME` is the fifth variable: it outranks
    // `XDG_STATE_HOME`, so a child given only the four XDG ones inherits
    // whatever the launching shell exported.
    under(pmacs::state::user_state_dir(), "the state dir");
    under(pmacs::minibuffer::user_history_dir(), "the history dir");
}

/// **N** — the ambient entry point is still what this suite drives.
///
/// The isolation seam has a `new_with_roots` / `open_with_roots` sibling
/// that every other suite in the tree now takes. Taking it *here* would
/// be a silent regression of what the ratchet exists to prove — a
/// production entry point with no caller — and would leave every
/// assertion in this file green. Asserted against this file's own source
/// rather than left as an obvious-by-inspection property.
#[test]
fn journey_drives_the_ambient_production_entry_point() {
    let src = std::fs::read_to_string(
        Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("tests")
            .join("journey_acceptance.rs"),
    )
    .expect("read this suite's own source");
    // Every needle is assembled rather than written whole: a literal
    // spelled out here would appear in this file and match itself, which
    // turns the positive check vacuous and the negative ones into
    // guaranteed failures.
    let ambient_open = concat!("EditorState::open(", "path.to_path_buf())");
    let seam_open = concat!("EditorState::open", "_with_roots(");
    let seam_new = concat!("EditorState::new", "_with_roots(");
    assert!(
        src.contains(ambient_open),
        "journey must call the parameterless production `open`"
    );
    assert!(
        !src.contains(seam_open),
        "journey must not take the isolation seam — its isolation comes \
         from the environment its child is launched with"
    );
    assert!(
        !src.contains(seam_new),
        "journey must not take the isolation seam — its isolation comes \
         from the environment its child is launched with"
    );
}
