//! Folding acceptance (Arc 6, Stage 1 — docs/folding-framing.md).
//!
//! Headless coverage of the fold engine over real grammars: the
//! structural source (derived head line + closer-aware tail across brace
//! and indentation grammars, wrapped signatures, and injection layers),
//! the state-aware operations, the data-API validation, and the
//! dispatch-layer command-path pre-edit unfold. The `FoldState` producer
//! transitions are pinned in `src/semantic_render.rs`
//! (`fold_state_producer_transitions`).

use std::sync::Arc;

use pmacs::buffer::{Buffer, BufferId, EditOp};
use pmacs::editor::EditorState;
use pmacs::fold::{
    CycleOutcome, FoldStore, close_at, cycle_at, fold_target_at, open_at, top_level_fold_targets,
};
use pmacs::protocol::ByteRange;
use pmacs::syntax::{ParseTreeBundle, ParseView, SyntaxRegistry, run_parse};

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

/// Parse `src` under `lang` synchronously into a resolved bundle (mirrors
/// the crate-internal `parse_layered` with public APIs).
fn parse(reg: &SyntaxRegistry, lang: &str, src: &[u8]) -> Arc<ParseTreeBundle> {
    let language = reg.language(lang).expect("grammar loads");
    let mut buf = Buffer::from_bytes(BufferId::next(), "doc", src);
    let view = ParseView::new(&buf, language, lang.to_owned());
    let handle = view.handle();
    let _ = buf.attach_view(Box::new(view));
    let mut req = handle.make_request();
    req.injection_aliases = reg.injection_alias_snapshot();
    let bundle = run_parse(req).expect("parse succeeds");
    reg.resolve_layer_queries(&bundle)
}

/// The byte offset of `needle`'s first occurrence in `src`.
fn byte_of(src: &str, needle: &str) -> u64 {
    src.find(needle).expect("needle present") as u64
}

/// The content-end byte (position of the terminating `\n`, or EOF) of the
/// line containing `needle`'s first occurrence.
fn line_content_end_of(src: &str, needle: &str) -> u64 {
    let idx = src.find(needle).expect("needle present");
    let bytes = src.as_bytes();
    let mut e = idx;
    while e < bytes.len() && bytes[e] != b'\n' {
        e += 1;
    }
    e as u64
}

/// The text of the line containing byte `b`.
fn line_text_at(src: &str, b: u64) -> &str {
    let bytes = src.as_bytes();
    let b = (b as usize).min(bytes.len());
    let start = bytes[..b]
        .iter()
        .rposition(|&c| c == b'\n')
        .map_or(0, |i| i + 1);
    let end = bytes[b..]
        .iter()
        .position(|&c| c == b'\n')
        .map_or(bytes.len(), |i| b + i);
    &src[start..end]
}

// ---------------------------------------------------------------------------
// 1. Head line — both grammar shapes, wrapped headers (R2-1, R3-1).
// ---------------------------------------------------------------------------

#[test]
fn head_line_rust_single_line_signature() {
    let reg = SyntaxRegistry::new();
    let src = "fn foo() {\n    let x = 1;\n    let y = 2;\n}\n";
    let bundle = parse(&reg, "rust", src.as_bytes());
    let r = fold_target_at(&bundle, byte_of(src, "let x")).expect("foldable");
    assert_eq!(r.start, line_content_end_of(src, "fn foo() {"));
    assert_eq!(
        line_text_at(src, r.start),
        "fn foo() {",
        "head line is the fn line"
    );
    assert_eq!(
        line_text_at(src, r.start + 1),
        "    let x = 1;",
        "first hidden line"
    );
}

