// tests/destination_capture_acceptance.rs --- the Lua-reachable capture.

//! Acceptance for `docs/destination-capture-framing.md` §7: a
//! destination any asynchronous continuation can capture, and the
//! profile that says which of `commit_to`'s preconditions it depends on
//! (Q#DC-1 … Q#DC-5).
//!
//! **What this suite does NOT prove**, deliberately: that git — or any
//! other adopter — surfaces in the right frontend. This lane ships the
//! mechanism and the tests for the mechanism; adoption is #227's, after
//! it lands (§8). Every test here therefore drives the Lua surface
//! directly rather than through a consumer.
//!
//! Three disciplines it keeps:
//!
//! * **Every "not applicable" cell in Q#DC-2's preflight matrix is
//!   asserted as NOT refusing**, not merely left untested. A check
//!   deliberately omitted and a check someone forgot look identical from
//!   the outside, and the next reader restores the second one.
//! * **A refusal is asserted on its reason**, never on the mere fact
//!   that something failed. `commit_to` has five distinct refusals and a
//!   raise; "it errored" would pass on any of the wrong ones.
//! * **The panel profile's relaxation is pinned at BOTH of its
//!   evaluation sites** (revision 7). The preflight is an early refusal
//!   that spares the body; the guarantee is enforced where placement
//!   resolves, because the body is arbitrary synchronous Lua and can
//!   create the fallback *after* any snapshot was taken — refusing
//!   `await` stops a second coroutine interleaving, not the body's own
//!   statements. Three tests carry that split and none subsumes another:
//!   `a_panel_commit_that_falls_back_runs_the_document_preflight` (the
//!   body must not run),
//!   `a_panel_commit_whose_body_creates_the_fallback_is_refused_at_placement`
//!   (the result must not land), and
//!   `a_panel_commit_that_falls_back_with_a_valid_destination_still_lands`
//!   (falling back is still graceful degradation, not an error).
//!
//! `tests/journey_acceptance.rs` and `tests/dired_acceptance.rs` are the
//! preservation half of the same §7 and are run alongside this suite:
//! they hold the Stage 1a contract this lane generalizes, and if either
//! needed editing the generalization changed Journey semantics rather
//! than extending them.

use pmacs::buffer::BufferId;
use pmacs::editor::EditorState;
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()
}

/// A fresh editor with LSP disabled: no test here asserts anything about
/// a language server, so the wipe cannot make an assertion vacuous.
fn editor() -> EditorState {
    let s = EditorState::new_with_roots(&crate::iso::roots());
    exec(&s, "pmacs.lsp.config = {}");
    s
}

/// A directory with a file worth displaying.
fn project() -> TempDir {
    let td = tempfile::tempdir().expect("tempdir");
    std::fs::write(td.path().join("alpha.txt"), b"alpha\n").expect("write alpha");
    td
}

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

fn buffer_in(s: &EditorState, window: WindowId) -> Option<BufferId> {
    s.core.borrow().windows.get(&window).map(|w| w.buffer_id)
}

/// A window's buffer **by name**, so a placement assertion reads as
/// "`*result*` went to the panel" rather than as two opaque ids.
fn name_in(s: &EditorState, window: WindowId) -> String {
    let buffer = buffer_in(s, window).expect("window is live");
    let core = s.core.borrow();
    let registry = core.registry.borrow();
    registry.get(buffer).expect("buffer").name().to_string()
}

fn local_window(s: &EditorState) -> WindowId {
    s.core
        .borrow()
        .views
        .get(&FrontendId::LOCAL)
        .expect("LOCAL view")
        .active
}

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

/// A frontend that has a layout but no live document window (Q#DC-4).
const DOCUMENTLESS: FrontendId = FrontendId(9);

/// Register a second frontend with its own single-window layout,
/// mirroring `build_fresh_frontend_view` — the same helper shape
/// `journey_acceptance` and `bottom_panel_stage1_acceptance` use.
fn attach_frontend(s: &EditorState, fid: FrontendId) -> WindowId {
    let win = WindowId::next();
    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"))
    };
    core.windows
        .insert(win, Window::new(win, buffer_id, text_view));
    core.register_frontend_view(fid, view_over(win));
    win
}

