// pmacs_fake_lsp.rs --- Test helper. Tiny LSP-protocol echo peer.

//! Test helper binary, used only by `tests/m4_acceptance.rs` to
//! exercise the M4.5 LSP transport without depending on a real
//! language server being installed.
//!
//! Behaviour:
//!
//! * Reads `Content-Length`-framed JSON-RPC bodies from stdin.
//! * On `initialize`: replies with a minimal capabilities object.
//! * On `initialized`: silent.
//! * On `textDocument/didOpen` or `textDocument/didChange`: echoes
//!   the document URI back as a `pmacs/echo` notification so the
//!   test can verify the wire is alive, and pushes a synthetic
//!   `publishDiagnostics` so M4.6 tests have content to read.
//! * On `textDocument/completion`: returns a deterministic
//!   `CompletionList` with three items so M4.7 tests can read it.
//! * On `textDocument/hover`: returns a `MarkupContent`-shaped
//!   markdown payload.
//! * On `textDocument/signatureHelp`: returns a one-signature
//!   payload with two parameters and the second one active.
//! * On any other request: replies with `result: {"echo": params}`.
//! * On `shutdown`: replies with `null` and waits for `exit`.
//! * On `exit`: exits 0.
//! * If launched with `PMACS_FAKE_LSP_MODE=garbage`: writes
//!   intentionally malformed framing once and exits 0, so the
//!   client can verify protocol-violation handling.
//! * If launched with `PMACS_FAKE_LSP_MODE=crash`: replies to
//!   `initialize`, then exits with code 7 immediately, so the
//!   client can verify crash + restart handling.
//! * If launched with `PMACS_FAKE_LSP_MODE=rooturi`: writes the
//!   `rootUri` received in `initialize` to the file named by
//!   `PMACS_FAKE_LSP_ROOT_SINK`, so a test can assert the
//!   auto-attach path derives the project root from the opened file.

use std::collections::HashMap;
use std::io::{self, Read, Write};