#[test]
fn head_line_rust_wrapped_signature_keeps_signature_visible() {
    // R3-1: rustfmt puts `{` on the `) -> bool {` line; the head must be
    // that line, NOT `fn foo(` — the wrapped signature stays visible.
    let reg = SyntaxRegistry::new();
    let src = "fn foo(\n    a: u32,\n) -> bool {\n    true\n}\n";
    let bundle = parse(&reg, "rust", src.as_bytes());
    let r = fold_target_at(&bundle, byte_of(src, "true")).expect("foldable");
    assert_eq!(r.start, line_content_end_of(src, ") -> bool {"));
    assert_eq!(line_text_at(src, r.start), ") -> bool {");
    // The two wrapped signature lines are before the fold → visible.
    assert!(r.start > line_content_end_of(src, "a: u32,"));
}

#[test]
fn head_line_python_uses_def_not_a_body_line() {
    // R2-1: tree-sitter-python's `block` starts on the first statement
    // line, so the head must ascend to `def foo():`, not `x = 1`.
    let reg = SyntaxRegistry::new();
    let src = "def foo():\n    x = 1\n    y = 2\n";
    let bundle = parse(&reg, "python", src.as_bytes());
    let r = fold_target_at(&bundle, byte_of(src, "x = 1")).expect("foldable");
    assert_eq!(r.start, line_content_end_of(src, "def foo():"));
    assert_eq!(line_text_at(src, r.start), "def foo():");
    assert_eq!(line_text_at(src, r.start + 1), "    x = 1");
}

#[test]
fn head_line_python_wrapped_signature_keeps_signature_visible() {
    let reg = SyntaxRegistry::new();
    let src = "def foo(\n    a,\n):\n    x = 1\n";
    let bundle = parse(&reg, "python", src.as_bytes());
    let r = fold_target_at(&bundle, byte_of(src, "x = 1")).expect("foldable");
    assert_eq!(r.start, line_content_end_of(src, "):"));
    assert_eq!(line_text_at(src, r.start), "):");
}

// ---------------------------------------------------------------------------
// 4. Range semantics — closer-aware tail (R2-5).
// ---------------------------------------------------------------------------

#[test]
fn brace_closer_line_stays_visible() {
    let reg = SyntaxRegistry::new();
    let src = "fn foo() {\n    let x = 1;\n    let y = 2;\n}\n";
    let bundle = parse(&reg, "rust", src.as_bytes());
    let r = fold_target_at(&bundle, byte_of(src, "let x")).expect("foldable");
    // Last hidden line is the last body line; the `}` line is outside.
    assert_eq!(r.end, line_content_end_of(src, "let y = 2;"));
    assert_eq!(
        line_text_at(src, r.end + 1),
        "}",
        "closer line stays visible"
    );
}

#[test]
fn shared_closer_line_else_stays_visible() {
    // R2-5: `} else {` keeps its trailing sibling on screen.
    let reg = SyntaxRegistry::new();
    let src = "fn f() {\n    if a {\n        one();\n    } else {\n        two();\n    }\n}\n";
    let bundle = parse(&reg, "rust", src.as_bytes());
    let r = fold_target_at(&bundle, byte_of(src, "one()")).expect("foldable");
    // The consequent block folds; its `} else {` line stays visible.
    assert_eq!(line_text_at(src, r.end + 1).trim(), "} else {");
}

#[test]
fn python_hides_through_last_body_line() {
    let reg = SyntaxRegistry::new();
    let src = "def foo():\n    x = 1\n    y = 2\n";
    let bundle = parse(&reg, "python", src.as_bytes());
    let r = fold_target_at(&bundle, byte_of(src, "x = 1")).expect("foldable");
    // Delimiter-less: the last body line is hidden (inside the range).
    assert_eq!(r.end, line_content_end_of(src, "y = 2"));
}

// ---------------------------------------------------------------------------
// 3. close-all folds top-level regions only; 9. nested + state-aware order.
// ---------------------------------------------------------------------------

#[test]
fn close_all_is_top_level_only() {
    let reg = SyntaxRegistry::new();
    let src = "fn a() {\n    if c {\n        work();\n        more();\n    }\n}\n\nfn b() {\n    x();\n    y();\n}\n";
    let bundle = parse(&reg, "rust", src.as_bytes());
    let top = top_level_fold_targets(&bundle);
    assert_eq!(
        top.len(),
        2,
        "two top-level fns, the nested `if` is not auto-folded"
    );
    // Neither top-level range is the inner `if` block.
    let inner = fold_target_at(&bundle, byte_of(src, "work()")).expect("inner foldable");
    assert!(
        !top.contains(&inner),
        "close-all does not fold the nested region"
    );
}