/// Register a frontend whose layout names a window that is **not live**,
/// so `primary_document_window` finds nothing to hand back.
///
/// **Why this shape and not a side-window-only layout.** The obvious
/// reading of "a frontend with no document window" is a frontend showing
/// only a bottom panel — but that state is asserted impossible: Q#BP6
/// says a layout always retains at least one non-side window, and
/// `EditorCore::non_side_target` carries a `debug_assert!` that fires
/// under `cargo test` if one ever does. So the reachable spelling of the
/// same condition is a layout whose document window has gone while the
/// view remains, which is what this builds.
///
/// **Recorded honestly, because the framing implies more than the tree
/// does** (`docs/destination-capture-framing.md` Q#DC-4): with Q#BP6
/// held, a *registered* frontend in a healthy editor always has a live
/// document window, so the absent document pair is a **defensive**
/// branch rather than a routine one. It is still the right decision —
/// capture stays total, and an adopter with nowhere to land gets a
/// refusal naming that rather than permission to fall back to ambient
/// state — and it is still worth pinning, because the alternative to
/// pinning it is a branch nothing ever executes.
fn attach_documentless_frontend(s: &EditorState, fid: FrontendId) {
    let mut core = s.core.borrow_mut();
    core.register_frontend_view(fid, view_over(WindowId::next()));
}

fn view_over(win: WindowId) -> FrontendView {
    FrontendView {
        layout: Layout::single(win),
        active: win,
        fold_projection: true,
        panel_capable: true,
        frame_geometry: None,
        panel_hidden: false,
    }
}

/// Capture through the **production** Lua entry point and leave the
/// userdata in the global `dest`.
///
/// Nothing in this suite can construct one by any other route — that is
/// what `a_forged_destination_is_still_refused` is about — so every test
/// below runs against a destination the editor minted.
fn capture(s: &EditorState) {
    exec(s, "dest = pmacs.window.capture_destination()");
    assert!(
        eval::<bool>(s, "return dest ~= nil"),
        "the capture must always yield a destination while a frontend exists"
    );
}

/// Run a body that also executes `also` under `profile`, reporting
/// `(ok, reason)`.
///
/// `profile` is spliced as a Lua expression, so a caller can pass
/// `"nil"`, `"'panel'"`, `"42"` — the argument-shape distinctions
/// Q#DC-5 turns on are exactly what this suite has to vary. `also` is
/// spliced as Lua statements, for the rows that must observe *where* an
/// accepted commit put its result and not merely that it was accepted.
fn commit_body(s: &EditorState, profile: Option<&str>, also: &str) {
    let call = match profile {
        Some(profile) => format!("pmacs.window.commit_to(dest, body, {profile})"),
        None => "pmacs.window.commit_to(dest, body)".to_string(),
    };
    exec(
        s,
        &format!(
            "ran = false
             local body = function() ran = true; {also} end
             raised = nil
             local caught, a, b = pcall(function() return {call} end)
             if caught then ok, reason = a, b
             else ok, reason, raised = false, nil, tostring(a) end"
        ),
    );
}

/// Run an inert body under `profile` and report `(ok, reason)`.
fn commit(s: &EditorState, profile: Option<&str>) {
    commit_body(s, profile, "");
}

fn ok(s: &EditorState) -> bool {
    eval(s, "return ok == true")
}

fn ran(s: &EditorState) -> bool {
    eval(s, "return ran")
}

fn reason(s: &EditorState) -> String {
    eval(s, "return tostring(reason)")
}

/// The message a raise (as opposed to a `(false, reason)` refusal)
/// carried, or `None` if nothing was raised.
fn raised(s: &EditorState) -> Option<String> {
    eval::<Option<String>>(s, "return raised")
}

// ---------------------------------------------------------------------------
// §7 — a captured destination survives a frontend switch
// ---------------------------------------------------------------------------