#[allow(
    clippy::too_many_lines,
    reason = "linear LSP-method dispatch; splitting a test helper this much fragments the read"
)]
fn main() {
    let mode = std::env::var("PMACS_FAKE_LSP_MODE").unwrap_or_default();
    if mode == "garbage" {
        write_garbage();
        return;
    }
    let mut stdin = io::stdin().lock();
    let mut stdout = io::stdout().lock();
    let mut crashed_after_init = false;
    let mut open_docs: HashMap<String, String> = HashMap::new();
    loop {
        let body = match read_frame(&mut stdin) {
            Ok(Some(b)) => b,
            Ok(None) => return, // EOF
            Err(e) => {
                eprintln!("frame read error: {e}");
                return;
            }
        };
        let msg: serde_json::Value = match serde_json::from_slice(&body) {
            Ok(v) => v,
            Err(e) => {
                eprintln!("bad json: {e}");
                continue;
            }
        };
        let method = msg
            .get("method")
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .to_owned();
        let id = msg.get("id").cloned();
        let params = msg
            .get("params")
            .cloned()
            .unwrap_or(serde_json::Value::Null);
        // T M4.5 `wsconfig`: the client's reply to the
        // `workspace/configuration` request we sent at `initialized`
        // arrives here as a response (id 9001, has `result`, no
        // method). Echo its result array back as a notification so
        // the test can assert what pmacs answered.
        if mode == "wsconfig"
            && method.is_empty()
            && msg.get("result").is_some()
            && id.as_ref().and_then(serde_json::Value::as_u64) == Some(9001)
        {
            let echo = serde_json::json!({
                "jsonrpc": "2.0",
                "method": "pmacs/wsconfig",
                "params": { "answer": msg.get("result").cloned() }
            });
            write_frame(&mut stdout, &echo);
            continue;
        }
        // T M4.5 async-bridge failure-path test modes:
        //  * `error`  — answer every `textDocument/*` request with a
        //    JSON-RPC error object (drives `Handle:await()` -> failed).
        //  * `silent` — accept the request but never answer it while
        //    staying alive (drives the request-timeout sweep, and
        //    makes supersede deterministic: a superseded handle can
        //    only settle via cancellation, never racing a response).
        if method.starts_with("textDocument/") {
            if mode == "error" && id.is_some() {
                let resp = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": id.clone(),
                    "error": { "code": -32603, "message": "synthetic error" }
                });
                write_frame(&mut stdout, &resp);
                continue;
            }
            if mode == "silent" {
                continue;
            }
        }
        match (method.as_str(), id) {
            ("initialize", Some(idv)) => {
                let mut resp = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": idv,
                    "result": {
                        "capabilities": {
                            "textDocumentSync": 1,
                            "hoverProvider": true,
                            "completionProvider": { "triggerCharacters": ["."] },
                            "definitionProvider": true,
                            "inlayHintProvider": true,
                            "documentFormattingProvider": true,
                            "diagnosticProvider": { "interFileDependencies": false, "workspaceDiagnostics": false },
                            "semanticTokensProvider": {
                                "legend": {
                                    "tokenTypes": ["namespace", "function", "variable"],
                                    "tokenModifiers": ["declaration", "readonly"]
                                },
                                "full": true
                            }
                        },
                        "serverInfo": { "name": "pmacs-fake-lsp", "version": "0.1.0" }
                    }
                });
                // T M4.5 Option B: `posecho` negotiates UTF-16 and
                // echoes request positions back (see definition arm)
                // so a test can prove the byte↔UTF-16 round-trip.
                if mode == "posecho" {
                    resp["result"]["capabilities"]["positionEncoding"] =
                        serde_json::Value::from("utf-16");
                }
                // T M4.5: advertise prepareRename only in the
                // prepare-* modes, so the default `rename` mode keeps
                // exercising the no-prepare path.
                if mode == "prepare" || mode == "preprefuse" {
                    resp["result"]["capabilities"]["renameProvider"] =
                        serde_json::json!({ "prepareProvider": true });
                }
                // T M4.5 hardening `rooturi`: record the `rootUri` the
                // client sent in `initialize` to a side-channel file
                // (env `PMACS_FAKE_LSP_ROOT_SINK`). Lets a test prove
                // the auto-attach path derives the project root from
                // the opened file, not the editor's cwd. Mirrors the
                // `filewatch` mode's `.received` disk side-channel.
                if mode == "rooturi"
                    && let Ok(sink) = std::env::var("PMACS_FAKE_LSP_ROOT_SINK")
                {
                    let recorded = params
                        .get("rootUri")
                        .and_then(|v| v.as_str())
                        .unwrap_or("<null>");
                    let _ = std::fs::write(&sink, recorded);
                }
                write_frame(&mut stdout, &resp);
                if mode == "crash" {
                    crashed_after_init = true;
                }
            }
            // T M4.5 `wsconfig`: pull config the way gopls / pyright
            // / clangd do right after initialize.
            ("initialized", _) if mode == "wsconfig" => {
                let req = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": 9001,
                    "method": "workspace/configuration",
                    "params": { "items": [
                        { "section": "pmacs.probe" },
                        { "section": "does.not.exist" }
                    ] }
                });
                write_frame(&mut stdout, &req);
            }
            // T M4.5 `inlayrefresh` / `semantictokensrefresh`: right
            // after initialize, signal that cached inlay hints /
            // semantic tokens are stale via the matching server→client
            // refresh request. The client must answer (null) and
            // re-pull the corresponding `textDocument/*`.
            ("initialized", _) if mode == "inlayrefresh" => {
                let req = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": 9200,
                    "method": "workspace/inlayHint/refresh",
                    "params": serde_json::Value::Null
                });
                write_frame(&mut stdout, &req);
            }
            ("initialized", _) if mode == "semantictokensrefresh" => {
                let req = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": 9201,
                    "method": "workspace/semanticTokens/refresh",
                    "params": serde_json::Value::Null
                });
                write_frame(&mut stdout, &req);
            }
            // T M4.5 `filewatch`: dynamically register a
            // `workspace/didChangeWatchedFiles` watcher (RelativePattern
            // rooted at PMACS_FAKE_LSP_WATCH_BASE, `**/*.txt`, all
            // kinds). The client must reply null and start watching.
            ("initialized", _) if mode == "filewatch" => {
                let base = std::env::var("PMACS_FAKE_LSP_WATCH_BASE").unwrap_or_default();
                let req = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": 9300,
                    "method": "client/registerCapability",
                    "params": { "registrations": [{
                        "id": "watch-1",
                        "method": "workspace/didChangeWatchedFiles",
                        "registerOptions": { "watchers": [{
                            "globPattern": {
                                "baseUri": format!("file://{base}"),
                                "pattern": "**/*.txt"
                            },
                            "kind": 7
                        }] }
                    }] }
                });
                write_frame(&mut stdout, &req);
            }
            ("initialized", _) => {}
            // T M4.5: the client's file-watch notifications. Append
            // `type uri` lines to `<base>/.received` as a test
            // side-channel (the protocol stream is drained by the Lua
            // server-request pump, so a disk channel is observable).
            ("workspace/didChangeWatchedFiles", _) => {
                let base = std::env::var("PMACS_FAKE_LSP_WATCH_BASE").unwrap_or_default();
                if !base.is_empty()
                    && let Some(changes) = params.get("changes").and_then(|c| c.as_array())
                    && let Ok(mut f) = std::fs::OpenOptions::new()
                        .create(true)
                        .append(true)
                        .open(format!("{base}/.received"))
                {
                    use std::io::Write as _;
                    for ch in changes {
                        let t = ch
                            .get("type")
                            .and_then(serde_json::Value::as_i64)
                            .unwrap_or(0);
                        let u = ch
                            .get("uri")
                            .and_then(serde_json::Value::as_str)
                            .unwrap_or("");
                        let _ = writeln!(f, "{t} {u}");
                    }
                }
            }
            ("shutdown", Some(idv)) => {
                let resp = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": idv,
                    "result": serde_json::Value::Null
                });
                write_frame(&mut stdout, &resp);
            }
            ("exit", _) => return,
            ("textDocument/completion", Some(idv)) => {
                let resp = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": idv,
                    "result": {
                        "isIncomplete": false,
                        "items": [
                            {
                                "label": "println",
                                "kind": 3,
                                "detail": "macro println!",
                                "documentation": {
                                    "kind": "markdown",
                                    "value": "Prints to stdout with a newline."
                                },
                                "insertText": "println!"
                            },
                            {
                                "label": "print",
                                "kind": 3,
                                "detail": "macro print!",
                                "insertText": "print!"
                            },
                            {
                                "label": "panic",
                                "kind": 3,
                                "detail": "macro panic!",
                                "insertText": "panic!"
                            }
                        ]
                    }
                });
                write_frame(&mut stdout, &resp);
            }
            ("textDocument/hover", Some(idv)) => {
                let resp = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": idv,
                    "result": {
                        "contents": {
                            "kind": "markdown",
                            "value": "# pmacs-fake-lsp\n\nSynthetic hover content for the symbol under cursor."
                        },
                        "range": {
                            "start": { "line": 0, "character": 0 },
                            "end":   { "line": 0, "character": 4 }
                        }
                    }
                });
                write_frame(&mut stdout, &resp);
            }
            ("textDocument/signatureHelp", Some(idv)) => {
                let resp = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": idv,
                    "result": {
                        "signatures": [
                            {
                                "label": "fn echo(name: &str, count: usize) -> String",
                                "documentation": "Echoes `name` `count` times.",
                                "parameters": [
                                    { "label": "name: &str" },
                                    { "label": "count: usize" }
                                ],
                                "activeParameter": 1
                            }
                        ],
                        "activeSignature": 0,
                        "activeParameter": 1
                    }
                });
                write_frame(&mut stdout, &resp);
            }
            ("textDocument/didOpen" | "textDocument/didChange", _) => {
                let uri = params
                    .get("textDocument")
                    .and_then(|t| t.get("uri"))
                    .cloned()
                    .unwrap_or(serde_json::Value::Null);
                if let Some(uri_s) = uri.as_str() {
                    let text = if method == "textDocument/didOpen" {
                        params
                            .get("textDocument")
                            .and_then(|t| t.get("text"))
                            .and_then(serde_json::Value::as_str)
                    } else {
                        params
                            .get("contentChanges")
                            .and_then(serde_json::Value::as_array)
                            .and_then(|a| a.first())
                            .and_then(|c| c.get("text"))
                            .and_then(serde_json::Value::as_str)
                    };
                    if let Some(text) = text {
                        open_docs.insert(uri_s.to_owned(), text.to_owned());
                    }
                }
                let echo = serde_json::json!({
                    "jsonrpc": "2.0",
                    "method": "pmacs/echo",
                    "params": {
                        "method": method,
                        "uri": uri,
                    }
                });
                write_frame(&mut stdout, &echo);
                // Also push a synthetic `publishDiagnostics`
                // notification with two entries (one Error, one
                // Warning) so M4.6 tests can exercise the store.
                if uri.is_string() {
                    let diags = serde_json::json!({
                        "jsonrpc": "2.0",
                        "method": "textDocument/publishDiagnostics",
                        "params": {
                            "uri": uri,
                            "diagnostics": [
                                {
                                    "range": {
                                        "start": { "line": 0, "character": 4 },
                                        "end":   { "line": 0, "character": 8 },
                                    },
                                    "severity": 1,
                                    "message": "synthetic error",
                                    "source": "pmacs-fake-lsp",
                                    "code": "E0001"
                                },
                                {
                                    "range": {
                                        "start": { "line": 2, "character": 0 },
                                        "end":   { "line": 2, "character": 5 },
                                    },
                                    "severity": 2,
                                    "message": "synthetic warning",
                                    "source": "pmacs-fake-lsp",
                                    "code": "W0001"
                                }
                            ]
                        }
                    });
                    write_frame(&mut stdout, &diags);
                }
            }
            ("textDocument/definition", Some(idv)) => {
                let uri = params
                    .get("textDocument")
                    .and_then(|t| t.get("uri"))
                    .cloned()
                    .unwrap_or(serde_json::Value::Null);
                // `posecho`: echo the request's own position back as
                // the range (so the stored byte offset round-trips iff
                // encode∘decode is correct) AND stamp the *wire*
                // `character` the client sent into the result `uri` as
                // `pos:N` (so the test can see the intermediate UTF-16
                // value and prove the outbound encode was non-identity;
                // the `uri` string is not a Position so the client's
                // inbound rewrite leaves it untouched).
                let (uri, range) = if mode == "posecho" {
                    let pos = params
                        .get("position")
                        .cloned()
                        .unwrap_or(serde_json::Value::Null);
                    let ch = pos
                        .get("character")
                        .and_then(serde_json::Value::as_u64)
                        .unwrap_or(0);
                    (
                        serde_json::Value::from(format!("pos:{ch}")),
                        serde_json::json!({ "start": pos, "end": pos }),
                    )
                } else if mode == "defenv" {
                    // T M4.5 L1 cross-file: point the definition at a
                    // *different* file URI supplied via env, so the
                    // client must decode the URI, open-or-reuse that
                    // buffer, and reposition (SP-4 Gap A path).
                    let target = std::env::var("PMACS_FAKE_LSP_DEF_URI").unwrap_or_default();
                    (
                        serde_json::Value::from(target),
                        serde_json::json!({
                            "start": { "line": 2, "character": 0 },
                            "end":   { "line": 2, "character": 3 }
                        }),
                    )
                } else {
                    (
                        uri,
                        serde_json::json!({
                            "start": { "line": 7, "character": 4 },
                            "end":   { "line": 7, "character": 9 }
                        }),
                    )
                };
                let resp = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": idv,
                    "result": [{ "uri": uri, "range": range }]
                });
                write_frame(&mut stdout, &resp);
            }
            // T M4.5 Location-shaped nav. Distinct line per method so
            // a test can confirm each routes into its own kind slot.
            (
                m @ ("textDocument/references"
                | "textDocument/declaration"
                | "textDocument/typeDefinition"
                | "textDocument/implementation"),
                Some(idv),
            ) => {
                let uri = params
                    .get("textDocument")
                    .and_then(|t| t.get("uri"))
                    .cloned()
                    .unwrap_or(serde_json::Value::Null);
                let line = match m {
                    "textDocument/references" => 11,
                    "textDocument/declaration" => 21,
                    "textDocument/typeDefinition" => 31,
                    _ => 41, // implementation
                };
                let resp = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": idv,
                    "result": [{
                        "uri": uri,
                        "range": {
                            "start": { "line": line, "character": 2 },
                            "end":   { "line": line, "character": 6 }
                        }
                    }]
                });
                write_frame(&mut stdout, &resp);
            }
            ("textDocument/formatting", Some(idv)) => {
                // Synthetic two-edit reply: trim leading whitespace on
                // line 0 and append a semicolon at line 3, col 7.
                let resp = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": idv,
                    "result": [
                        {
                            "range": {
                                "start": { "line": 0, "character": 0 },
                                "end":   { "line": 0, "character": 4 }
                            },
                            "newText": ""
                        },
                        {
                            "range": {
                                "start": { "line": 3, "character": 7 },
                                "end":   { "line": 3, "character": 7 }
                            },
                            "newText": ";"
                        }
                    ]
                });
                write_frame(&mut stdout, &resp);
            }
            ("textDocument/prepareRename", Some(idv)) => {
                // T M4.5: `preprefuse` → null (not renameable here);
                // otherwise the `{ range, placeholder }` shape over
                // the line-0 cols 3..6 span ("foo").
                let result = if mode == "preprefuse" {
                    serde_json::Value::Null
                } else {
                    serde_json::json!({
                        "range": {
                            "start": { "line": 0, "character": 3 },
                            "end":   { "line": 0, "character": 6 }
                        },
                        "placeholder": "foo"
                    })
                };
                let resp = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": idv,
                    "result": result
                });
                write_frame(&mut stdout, &resp);
            }
            ("textDocument/rename", Some(idv)) => {
                // T M4.5 L2: reply with a `WorkspaceEdit`. The edit
                // replaces the 3-char span at line 0, cols 3..6 with
                // the requested `newName` (so the test can assert the
                // buffer text changed). In `rename` mode a *second*
                // file URI is taken from `PMACS_FAKE_LSP_RENAME_URI`
                // and given the same edit, plus a `create` resource
                // op — exercising the cross-file applier and the
                // unsupported-op count. Otherwise the edit is
                // single-file (the request's own document).
                let uri = params
                    .get("textDocument")
                    .and_then(|t| t.get("uri"))
                    .cloned()
                    .unwrap_or(serde_json::Value::Null);
                let new_name = params
                    .get("newName")
                    .and_then(serde_json::Value::as_str)
                    .unwrap_or("renamed")
                    .to_owned();
                let edit = serde_json::json!([{
                    "range": {
                        "start": { "line": 0, "character": 3 },
                        "end":   { "line": 0, "character": 6 }
                    },
                    "newText": new_name
                }]);
                let workspace_edit = if mode == "rename" {
                    let second = std::env::var("PMACS_FAKE_LSP_RENAME_URI").unwrap_or_default();
                    serde_json::json!({
                        "documentChanges": [
                            {
                                "textDocument": { "uri": uri, "version": 1 },
                                "edits": edit.clone()
                            },
                            {
                                "textDocument": { "uri": second, "version": 1 },
                                "edits": edit.clone()
                            }
                        ]
                    })
                } else {
                    let mut changes = serde_json::Map::new();
                    changes.insert(uri.as_str().unwrap_or("").to_owned(), edit);
                    serde_json::json!({ "changes": changes })
                };
                let resp = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": idv,
                    "result": workspace_edit
                });
                write_frame(&mut stdout, &resp);
            }
            ("textDocument/codeAction", Some(idv)) => {
                // T M4.5 L3: two actions — one with an inline edit
                // (replace line-0 cols 3..6 with "ED1"), one that is
                // command-only (the client must `executeCommand` it,
                // and we then drive the change via a server→client
                // `applyEdit`). The command carries the document URI
                // as its argument so the executeCommand arm knows
                // what to edit.
                let uri = params
                    .get("textDocument")
                    .and_then(|t| t.get("uri"))
                    .cloned()
                    .unwrap_or(serde_json::Value::Null);
                let mut changes = serde_json::Map::new();
                changes.insert(
                    uri.as_str().unwrap_or("").to_owned(),
                    serde_json::json!([{
                        "range": {
                            "start": { "line": 0, "character": 3 },
                            "end":   { "line": 0, "character": 6 }
                        },
                        "newText": "ED1"
                    }]),
                );
                // Command action first so a "apply the first action"
                // client drives the executeCommand→applyEdit path;
                // the inline-edit action second still exercises the
                // CodeAction.edit normalisation in the store.
                let resp = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": idv,
                    "result": [
                        {
                            "title": "Run server command",
                            "kind": "refactor",
                            "command": {
                                "title": "Run",
                                "command": "pmacs.fake.applyEdit",
                                "arguments": [uri]
                            }
                        },
                        {
                            "title": "Inline fix",
                            "kind": "quickfix",
                            "edit": { "changes": changes }
                        }
                    ]
                });
                write_frame(&mut stdout, &resp);
            }
            ("workspace/executeCommand", Some(idv)) => {
                // T M4.5 L3: the real edit is delivered out of band
                // via a server→client `workspace/applyEdit` request
                // (id 9100), exactly as rust-analyzer et al. do.
                // Replace line-1 cols 0..3 with "ED2". Then answer
                // the original executeCommand with a null result; the
                // client's reply to 9100 lands in the default arm and
                // is ignored.
                let cmd = params
                    .get("command")
                    .and_then(serde_json::Value::as_str)
                    .unwrap_or("");
                if cmd == "pmacs.fake.applyEdit" {
                    let target = params
                        .get("arguments")
                        .and_then(serde_json::Value::as_array)
                        .and_then(|a| a.first())
                        .cloned()
                        .unwrap_or(serde_json::Value::Null);
                    // T M4.5 L4 `resourceops`: deliver an ordered
                    // documentChanges that creates a file, fills it
                    // (create-before-edit ordering), renames a
                    // sibling, and deletes another — paths derived
                    // from the request URI's directory so the test
                    // doesn't have to thread them through env.
                    let we = if mode == "resourceops" {
                        let s = target.as_str().unwrap_or("");
                        let base = match s.rfind('/') {
                            Some(i) => &s[..=i],
                            None => "",
                        };
                        let created = format!("{base}created.rs");
                        let b = format!("{base}b.rs");
                        let b2 = format!("{base}b2.rs");
                        let c = format!("{base}c.rs");
                        serde_json::json!({
                            "documentChanges": [
                                { "kind": "create", "uri": created },
                                {
                                    "textDocument": { "uri": created, "version": 1 },
                                    "edits": [{
                                        "range": {
                                            "start": { "line": 0, "character": 0 },
                                            "end":   { "line": 0, "character": 0 }
                                        },
                                        "newText": "NEW"
                                    }]
                                },
                                { "kind": "rename", "oldUri": b, "newUri": b2 },
                                { "kind": "delete", "uri": c }
                            ]
                        })
                    } else {
                        serde_json::json!({
                            "documentChanges": [{
                                "textDocument": { "uri": target, "version": 1 },
                                "edits": [{
                                    "range": {
                                        "start": { "line": 1, "character": 0 },
                                        "end":   { "line": 1, "character": 3 }
                                    },
                                    "newText": "ED2"
                                }]
                            }]
                        })
                    };
                    let apply = serde_json::json!({
                        "jsonrpc": "2.0",
                        "id": 9100,
                        "method": "workspace/applyEdit",
                        "params": { "label": "fake refactor", "edit": we }
                    });
                    write_frame(&mut stdout, &apply);
                }
                let resp = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": idv,
                    "result": serde_json::Value::Null
                });
                write_frame(&mut stdout, &resp);
            }
            ("textDocument/semanticTokens/full", Some(idv)) => {
                // T M4.5: relative-encoded `data`. Three tokens:
                //   [0,0,4,1,1]  line 0 col 0 len 4, function, decl
                //   [0,5,3,2,0]  same line col 5 len 3, variable
                //   [2,2,7,0,2]  +2 lines col 2 len 7, namespace, ro
                let resp = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": idv,
                    "result": {
                        "resultId": "rid-1",
                        "data": [0, 0, 4, 1, 1, 0, 5, 3, 2, 0, 2, 2, 7, 0, 2]
                    }
                });
                write_frame(&mut stdout, &resp);
            }
            ("textDocument/semanticTokens/range", Some(idv)) => {
                // T M4.5: same shape as /full, scoped to a range.
                // One token: line 1 col 0 len 3, variable.
                let resp = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": idv,
                    "result": { "resultId": "rid-range", "data": [1, 0, 3, 2, 0] }
                });
                write_frame(&mut stdout, &resp);
            }
            ("textDocument/semanticTokens/full/delta", Some(idv)) => {
                // T M4.5: a `SemanticTokensDelta` over the /full data
                // `[0,0,4,1,1, 0,5,3,2,0, 2,2,7,0,2]` — replace the
                // last 5-int group (idx 10..15) with [3,0,9,1,0], so
                // token 3 becomes line 3 col 0 len 9, function.
                let resp = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": idv,
                    "result": {
                        "resultId": "rid-2",
                        "edits": [
                            { "start": 10, "deleteCount": 5, "data": [3, 0, 9, 1, 0] }
                        ]
                    }
                });
                write_frame(&mut stdout, &resp);
            }
            ("textDocument/inlayHint", Some(idv)) => {
                if mode == "inlaybounds"
                    && let Some(message) = inlay_range_error(&params, &open_docs)
                {
                    let resp = serde_json::json!({
                        "jsonrpc": "2.0",
                        "id": idv,
                        "error": { "code": -32603, "message": message }
                    });
                    write_frame(&mut stdout, &resp);
                    continue;
                }
                // T M4.5: a type hint (string label, kind 1) and a
                // parameter hint (label *parts*, kind 2) so both
                // label shapes are exercised.
                let resp = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": idv,
                    "result": [
                        {
                            "position": { "line": 0, "character": 9 },
                            "label": ": i32",
                            "kind": 1,
                            "paddingLeft": false,
                            "paddingRight": false,
                            "tooltip": "inferred type"
                        },
                        {
                            "position": { "line": 1, "character": 4 },
                            "label": [ { "value": "count" }, { "value": ":" } ],
                            "kind": 2,
                            "paddingLeft": false,
                            "paddingRight": true
                        }
                    ]
                });
                write_frame(&mut stdout, &resp);
            }
            // T M4.5 symbols/highlight. documentSymbol returns the
            // *hierarchical* DocumentSymbol shape (exercises tree
            // flatten + depth + parent); workspace/symbol the flat
            // SymbolInformation shape (exercises location.uri);
            // documentHighlight a two-occurrence list.
            ("textDocument/documentSymbol", Some(idv)) => {
                let resp = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": idv,
                    "result": [{
                        "name": "Outer", "kind": 5,
                        "range": { "start": { "line": 1, "character": 0 }, "end": { "line": 9, "character": 0 } },
                        "selectionRange": { "start": { "line": 1, "character": 6 }, "end": { "line": 1, "character": 11 } },
                        "children": [{
                            "name": "inner", "kind": 6,
                            "range": { "start": { "line": 3, "character": 2 }, "end": { "line": 5, "character": 2 } },
                            "selectionRange": { "start": { "line": 3, "character": 7 }, "end": { "line": 3, "character": 12 } }
                        }]
                    }]
                });
                write_frame(&mut stdout, &resp);
            }
            ("workspace/symbol", Some(idv)) => {
                let resp = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": idv,
                    "result": [{
                        "name": "WsThing", "kind": 12,
                        "location": {
                            "uri": "file:///ws.rs",
                            "range": { "start": { "line": 7, "character": 3 }, "end": { "line": 7, "character": 10 } }
                        },
                        "containerName": "modw"
                    }]
                });
                write_frame(&mut stdout, &resp);
            }
            ("textDocument/documentHighlight", Some(idv)) => {
                let resp = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": idv,
                    "result": [
                        { "range": { "start": { "line": 2, "character": 4 }, "end": { "line": 2, "character": 9 } }, "kind": 2 },
                        { "range": { "start": { "line": 6, "character": 0 }, "end": { "line": 6, "character": 5 } } }
                    ]
                });
                write_frame(&mut stdout, &resp);
            }
            (_, Some(idv)) => {
                // Generic echo response.
                let resp = serde_json::json!({
                    "jsonrpc": "2.0",
                    "id": idv,
                    "result": { "echo": params, "method": method }
                });
                write_frame(&mut stdout, &resp);
            }
            _ => {}
        }
        if crashed_after_init {
            // Honour the test's request to die after the first
            // useful exchange.
            std::process::exit(7);
        }
    }
}