#[test]
fn nested_state_aware_ordering() {
    // 9 / R3-2: close walks innermost→outer, open walks outer→inner, and
    // toggle cycles close-inner → close-outer → open-all so every command
    // reaches the outer fold.
    let reg = SyntaxRegistry::new();
    let src = "fn outer() {\n    if cond {\n        work();\n        more();\n    }\n}\n";
    let bundle = parse(&reg, "rust", src.as_bytes());
    let p = byte_of(src, "work()");

    let mut store = FoldStore::new();
    let inner = close_at(&mut store, &bundle, p).expect("close inner");
    let outer = close_at(&mut store, &bundle, p).expect("close outer");
    assert!(
        inner.start > outer.start,
        "inner fold is more deeply nested"
    );
    assert!(
        close_at(&mut store, &bundle, p).is_none(),
        "nothing left to close"
    );
    assert_eq!(store.folds().len(), 2);

    assert_eq!(open_at(&mut store, p), Some(outer), "open outermost first");
    assert_eq!(open_at(&mut store, p), Some(inner), "then the inner");
    assert!(store.is_empty());

    assert!(matches!(
        cycle_at(&mut store, &bundle, p),
        CycleOutcome::Closed(_)
    ));
    assert!(matches!(
        cycle_at(&mut store, &bundle, p),
        CycleOutcome::Closed(_)
    ));
    assert_eq!(store.folds().len(), 2, "cycle closed both");
    assert!(matches!(
        cycle_at(&mut store, &bundle, p),
        CycleOutcome::OpenedAll(2)
    ));
    assert!(store.is_empty(), "one more cycle opened them all");
}

// ---------------------------------------------------------------------------
// 10. Injected layer (a fenced rust block inside markdown).
// ---------------------------------------------------------------------------

#[test]
fn fold_sourced_inside_injected_layer() {
    let reg = SyntaxRegistry::new();
    let src = "# Title\n\n```rust\nfn demo() {\n    let x = 1;\n    let y = 2;\n}\n```\n\nText.\n";
    let bundle = parse(&reg, "markdown", src.as_bytes());
    assert!(
        bundle.layers.len() >= 2,
        "markdown fence produced an injected rust layer"
    );
    let r = fold_target_at(&bundle, byte_of(src, "let x")).expect("foldable inside the fence");
    assert_eq!(
        line_text_at(src, r.start),
        "fn demo() {",
        "resolved the inner block"
    );
}

// ---------------------------------------------------------------------------
// 2. Stale / absent parse tree refuses (the precondition the binding keys on).
// ---------------------------------------------------------------------------

#[test]
fn absent_tree_has_no_current_bundle() {
    let reg = SyntaxRegistry::new();
    let language = reg.language("rust").expect("grammar");
    let src = b"fn foo() {\n    let x = 1;\n}\n";
    let buf = Buffer::from_bytes(BufferId::next(), "doc", src);
    let view = ParseView::new(&buf, language, "rust".to_owned());
    let handle = view.handle();
    // Before any parse installs, `current()` is None → the binding refuses.
    assert!(handle.current().is_none());
    let mut req = handle.make_request();
    req.injection_aliases = reg.injection_alias_snapshot();
    let bundle = run_parse(req).expect("parse");
    handle.install(reg.resolve_layer_queries(&bundle));
    assert!(
        handle.current().is_some(),
        "after install, a target is derivable"
    );
}

// ---------------------------------------------------------------------------
// 6. Command-path pre-edit unfold (Q#FD5).
// ---------------------------------------------------------------------------

fn active_id(s: &EditorState) -> BufferId {
    s.core.borrow().active_buffer_id()
}