/// **N** — the failure the lane exists for: the result lands in the
/// frontend that *asked*, not in whichever one is ambient when the work
/// settles.
///
/// Asserted for **both** profiles. The panel profile drops three of the
/// four preflight checks, and a plausible way to implement that is to
/// drop the scope with them — which would leave a panel continuation
/// resolving its target from ambient state, the exact P1a defect. So the
/// scope is pinned per profile rather than once.
///
/// Falsified by making the commit display ambiently: the file then
/// appears in the competitor's window. Asserting merely that
/// `capture_destination()` returns userdata would pass on a capture that
/// does nothing.
#[test]
fn a_captured_destination_survives_a_frontend_switch() {
    for profile in [None, Some("'panel'")] {
        let td = project();
        let s = editor();
        capture(&s);

        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 returns to.
        s.core.borrow_mut().active_frontend = COMPETITOR;

        let alpha = td.path().join("alpha.txt").display().to_string();
        exec(
            &s,
            &format!(
                "committed = pmacs.window.commit_to(dest, function()
                   pmacs.window.display_file({alpha:?})
                 end{})",
                profile.map_or(String::new(), |p| format!(", {p}"))
            ),
        );

        assert!(
            eval::<bool>(&s, "return committed"),
            "{profile:?}: the commit must be accepted"
        );
        assert_eq!(
            buffer_in(&s, other_win),
            other_before,
            "{profile:?}: the competing frontend's window must be untouched"
        );
        s.core.borrow_mut().active_frontend = FrontendId::LOCAL;
        assert_eq!(
            active_name(&s),
            alpha,
            "{profile:?}: the commit must land in the capturing frontend's window"
        );
        assert_eq!(
            local_window(&s),
            local_win,
            "{profile:?}: and in that window, not a new one"
        );
    }
}

// ---------------------------------------------------------------------------
// §7 — the forged destination stays refused
// ---------------------------------------------------------------------------