fn read_frame<R: Read>(r: &mut R) -> io::Result<Option<Vec<u8>>> {
    // Read header byte-by-byte until \r\n\r\n, then read body.
    let mut header = Vec::new();
    let mut window = [0u8; 4];
    let mut filled = 0usize;
    loop {
        let mut byte = [0u8; 1];
        match r.read(&mut byte) {
            Ok(0) => return Ok(None),
            Ok(_) => {}
            Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
            Err(e) => return Err(e),
        }
        header.push(byte[0]);
        if filled < 4 {
            window[filled] = byte[0];
            filled += 1;
        } else {
            window.copy_within(1..4, 0);
            window[3] = byte[0];
        }
        if &window == b"\r\n\r\n" {
            break;
        }
    }
    let header_str =
        std::str::from_utf8(&header).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
    let mut content_length: Option<usize> = None;
    for line in header_str.split("\r\n") {
        if line.is_empty() {
            continue;
        }
        if let Some((k, v)) = line.split_once(':')
            && k.trim().eq_ignore_ascii_case("content-length")
        {
            content_length = v.trim().parse().ok();
        }
    }
    let n = content_length
        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing Content-Length"))?;
    let mut body = vec![0u8; n];
    r.read_exact(&mut body)?;
    Ok(Some(body))
}