fn insert_into(s: &EditorState, id: BufferId, text: &str) {
    let core = s.core.borrow();
    let mut reg = core.registry.borrow_mut();
    reg.get_mut(id)
        .unwrap()
        .apply_edit(EditOp::Insert {
            pos: 0,
            bytes: text.as_bytes(),
        })
        .unwrap();
}

#[test]
fn command_path_self_insert_unfolds_at_point() {
    let s = EditorState::new();
    let id = active_id(&s);
    insert_into(&s, id, "line0\nline1\nline2\nline3\n");
    // Fold the interior of lines 1..2: [end of line0, end of line2].
    let store = {
        let core = s.core.borrow();
        let mut reg = core.registry.borrow_mut();
        s.fold_registry.store_or_attach(reg.get_mut(id).unwrap())
    };
    store
        .lock()
        .unwrap()
        .insert(ByteRange { start: 5, end: 17 });
    // Cursor strictly inside the fold (start of "line2").
    s.core.borrow_mut().set_cursor_byte(12);

    // A command-path self-insert unfolds before the edit lands.
    s.core.borrow_mut().insert_char('x');
    assert!(
        store.lock().unwrap().is_empty(),
        "typing inside a fold unfolds it (Q#FD5)"
    );
}

#[test]
fn self_insert_at_head_line_end_does_not_unfold() {
    // `(start, end]` containment: a self-insert exactly at the end of the
    // head line (== range.start) is outside the fold — it must NOT unfold,
    // and the translator shifts the fold right so the char lands visible.
    let s = EditorState::new();
    let id = active_id(&s);
    insert_into(&s, id, "line0\nline1\nline2\nline3\n");
    let store = {
        let core = s.core.borrow();
        let mut reg = core.registry.borrow_mut();
        s.fold_registry.store_or_attach(reg.get_mut(id).unwrap())
    };
    store
        .lock()
        .unwrap()
        .insert(ByteRange { start: 5, end: 17 });
    s.core.borrow_mut().set_cursor_byte(5); // end of head line "line0"

    s.core.borrow_mut().insert_char('x');
    let folds = store.lock().unwrap().folds();
    assert_eq!(
        folds,
        vec![ByteRange { start: 6, end: 18 }],
        "fold shifts right; the character lands on the head line"
    );
}

// ---------------------------------------------------------------------------
// 5. Point moves to the head line when a fold is created around it (Q#FD3).
// 11. Data-API validation (Q#FD11) — driven through the Lua surface.
// ---------------------------------------------------------------------------

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

/// Install a settled rust parse over the active scratch buffer so the
/// `pmacs.fold` surface can drive it end to end.
fn install_rust_parse(s: &EditorState, id: BufferId) {
    let reg = &s.syntax_registry;
    let language = reg.language("rust").expect("grammar");
    let handle = {
        let core = s.core.borrow();
        let mut breg = core.registry.borrow_mut();
        let buf = breg.get_mut(id).unwrap();
        let view = ParseView::new(buf, language, "rust".to_owned());
        let handle = view.handle();
        buf.attach_view(Box::new(view));
        handle
    };
    let mut req = handle.make_request();
    req.injection_aliases = reg.injection_alias_snapshot();
    let bundle = run_parse(req).expect("parse");
    handle.install(reg.resolve_layer_queries(&bundle));
    reg.attach_view(id, handle);
}

#[test]
fn folding_moves_point_to_head_line() {
    let s = EditorState::new();
    let id = active_id(&s);
    let src = "fn foo() {\n    let x = 1;\n    let y = 2;\n}\n";
    insert_into(&s, id, src);
    install_rust_parse(&s, id);
    // Cursor inside the body; `let x` starts on line 1.
    let p = byte_of(src, "let x");
    s.core.borrow_mut().set_cursor_byte(p);

    exec(&s, "pmacs.command.invoke('fold.close')");

    let head = line_content_end_of(src, "fn foo() {");
    assert_eq!(
        s.core.borrow().active_window().cursor,
        head,
        "the invoking point moved to the head line"
    );
    // And the fold exists.
    let n: i64 = eval(&s, "return #pmacs.fold.folds(pmacs.window.buffer())");
    assert_eq!(n, 1);
}