/// **P (Q#JR14d)** — generalizing the capture does not widen what
/// extension code can fabricate.
///
/// A plausible `{frontend, window, buffer}` table is what any Lua could
/// build, and the capture now hands out the *same* userdata type through
/// a public entry point — so the type check is re-asserted after the
/// rename rather than assumed to have survived it.
///
/// *Mutation:* accept `mlua::Value::Table` in the borrow arm. This
/// fails; nothing in `journey_acceptance` covers the new entry point.
#[test]
fn a_forged_destination_is_still_refused() {
    let s = editor();
    capture(&s);
    let win = eval::<i64>(&s, "return dest:window()");

    exec(
        &s,
        &format!(
            "ran = false
             local caught, err = pcall(pmacs.window.commit_to,
               {{ frontend = 0, window = {win}, buffer = 0 }},
               function() ran = true end)
             rejected = (not caught) 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!(
        !ran(&s),
        "a rejected destination must not reach the callback"
    );
}

// ---------------------------------------------------------------------------
// §7 — the preflight matrix, in BOTH profiles (Q#DC-2)
// ---------------------------------------------------------------------------

/// **N** — each of the four preconditions refuses under the document
/// profile, and each of the three the panel profile omits does **not**
/// refuse under it.
///
/// This is the substance of Q#DC-2. The matrix:
///
/// | # | precondition | document | panel |
/// |---|--------------|----------|-------|
/// | 1 | frontend has a layout | required | **required** |
/// | 2 | window still live | required | not applicable |
/// | 3 | window still shows the captured buffer | required | not applicable |
/// | 4 | window is not dedicated | required | not applicable |
///
/// The panel column is the half that could not be written before this
/// lane, and the half most at risk of being "fixed" later by someone who
/// reads an omission as an oversight — a panel result does not occupy
/// the captured document window, does not replace its buffer, and does
/// not need it to exist, so each of checks 2–4 would refuse `git.status`
/// for a document-window change unrelated to where the panel goes.
///
/// Table-driven so the failure message names *which* cell regressed,
/// which eight near-identical tests would give up in exchange for
/// nothing.
///
/// *Mutation:* apply all four checks in both profiles — the three panel
/// rows fail. *Second mutation:* apply only check 1 in both profiles —
/// the three document rows fail.
#[test]
fn the_preflight_matrix_holds_in_both_profiles() {
    // (label, Lua that breaks the precondition, reason fragment,
    //  whether the PANEL profile refuses too)
    let cases: [(&str, &str, &str, bool); 4] = [
        (
            "frontend gone",
            // Handled in Rust below: unregistering a view has no Lua surface.
            "",
            "requesting frontend is gone",
            true,
        ),
        (
            "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",
            false,
        ),
        (
            "stale buffer",
            "pmacs.window.switch_buffer(pmacs.buffer.create('*usurper*'))",
            "now shows another buffer",
            false,
        ),
        (
            "dedicated",
            "pmacs.window.set_params(dest:window(), { dedicated = true })",
            "is dedicated",
            false,
        ),
    ];

    for (label, break_it, expected, panel_refuses) in cases {
        for profile in [None, Some("'panel'")] {
            let s = editor();
            capture(&s);

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

            commit(&s, profile);
            assert_eq!(
                raised(&s),
                None,
                "{label}/{profile:?}: a precondition is a refusal, not a raise"
            );

            let refuses = profile.is_none() || panel_refuses;
            if refuses {
                assert!(!ok(&s), "{label}/{profile:?}: commit_to must refuse");
                assert!(
                    reason(&s).contains(expected),
                    "{label}/{profile:?}: reason must say why; wanted {expected:?}, got {:?}",
                    reason(&s)
                );
                assert!(
                    !ran(&s),
                    "{label}/{profile:?}: the callback must not run at all -- validating \
                     after it is four mutations too late"
                );
            } else {
                assert!(
                    ok(&s),
                    "{label}/panel: this check is DELIBERATELY omitted for a panel \
                     result, which touches no document window; got refusal {:?}",
                    reason(&s)
                );
                assert!(ran(&s), "{label}/panel: the callback must run");
            }
        }
    }
}

// ---------------------------------------------------------------------------
// §7 — the panel profile's relaxation is CONDITIONAL (Q#DC-2, revision 7)
// ---------------------------------------------------------------------------

/// The Lua a `"panel"` continuation runs: put a result buffer in the
/// bottom panel. It is the shape `listview.open` resolves to by default
/// (`builtin/runtime/listview.lua`), and the shape git's `*git-status*`
/// adoption will take.
const PANEL_BODY: &str = "pmacs.window.display(pmacs.buffer.create('*result*'), \
                          { side = 'bottom' })";

/// Arrange one of the two reasons a side request falls back into a
/// document window, and assert the arrangement took.
///
/// The two arms are independent branches of
/// `EditorCore::resolve_placement`, so a fix that handled only one would
/// leave the other live. Every fallback test below drives both.
fn arrange_fallback(s: &EditorState, cause: &str) {
    if cause == "not panel-capable" {
        // Q#BP13's capability gate: `side` is honoured only on a
        // panel-capable frontend.
        s.core
            .borrow_mut()
            .views
            .get_mut(&FrontendId::LOCAL)
            .expect("LOCAL view")
            .panel_capable = false;
    } else {
        // Q#BP3 2.iii: the one side slot is dedicated to another buffer,
        // and a second panel is never created.
        exec(
            s,
            "pmacs.window.display(pmacs.buffer.create('*pinned*'),
               { side = 'bottom', dedicated = true, select = false })",
        );
        assert!(
            s.core.borrow().side_window_for(FrontendId::LOCAL).is_some(),
            "{cause}: the arrangement must actually create the side slot"
        );
    }
}

/// **N** — a `"panel"` commit whose placement *already* falls back is
/// refused **before its body runs**, on the stale-intent reason.
///
/// The defect: the panel column dropped checks 2–4 on the claim that a
/// panel result never touches a document window — but panel placement
/// falls back to an ordinary document window and then *installs the
/// result there* (`EditorCore::apply_placement` says so in its own
/// comment). The relaxation therefore handed a `"panel"` commit
/// permission to overwrite a document view with no stale-intent guard:
/// capture A, the user opens B, the continuation lands and B is gone.
///
/// **This is the EARLY half, not the guarantee.** It is served by
/// `EditorCore::commit_destination_refusal` consulting
/// `panel_placement_can_fall_back`, which can only read the state that
/// holds *now*. The reason that is worth having anyway is the same reason
/// `commit_to` preflights at all: a body allocates a buffer, registers a
/// handle and paints long before it reaches any call that could refuse,
/// so refusing here leaves no debris. A frontend that cannot render a
/// panel will not acquire the capability mid-body, which is exactly the
/// case this catches.
///
/// The guarantee — for the case a snapshot **cannot** catch, where the
/// body creates the fallback itself — is
/// `a_panel_commit_whose_body_creates_the_fallback_is_refused_at_placement`.
/// Neither test subsumes the other: this one pins that nothing runs, that
/// one pins that nothing lands.
///
/// Each row asserts four things: the commit **refuses**, it refuses for
/// the stale-intent reason (not incidentally), the body never ran, and
/// the newer buffer is still there.
///
/// *Mutation:* delete the `panel_placement_can_fall_back` arm from
/// `commit_destination_refusal`. Both rows fail — the body runs, and the
/// placement backstop then refuses as a *raise*, so `ok`/`ran`/`reason`
/// all move.
#[test]
fn a_panel_commit_that_falls_back_runs_the_document_preflight() {
    for cause in ["not panel-capable", "side slot dedicated elsewhere"] {
        let s = editor();

        // Arrange the fallback cause BEFORE capturing, so the preflight
        // can see it — which is exactly what distinguishes this test from
        // the body-induced one below.
        arrange_fallback(&s, cause);

        capture(&s);
        let doc = local_window(&s);
        assert_eq!(
            eval::<Option<u64>>(&s, "return dest:window()"),
            Some(doc.raw()),
            "{cause}: the capture must name the document window, not the panel"
        );

        // The user replaces the captured buffer while the work is in
        // flight: `*newer*` is newer information than the request.
        exec(
            &s,
            "pmacs.window.switch_buffer(pmacs.buffer.create('*newer*'))",
        );
        assert_eq!(
            name_in(&s, doc),
            "*newer*",
            "{cause}: the arrangement must make the captured window stale"
        );

        commit_body(&s, Some("'panel'"), PANEL_BODY);

        assert_eq!(
            raised(&s),
            None,
            "{cause}: a precondition is a refusal, not a raise"
        );
        assert!(
            !ok(&s),
            "{cause}: a \"panel\" commit that lands in a DOCUMENT window must run the \
             document preflight -- the relaxation is conditional on the placement really \
             being a panel"
        );
        assert!(
            reason(&s).contains("now shows another buffer"),
            "{cause}: and refuse on stale intent; got {:?}",
            reason(&s)
        );
        assert!(!ran(&s), "{cause}: the callback must not run");
        assert_eq!(
            name_in(&s, doc),
            "*newer*",
            "{cause}: the user's newer buffer must survive -- this is the assertion that \
             fails loudest when the guard is removed"
        );
    }
}

/// **N** — the case no preflight snapshot can catch: the **body itself**
/// creates the fallback, and the refusal still fires.
///
/// This is why the guarantee moved to the placement boundary. Revision 6
/// argued that a prediction taken at preflight could not go stale,
/// because `commit_to`'s body cannot `await`. Refusing `await` prevents
/// another *coroutine* interleaving; it places no restriction on the body
/// itself, which is arbitrary Lua running synchronously:
///
/// ```lua
/// pmacs.window.set_params(pmacs.window.panel(), { dedicated = true })
/// pmacs.window.display(result, { side = "bottom" })
/// ```
///
/// Two statements. The first invalidates the prediction, the second cashes
/// it in. The arrangement here is deliberately the **inverse** of the
/// preflight rows: an undedicated panel exists, so the prediction says
/// "this will land in the panel", the relaxation applies, and the body
/// runs. Only when placement resolves is the fallback a fact.
///
/// What it asserts, and why each is load-bearing:
///
/// * the body **did** run — otherwise the test would be re-proving the
///   preflight and this whole case would be untested;
/// * the refusal arrives as a **raise** from `display`, since the body was
///   already running and there is no `(false, reason)` left to return —
///   asserted on content, and it names both the fallback and the
///   stale-intent reason;
/// * `*newer*` is **still in the document window**. That is the actual
///   user-visible guarantee; everything above it is mechanism.
///
/// *Mutation:* delete the `fallback_commit_refusal` call from
/// `display_buffer`. This test fails on all three; every other test in
/// this file still passes, which is precisely the hole revision 6 left.
#[test]
fn a_panel_commit_whose_body_creates_the_fallback_is_refused_at_placement() {
    let s = editor();

    // A REUSABLE panel: undedicated, so the preflight prediction says
    // this frontend places side requests in the panel.
    exec(
        &s,
        "pmacs.window.display(pmacs.buffer.create('*pinned*'),
           { side = 'bottom', dedicated = false, select = false })",
    );
    capture(&s);
    let doc = local_window(&s);
    exec(
        &s,
        "pmacs.window.switch_buffer(pmacs.buffer.create('*newer*'))",
    );
    assert_eq!(
        name_in(&s, doc),
        "*newer*",
        "the arrangement must make the captured window stale"
    );

    commit_body(
        &s,
        Some("'panel'"),
        &format!(
            "pmacs.window.set_params(pmacs.window.panel(), {{ dedicated = true }})
             {PANEL_BODY}"
        ),
    );

    assert!(
        ran(&s),
        "the body must have run -- the preflight could not have known, and a test where \
         it did not run would be re-proving the preflight"
    );
    let raised = raised(&s).expect(
        "the refusal arrives as a raise: the body was already running, so there is no \
         (false, reason) return left to make",
    );
    assert!(
        raised.contains("fell back to a document window"),
        "the message must name what happened; got {raised:?}"
    );
    assert!(
        raised.contains("now shows another buffer"),
        "and which document precondition failed; got {raised:?}"
    );
    assert_eq!(
        name_in(&s, doc),
        "*newer*",
        "the user's newer buffer must survive -- this is the guarantee, and it is what a \
         preflight-only design cannot provide"
    );
}