fn write_frame<W: Write>(w: &mut W, body: &serde_json::Value) {
    let bytes = serde_json::to_vec(body).expect("json serialize");
    let _ = write!(w, "Content-Length: {}\r\n\r\n", bytes.len());
    let _ = w.write_all(&bytes);
    let _ = w.flush();
}

fn write_garbage() {
    let mut stdout = io::stdout().lock();
    let _ = stdout.write_all(b"NotAValidLspFrame\r\nGarbageHeader\r\n\r\n{}");
    let _ = stdout.flush();
}

fn document_end_position(text: &str) -> (u64, u64) {
    let mut line = 0;
    let mut col = 0;
    for byte in text.bytes() {
        if byte == b'\n' {
            line += 1;
            col = 0;
        } else {
            col += 1;
        }
    }
    (line, col)
}

fn inlay_range_error(
    params: &serde_json::Value,
    open_docs: &HashMap<String, String>,
) -> Option<String> {
    let uri = params
        .get("textDocument")
        .and_then(|t| t.get("uri"))
        .and_then(serde_json::Value::as_str)?;
    let text = open_docs.get(uri)?;
    let (last_line, last_col) = document_end_position(text);
    let end = params.get("range")?.get("end")?;
    let line = end.get("line")?.as_u64()?;
    let col = end.get("character")?.as_u64()?;
    if line > last_line || (line == last_line && col > last_col) {
        Some(format!(
            "invalid inlay range end {line}:{col}; document ends at {last_line}:{last_col}"
        ))
    } else {
        None
    }
}