#[test]
fn data_api_validation() {
    let s = EditorState::new();
    // A plain document buffer with four lines.
    exec(
        &s,
        "b = pmacs.buffer.from_bytes('doc.rs', 'aaa\\nbbb\\nccc\\nddd\\n')",
    );

    // A valid multi-line range folds.
    let ok: bool = eval(&s, "return pmacs.fold.fold(b, { start = 3, ['end'] = 11 })");
    assert!(ok, "a >=1-hidden-line range is accepted");
    let n: i64 = eval(&s, "return #pmacs.fold.folds(b)");
    assert_eq!(n, 1);

    // A sub-one-line range (both endpoints on the same line) is rejected.
    let same_line: bool = eval(&s, "return pmacs.fold.fold(b, { start = 0, ['end'] = 2 })");
    assert!(
        !same_line,
        "a range hiding no full line is rejected (Q#FD11)"
    );

    // An out-of-bounds range is rejected.
    let oob: bool = eval(
        &s,
        "return pmacs.fold.fold(b, { start = 0, ['end'] = 99999 })",
    );
    assert!(!oob, "an out-of-bounds range is rejected");

    // Q#FD9 via the >=1-hidden-line rule: a fold at (0,0) on an empty
    // buffer normalizes to zero hidden lines and is rejected.
    exec(&s, "e = pmacs.buffer.from_bytes('empty.rs', '')");
    let empty: bool = eval(&s, "return pmacs.fold.fold(e, { start = 0, ['end'] = 0 })");
    assert!(
        !empty,
        "a zero-length range is rejected (terminals never fold)"
    );

    // Round-trip: unfold the stored range clears it.
    let unfolded: bool = eval(
        &s,
        "local f = pmacs.fold.folds(b)[1]; return pmacs.fold.unfold(b, f)",
    );
    assert!(unfolded);
    let n2: i64 = eval(&s, "return #pmacs.fold.folds(b)");
    assert_eq!(n2, 0);
}

// ---------------------------------------------------------------------------
// 8. Buffer content replacement drops the store.
// ---------------------------------------------------------------------------

#[test]
fn forget_drops_store_and_detaches_view() {
    let s = EditorState::new();
    let id = active_id(&s);
    insert_into(&s, id, "line0\nline1\nline2\n");
    let store = {
        let core = s.core.borrow();
        let mut reg = core.registry.borrow_mut();
        s.fold_registry.store_or_attach(reg.get_mut(id).unwrap())
    };
    store
        .lock()
        .unwrap()
        .insert(ByteRange { start: 5, end: 11 });
    assert!(s.fold_registry.store(id).is_some());

    // Content replacement (revert/reload) drops the store.
    {
        let core = s.core.borrow();
        let mut reg = core.registry.borrow_mut();
        s.fold_registry.forget(reg.get_mut(id).unwrap());
    }
    assert!(s.fold_registry.store(id).is_none(), "the store is dropped");
    assert!(
        s.fold_registry.folds(id).is_empty(),
        "no folds survive the drop"
    );
}

#[test]
fn killing_a_buffer_through_the_real_path_purges_its_fold_store() {
    // Drive the production `pmacs.buffer.remove` route (not `forget_buffer`
    // directly), so the app-data stash + the `after_buffer_removed` branch
    // are what get exercised — deleting either would leave this red. The
    // assertion reads through the DEAD id on purpose: BufferIds are never
    // reused, so a stale id cannot alias a later buffer. Mirrors
    // config_registry's `killing_a_buffer_through_the_real_path_...`.
    let s = EditorState::new();
    exec(
        &s,
        "b = pmacs.buffer.from_bytes('kill.rs', 'aaa\\nbbb\\nccc\\nddd\\n')",
    );
    let id = {
        let core = s.core.borrow();
        let reg = core.registry.borrow();
        reg.find_by_name("kill.rs").expect("buffer")
    };
    // Fold via the data API (no parse tree needed) so the store exists.
    let folded: bool = eval(&s, "return pmacs.fold.fold(b, { start = 3, ['end'] = 11 })");
    assert!(folded);
    assert!(
        s.fold_registry.store(id).is_some(),
        "store exists before kill"
    );

    exec(&s, "pmacs.buffer.remove(b)");
    assert!(
        s.fold_registry.store(id).is_none(),
        "the real kill path purges the fold store"
    );
}