/// **P** — a `"panel"` commit that falls back with a **still-valid**
/// destination lands in the document window, exactly as it does today.
///
/// The guard refuses on *staleness*, not on *falling back*. Falling back
/// is deliberate graceful degradation for a frontend that cannot render a
/// panel (`EditorCore::apply_placement`), and turning it into an error
/// would regress every consumer that works today on such a frontend — a
/// much bigger behaviour change than the defect being fixed.
///
/// Both causes, and asserted on **where the result landed** rather than
/// on the commit merely being accepted: a design that accepted the commit
/// and then dropped the display on the floor would pass a weaker version
/// of this.
///
/// *Mutation:* make `fallback_commit_refusal` refuse whenever a `"panel"`
/// commit falls back, instead of only when a document precondition fails.
/// Both rows fail here; every refusal test still passes, which is what
/// makes this the pin that stops the fix over-reaching.
#[test]
fn a_panel_commit_that_falls_back_with_a_valid_destination_still_lands() {
    for cause in ["not panel-capable", "side slot dedicated elsewhere"] {
        let s = editor();
        arrange_fallback(&s, cause);
        capture(&s);
        let doc = local_window(&s);

        // No staleness: the captured window still holds what it held.
        commit_body(&s, Some("'panel'"), PANEL_BODY);

        assert_eq!(raised(&s), None, "{cause}: the commit must not raise");
        assert!(
            ok(&s),
            "{cause}: a fallback with an intact destination is graceful degradation, not \
             an error; got refusal {:?}",
            reason(&s)
        );
        assert!(ran(&s), "{cause}: the callback must run");
        assert_eq!(
            name_in(&s, doc),
            "*result*",
            "{cause}: and the result really must land in the document window it fell \
             back to"
        );
    }
}

/// **P** — a `"panel"` commit that really lands in the panel still skips
/// checks 2–4.
///
/// The other half of the correction, and it is not optional coverage.
/// The cheapest way to close the fallback hole is to make the panel
/// profile run the document preflight unconditionally — which passes
/// every fallback row above while quietly collapsing the two profiles
/// into one, leaving the whole parameterization buying nothing and
/// `git.status` refused for a document-window change unrelated to where
/// its panel goes.
///
/// Deliberately arranged in the **same stale-intent state** the fallback
/// rows refuse on, so the only difference between this test and those is
/// whether the placement is really a panel. And it asserts *where* the
/// result went, not merely that the commit was accepted: an accepted
/// commit that still overwrote the document window would be the same
/// defect wearing a `true`.
///
/// *Mutation:* widen the relaxation's condition back — i.e. make
/// `panel_placement_can_fall_back` return `true` unconditionally, or run
/// the document preflight for every `"panel"` commit. This fails on the
/// refusal; the fallback rows above still pass. **This is the pin that
/// makes "collapse the two profiles into one" a visible design change
/// rather than a quiet implementation choice.**
#[test]
fn a_panel_commit_that_really_lands_in_the_panel_keeps_its_relaxation() {
    let s = editor();
    capture(&s);
    let doc = local_window(&s);

    // Exactly the state the fallback rows refuse on.
    exec(
        &s,
        "pmacs.window.switch_buffer(pmacs.buffer.create('*newer*'))",
    );

    commit_body(&s, Some("'panel'"), PANEL_BODY);

    assert!(
        ok(&s),
        "a panel-capable frontend with no dedicated side slot really places in the \
         panel, so checks 2-4 stay omitted; got refusal {:?}",
        reason(&s)
    );
    assert!(ran(&s), "and the callback must run");

    let panel = s
        .core
        .borrow()
        .side_window_for(FrontendId::LOCAL)
        .expect("the commit must have created the side window");
    assert_eq!(
        name_in(&s, panel),
        "*result*",
        "the result must land in the PANEL -- an accepted commit that fell back would \
         be the same defect with a `true` in front of it"
    );
    assert_eq!(
        name_in(&s, doc),
        "*newer*",
        "and the captured document window must be untouched"
    );
}