#[test]
fn close_all_command_moves_point_to_enclosing_head() {
    // Q#FD3 through the command surface: `fold.close-all` is interactive,
    // so when it collapses a top-level fold around the invoking point, the
    // point moves to that fold's head line (Finding 3, round 1).
    let s = EditorState::new();
    let id = active_id(&s);
    let src = "fn first() {\n    a();\n    b();\n}\nfn second() {\n    c();\n    d();\n}\n";
    insert_into(&s, id, src);
    install_rust_parse(&s, id);
    // Point inside the SECOND function's body.
    s.core.borrow_mut().set_cursor_byte(byte_of(src, "c()"));

    exec(&s, "pmacs.command.invoke('fold.close-all')");

    let n: i64 = eval(&s, "return #pmacs.fold.folds(pmacs.window.buffer())");
    assert_eq!(n, 2, "both top-level functions are folded");
    assert_eq!(
        s.core.borrow().active_window().cursor,
        line_content_end_of(src, "fn second() {"),
        "the invoking point moved to the enclosing fold's head line"
    );
}

#[test]
fn stale_parse_tree_refuses_fold() {
    // Q#FD10: an edit after the parse leaves `pending_edit_count() > 0`,
    // so a fold command refuses (the settled coordinates are stale).
    let s = EditorState::new();
    let id = active_id(&s);
    let src = "fn foo() {\n    let x = 1;\n    let y = 2;\n}\n";
    insert_into(&s, id, src);
    install_rust_parse(&s, id);
    // A further edit accumulates a pending edit on the attached parse view.
    insert_into(&s, id, "// stale\n");
    s.core.borrow_mut().set_cursor_byte(20);

    exec(&s, "pmacs.command.invoke('fold.close')");
    let n: i64 = eval(&s, "return #pmacs.fold.folds(pmacs.window.buffer())");
    assert_eq!(n, 0, "a stale parse tree refuses fold creation");
}

#[test]
fn read_only_buffer_is_rejected() {
    // Q#FD11's "normal document buffer" guard: terminals are read-only, so
    // a read-only buffer is not foldable.
    let s = EditorState::new();
    exec(
        &s,
        "b = pmacs.buffer.from_bytes('ro.rs', 'aaa\\nbbb\\nccc\\n')",
    );
    let id = {
        let core = s.core.borrow();
        let reg = core.registry.borrow();
        reg.find_by_name("ro.rs").expect("buffer")
    };
    {
        let core = s.core.borrow();
        let mut reg = core.registry.borrow_mut();
        reg.get_mut(id).unwrap().set_read_only(true);
    }
    let ok: bool = eval(&s, "return pmacs.fold.fold(b, { start = 3, ['end'] = 7 })");
    assert!(!ok, "a read-only buffer is rejected (Q#FD11)");
}

#[test]
fn unfold_normalizes_an_arbitrary_range_to_a_stored_fold() {
    let s = EditorState::new();
    exec(
        &s,
        "b = pmacs.buffer.from_bytes('doc.rs', 'aaa\\nbbb\\nccc\\nddd\\n')",
    );
    let folded: bool = eval(&s, "return pmacs.fold.fold(b, { start = 3, ['end'] = 11 })");
    assert!(folded);
    // A *different* input range that normalizes to the same stored fold.
    let unfolded: bool = eval(
        &s,
        "return pmacs.fold.unfold(b, { start = 1, ['end'] = 10 })",
    );
    assert!(unfolded, "unfold normalizes an arbitrary range");
    let n: i64 = eval(&s, "return #pmacs.fold.folds(b)");
    assert_eq!(n, 0);
}