// ---------------------------------------------------------------------------
// §7 — the profile argument (Q#DC-5)
// ---------------------------------------------------------------------------

/// **P** — a two-argument `commit_to(dest, body)` takes the **document**
/// profile, so every caller written before the profile existed keeps all
/// four checks.
///
/// Witnessed by a check the panel profile omits — a stale buffer.
/// Asserting merely that the call does not error would pass on a legacy
/// call silently downgraded to the panel profile, which is the
/// regression that would quietly void Journey Stage 1a's guarantees for
/// dired and every future two-argument caller.
///
/// *Mutation:* default the profile to `Panel`. This fails;
/// `journey_acceptance` also fails, which is the point — the default is
/// what makes that suite's untouched pass a consequence of the signature
/// rather than of care.
#[test]
fn a_two_argument_commit_takes_the_document_profile() {
    let s = editor();
    capture(&s);
    exec(
        &s,
        "pmacs.window.switch_buffer(pmacs.buffer.create('*usurper*'))",
    );

    commit(&s, None);

    assert!(
        !ok(&s),
        "a two-argument commit must keep the stale-intent check"
    );
    assert!(
        reason(&s).contains("now shows another buffer"),
        "and refuse for that reason; got {:?}",
        reason(&s)
    );
    assert!(!ran(&s), "the callback must not run");
}

/// **N** — an explicit `nil` profile is the document profile, exactly as
/// omitting it is.
///
/// Witnessed separately from the two-argument case rather than assumed
/// equivalent: a Lua caller threading an optional variable produces
/// `commit_to(dest, body, nil)`, and a third behaviour there would stay
/// invisible until someone hit it in production.
///
/// *Mutation:* treat `Value::Nil` as an unrecognized profile. This
/// fails; the two-argument test above does not, because mlua supplies
/// `Nil` for a missing argument either way only if the binding asks for
/// a `Value` — which is the type this suite also pins below.
#[test]
fn an_explicit_nil_profile_is_the_document_profile() {
    let s = editor();
    capture(&s);
    exec(
        &s,
        "pmacs.window.switch_buffer(pmacs.buffer.create('*usurper*'))",
    );

    commit(&s, Some("nil"));

    assert_eq!(
        raised(&s),
        None,
        "an explicit nil must not be treated as a bad profile"
    );
    assert!(!ok(&s), "an explicit nil must keep the stale-intent check");
    assert!(
        reason(&s).contains("now shows another buffer"),
        "and refuse for that reason; got {:?}",
        reason(&s)
    );
}

/// **N** — an unrecognized profile is an ERROR naming the accepted
/// values, and a non-string profile is refused by the **same** message.
///
/// Two claims, one test, because their whole content is that they agree:
///
/// * a fallback to `"document"` would hand a caller different checks
///   than it asked for — the failure the parameterization exists to
///   prevent — so an unknown string raises;
/// * **this is the guard on the argument's type.** With
///   `profile: Option<String>` mlua rejects `42` and `{}` during
///   argument *conversion*, before the closure body runs, and the
///   message below becomes unreachable — the caller gets a generic
///   conversion error naming neither the rule nor the vocabulary. So the
///   number and table cases are asserted on the message's *content* and
///   against the string case's message, not merely on "an error
///   occurred".
///
/// **The `invalid utf-8` row is the same reachability class one layer
/// down.** A Lua string is a *byte* string, so `string.char(255)` is a
/// perfectly ordinary `Value::String` that a `to_str()` inside the body
/// still fails to convert — surfacing mlua's generic UTF-8 error before
/// the documented message is ever constructed. Accepting `Value` is not
/// enough on its own; the comparison has to be on bytes.
///
/// *Mutation:* retype the argument to `Option<String>`. The number and
/// table rows fail. *Second mutation:* compare via `name.to_str()?`. The
/// `invalid utf-8` row fails.
#[test]
fn a_bad_profile_is_refused_by_one_message_that_names_the_accepted_values() {
    let mut messages = Vec::new();
    for (label, profile) in [
        ("unknown string", "'documents'"),
        ("number", "42"),
        ("table", "{}"),
        ("boolean", "true"),
        ("invalid utf-8", "string.char(255)"),
    ] {
        let s = editor();
        capture(&s);
        commit(&s, Some(profile));

        let raised = raised(&s).unwrap_or_else(|| panic!("{label}: a bad profile must raise"));
        assert!(
            raised.contains("\"document\"") && raised.contains("\"panel\""),
            "{label}: the message must name both accepted values; got {raised:?}"
        );
        assert!(
            raised.contains("must be the string"),
            "{label}: and say a string was expected; got {raised:?}"
        );
        assert!(
            !ran(&s),
            "{label}: a bad profile must not reach the callback"
        );
        messages.push((label, raised));
    }

    let (_, first) = &messages[0];
    for (label, message) in &messages[1..] {
        assert_eq!(
            message, first,
            "{label}: a non-string profile must be refused by the SAME message as an \
             unrecognized one -- a different message means mlua rejected the value \
             during argument conversion, which is what `Option<String>` would do"
        );
    }
}

// ---------------------------------------------------------------------------
// §7 — no document window (Q#DC-4)
// ---------------------------------------------------------------------------

/// **N** — a frontend with no live document window still captures, and
/// the destination reports the absence.
///
/// Asserted as a *successful* capture rather than as `nil`: returning
/// `nil` here would push the adopter back onto ambient behaviour, which
/// is the P1a bug this lane removes. An adopter with nowhere to land
/// gets a refusal it can report; it does not get permission to guess.
///
/// The `window()` accessor reporting **nil** is the other half: the pair
/// is set or cleared together, so no consumer ever sees a window id
/// without the buffer that was captured with it.
///
/// *Mutation:* return `None` from `capture_view_destination` when
/// `primary_document_window` finds nothing. This fails on the capture
/// assertion inside the helper. *Second mutation:* keep `window` while
/// clearing `buffer`. This fails here.
#[test]
fn capture_succeeds_with_no_document_window() {
    let s = editor();
    attach_documentless_frontend(&s, DOCUMENTLESS);
    s.core.borrow_mut().active_frontend = DOCUMENTLESS;

    capture(&s);

    assert!(
        eval::<bool>(&s, "return dest:window() == nil"),
        "the document pair must be reported as ABSENT, not invented"
    );
}

/// **N** — on that destination a panel commit **succeeds** and a
/// document commit is **refused**, naming the missing window.
///
/// Both halves, because asserting only the refusal would pass on a
/// capture that refuses everything, and asserting only the success would
/// pass on one that checks nothing. Together they are Q#DC-4's decision:
/// the document pair is optional, and the profile is what decides
/// whether its absence matters.
///
/// The refusal is a `(false, reason)` like the other four rather than a
/// raise, so an adopter handles all five the same way.
///
/// *Mutation:* drop the `dest.window == None` arm. The document half
/// then commits against no window at all.
#[test]
fn a_panel_commit_succeeds_where_a_document_commit_is_refused() {
    let s = editor();
    attach_documentless_frontend(&s, DOCUMENTLESS);
    s.core.borrow_mut().active_frontend = DOCUMENTLESS;
    capture(&s);

    commit(&s, Some("'panel'"));
    assert!(
        ok(&s),
        "a panel result needs only a live frontend; got refusal {:?}",
        reason(&s)
    );
    assert!(ran(&s), "and its callback must run");

    commit(&s, None);
    assert_eq!(
        raised(&s),
        None,
        "the missing document window joins the preflight refusals rather than raising"
    );
    assert!(!ok(&s), "a document commit has nowhere to land");
    assert!(
        reason(&s).contains("no document window"),
        "and must say so; got {:?}",
        reason(&s)
    );
    assert!(!ran(&s), "and must not reach the callback");
}

// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and
// write into their real data root.
#[path = "common/iso.rs"]
mod iso;
