//! Web Fetch API family lowering: Response / Headers / Request method
//! calls + property getters, plus axios / blob / readable_stream.
//!
//! Extracted from `lower_call.rs` (#1099, part of #1097) — pure move,
//! no behavior change. Called before the generic
//! `lower_native_method_call` path so static factories
//! (`Response.json(v)`) also land here.

use anyhow::Result;
use perry_hir::Expr;

use super::get_raw_string_ptr;
use crate::expr::{lower_expr, nanbox_pointer_inline, nanbox_string_inline, unbox_to_i64, FnCtx};
use crate::nanbox::double_literal;
use crate::types::{DOUBLE, I64};

/// Dispatch for the Web Fetch API family: Response/Headers/Request
/// methods and property getters. Called before the generic
/// `lower_native_method_call` path so static factories
/// (`Response.json(v)`) also land here. Returns `Ok(None)` if the
/// (module, method) combination isn't handled.
///
/// Handle ABI note: Response/Headers/Request handles are plain numeric
/// doubles (ids into the runtime's registry), not NaN-boxed pointers.
/// Most runtime functions take the handle as f64; status/statusText/
/// ok/text/json take i64 and we convert via `fptosi`.
pub(in crate::lower_call) fn lower_fetch_native_method(
    ctx: &mut FnCtx<'_>,
    module: &str,
    method: &str,
    object: Option<&Expr>,
    args: &[Expr],
) -> Result<Option<String>> {
    // ── Response static factories (no receiver) ──
    if module == "fetch" && object.is_none() {
        match method {
            "static_json" => {
                let v = if !args.is_empty() {
                    lower_expr(ctx, &args[0])?
                } else {
                    double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))
                };
                let handle = ctx
                    .block()
                    .call(DOUBLE, "js_response_static_json", &[(DOUBLE, &v)]);
                return Ok(Some(handle));
            }
            "static_redirect" => {
                let url_ptr = if !args.is_empty() {
                    get_raw_string_ptr(ctx, &args[0])?
                } else {
                    "0".to_string()
                };
                let status = if args.len() >= 2 {
                    lower_expr(ctx, &args[1])?
                } else {
                    "302.0".to_string()
                };
                let handle = ctx.block().call(
                    DOUBLE,
                    "js_response_static_redirect",
                    &[(I64, &url_ptr), (DOUBLE, &status)],
                );
                return Ok(Some(handle));
            }
            _ => {}
        }
    }

    // ── axios: static method calls (axios.get/post/put/delete/patch) ──
    // Must be before the receiver guard — these are receiver-less calls.
    if module == "axios" && object.is_none() {
        let url_box = if !args.is_empty() {
            lower_expr(ctx, &args[0])?
        } else {
            double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))
        };
        let blk = ctx.block();
        let url_handle = unbox_to_i64(blk, &url_box);
        match method {
            "get" => {
                let promise = blk.call(I64, "js_axios_get", &[(I64, &url_handle)]);
                return Ok(Some(nanbox_pointer_inline(blk, &promise)));
            }
            "delete" => {
                let promise = blk.call(I64, "js_axios_delete", &[(I64, &url_handle)]);
                return Ok(Some(nanbox_pointer_inline(blk, &promise)));
            }
            "post" | "put" | "patch" => {
                // #598: pass the body as a NaN-boxed f64 instead of
                // unboxing to i64. Pre-fix the unbox produced a raw
                // pointer the runtime read as `*const StringHeader`
                // — for an object literal the pointer was a real
                // ObjectHeader, the runtime read its bytes as a
                // StringHeader (length / refcount / data prefix),
                // and the request body became `^@^B^@^@H...` (the
                // ObjectHeader struct followed by the first character
                // of the stringified field). The runtime side now
                // detects strings vs everything-else via the NaN-box
                // tag and routes through `js_json_stringify`.
                let body_box = if args.len() > 1 {
                    lower_expr(ctx, &args[1])?
                } else {
                    double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))
                };
                let rt_fn = match method {
                    "post" => "js_axios_post",
                    "put" => "js_axios_put",
                    _ => "js_axios_patch",
                };
                let promise =
                    ctx.block()
                        .call(I64, rt_fn, &[(I64, &url_handle), (DOUBLE, &body_box)]);
                return Ok(Some(nanbox_pointer_inline(ctx.block(), &promise)));
            }
            _ => {}
        }
    }

    // Everything below needs a receiver.
    let Some(recv) = object else {
        return Ok(None);
    };

    // ── Headers method dispatch ──
    if module == "Headers" {
        let h_handle = lower_expr(ctx, recv)?;
        match method {
            "set" | "append" => {
                if args.len() < 2 {
                    return Ok(Some(double_literal(0.0)));
                }
                let key_ptr = get_raw_string_ptr(ctx, &args[0])?;
                let val_ptr = get_raw_string_ptr(ctx, &args[1])?;
                ctx.block().call(
                    DOUBLE,
                    "js_headers_set",
                    &[(DOUBLE, &h_handle), (I64, &key_ptr), (I64, &val_ptr)],
                );
                return Ok(Some(double_literal(f64::from_bits(
                    crate::nanbox::TAG_UNDEFINED,
                ))));
            }
            "get" => {
                if args.is_empty() {
                    return Ok(Some(double_literal(0.0)));
                }
                let key_ptr = get_raw_string_ptr(ctx, &args[0])?;
                let str_ptr = ctx.block().call(
                    I64,
                    "js_headers_get",
                    &[(DOUBLE, &h_handle), (I64, &key_ptr)],
                );
                let blk = ctx.block();
                return Ok(Some(nanbox_string_inline(blk, &str_ptr)));
            }
            "has" => {
                if args.is_empty() {
                    return Ok(Some(double_literal(f64::from_bits(
                        crate::nanbox::TAG_FALSE,
                    ))));
                }
                let key_ptr = get_raw_string_ptr(ctx, &args[0])?;
                let out = ctx.block().call(
                    DOUBLE,
                    "js_headers_has",
                    &[(DOUBLE, &h_handle), (I64, &key_ptr)],
                );
                return Ok(Some(out));
            }
            "delete" => {
                if args.is_empty() {
                    return Ok(Some(double_literal(f64::from_bits(
                        crate::nanbox::TAG_UNDEFINED,
                    ))));
                }
                let key_ptr = get_raw_string_ptr(ctx, &args[0])?;
                ctx.block().call(
                    DOUBLE,
                    "js_headers_delete",
                    &[(DOUBLE, &h_handle), (I64, &key_ptr)],
                );
                return Ok(Some(double_literal(f64::from_bits(
                    crate::nanbox::TAG_UNDEFINED,
                ))));
            }
            "forEach" => {
                if args.is_empty() {
                    return Ok(Some(double_literal(0.0)));
                }
                let cb = lower_expr(ctx, &args[0])?;
                ctx.block().call(
                    DOUBLE,
                    "js_headers_for_each",
                    &[(DOUBLE, &h_handle), (DOUBLE, &cb)],
                );
                return Ok(Some(double_literal(f64::from_bits(
                    crate::nanbox::TAG_UNDEFINED,
                ))));
            }
            // `headers.keys()` / `.values()` / `.entries()` return arrays
            // sorted by header name (WHATWG Fetch spec). The arrays are
            // themselves iterable via the array Symbol.iterator, so
            // `for…of`, spread, and `Array.from` all work for free
            // (refs #576).
            "keys" => {
                let arr = ctx
                    .block()
                    .call(DOUBLE, "js_headers_keys", &[(DOUBLE, &h_handle)]);
                return Ok(Some(arr));
            }
            "values" => {
                let arr = ctx
                    .block()
                    .call(DOUBLE, "js_headers_values", &[(DOUBLE, &h_handle)]);
                return Ok(Some(arr));
            }
            "entries" => {
                let arr = ctx
                    .block()
                    .call(DOUBLE, "js_headers_entries", &[(DOUBLE, &h_handle)]);
                return Ok(Some(arr));
            }
            _ => return Ok(None),
        }
    }

    // ── Request property getters ──
    if module == "Request" {
        let h_handle = lower_expr(ctx, recv)?;
        match method {
            "url" => {
                let str_ptr = ctx
                    .block()
                    .call(I64, "js_request_get_url", &[(DOUBLE, &h_handle)]);
                let blk = ctx.block();
                return Ok(Some(nanbox_string_inline(blk, &str_ptr)));
            }
            "method" => {
                let str_ptr =
                    ctx.block()
                        .call(I64, "js_request_get_method", &[(DOUBLE, &h_handle)]);
                let blk = ctx.block();
                return Ok(Some(nanbox_string_inline(blk, &str_ptr)));
            }
            "body" => {
                let val = ctx
                    .block()
                    .call(DOUBLE, "js_request_get_body", &[(DOUBLE, &h_handle)]);
                return Ok(Some(val));
            }
            _ => return Ok(None),
        }
    }

    // ── Response methods / property getters ──
    if module == "fetch" {
        // Lower the receiver once. It's a NaN-boxed POINTER_TAG handle (Phase 1
        // of the handle-NaN-boxing unification, refs #421) — accessors unbox
        // via `handle_id` on entry, so codegen passes recv_handle through as
        // DOUBLE without any fptosi/bitcast conversion. May also be a chained
        // result from `.headers` / `.clone()` — those cases are recognised at
        // the Call callsite in lower_call.
        let recv_handle = lower_expr(ctx, recv)?;
        match method {
            "text" => {
                let blk = ctx.block();
                let promise = blk.call(I64, "js_fetch_response_text", &[(DOUBLE, &recv_handle)]);
                return Ok(Some(nanbox_pointer_inline(blk, &promise)));
            }
            "json" => {
                let blk = ctx.block();
                let promise = blk.call(I64, "js_fetch_response_json", &[(DOUBLE, &recv_handle)]);
                return Ok(Some(nanbox_pointer_inline(blk, &promise)));
            }
            "status" => {
                let blk = ctx.block();
                let status = blk.call(
                    DOUBLE,
                    "js_fetch_response_status",
                    &[(DOUBLE, &recv_handle)],
                );
                return Ok(Some(status));
            }
            "statusText" => {
                let blk = ctx.block();
                let str_ptr = blk.call(
                    I64,
                    "js_fetch_response_status_text",
                    &[(DOUBLE, &recv_handle)],
                );
                return Ok(Some(nanbox_string_inline(blk, &str_ptr)));
            }
            "ok" => {
                // js_fetch_response_ok returns 1.0 or 0.0 as f64. Map to
                // TAG_TRUE/TAG_FALSE so console.log prints "true"/"false".
                let blk = ctx.block();
                let raw = blk.call(DOUBLE, "js_fetch_response_ok", &[(DOUBLE, &recv_handle)]);
                let cmp = blk.fcmp("une", &raw, "0.0");
                let tagged = blk.select(
                    crate::types::I1,
                    &cmp,
                    I64,
                    crate::nanbox::TAG_TRUE_I64,
                    crate::nanbox::TAG_FALSE_I64,
                );
                return Ok(Some(blk.bitcast_i64_to_double(&tagged)));
            }
            "headers" => {
                let out =
                    ctx.block()
                        .call(DOUBLE, "js_response_get_headers", &[(DOUBLE, &recv_handle)]);
                return Ok(Some(out));
            }
            "clone" => {
                let out = ctx
                    .block()
                    .call(DOUBLE, "js_response_clone", &[(DOUBLE, &recv_handle)]);
                return Ok(Some(out));
            }
            "arrayBuffer" => {
                let blk = ctx.block();
                let promise = blk.call(I64, "js_response_array_buffer", &[(DOUBLE, &recv_handle)]);
                return Ok(Some(nanbox_pointer_inline(blk, &promise)));
            }
            "blob" => {
                let blk = ctx.block();
                let promise = blk.call(I64, "js_response_blob", &[(DOUBLE, &recv_handle)]);
                return Ok(Some(nanbox_pointer_inline(blk, &promise)));
            }
            // Issue #237: response.body — returns ReadableStream over the
            // buffered body bytes. Property access lowers as a zero-arg
            // method call here, same as response.headers above.
            "body" => {
                let h = ctx
                    .block()
                    .call(DOUBLE, "js_response_body", &[(DOUBLE, &recv_handle)]);
                return Ok(Some(h));
            }
            _ => return Ok(None),
        }
    }

    // ── Blob instance methods + property getters (issue #234) ──
    // The receiver is a numeric Blob handle (registry id) carried as f64,
    // mirroring the Response handle ABI. Locals are tagged blob::Blob via
    // `register_native_instance` in `destructuring.rs`.
    if module == "blob" {
        let recv_handle = lower_expr(ctx, recv)?;
        match method {
            "size" => {
                let blk = ctx.block();
                let n = blk.call(DOUBLE, "js_blob_size", &[(DOUBLE, &recv_handle)]);
                return Ok(Some(n));
            }
            "type" => {
                let str_ptr = ctx
                    .block()
                    .call(I64, "js_blob_type", &[(DOUBLE, &recv_handle)]);
                let blk = ctx.block();
                return Ok(Some(nanbox_string_inline(blk, &str_ptr)));
            }
            "arrayBuffer" => {
                let blk = ctx.block();
                let promise = blk.call(I64, "js_blob_array_buffer", &[(DOUBLE, &recv_handle)]);
                return Ok(Some(nanbox_pointer_inline(blk, &promise)));
            }
            "bytes" => {
                let blk = ctx.block();
                let promise = blk.call(I64, "js_blob_bytes", &[(DOUBLE, &recv_handle)]);
                return Ok(Some(nanbox_pointer_inline(blk, &promise)));
            }
            "text" => {
                let blk = ctx.block();
                let promise = blk.call(I64, "js_blob_text", &[(DOUBLE, &recv_handle)]);
                return Ok(Some(nanbox_pointer_inline(blk, &promise)));
            }
            "slice" => {
                // slice(start?, end?, type?) — missing numeric args use
                // canonical f64::NAN as sentinel; missing type uses null
                // pointer (0). Runtime `js_blob_slice` checks `is_nan()`
                // / `type_ptr.is_null()` to apply WHATWG defaults
                // (start=0, end=len, type="").
                let start = if !args.is_empty() {
                    lower_expr(ctx, &args[0])?
                } else {
                    double_literal(f64::NAN)
                };
                let end = if args.len() >= 2 {
                    lower_expr(ctx, &args[1])?
                } else {
                    double_literal(f64::NAN)
                };
                let type_ptr = if args.len() >= 3 {
                    get_raw_string_ptr(ctx, &args[2])?
                } else {
                    "0".to_string()
                };
                let new_handle = ctx.block().call(
                    DOUBLE,
                    "js_blob_slice",
                    &[
                        (DOUBLE, &recv_handle),
                        (DOUBLE, &start),
                        (DOUBLE, &end),
                        (I64, &type_ptr),
                    ],
                );
                return Ok(Some(new_handle));
            }
            // Issue #237: blob.stream() — returns ReadableStream over the
            // blob's bytes. Single-chunk; closes after one read.
            "stream" => {
                let h = ctx
                    .block()
                    .call(DOUBLE, "js_blob_stream", &[(DOUBLE, &recv_handle)]);
                return Ok(Some(h));
            }
            // Issue #1211: File-specific properties.  Plain Blob handles
            // resolve `name` to the empty string and `lastModified` to 0,
            // which matches Node's behavior for non-File Blobs (no
            // ambiguity from sharing the registry).
            "name" => {
                let str_ptr = ctx
                    .block()
                    .call(I64, "js_file_name", &[(DOUBLE, &recv_handle)]);
                let blk = ctx.block();
                return Ok(Some(nanbox_string_inline(blk, &str_ptr)));
            }
            "lastModified" => {
                let blk = ctx.block();
                let n = blk.call(DOUBLE, "js_file_last_modified", &[(DOUBLE, &recv_handle)]);
                return Ok(Some(n));
            }
            _ => return Ok(None),
        }
    }

    // ─────────────────────────────────────────────────────────────────
    // Web Streams API (issue #237)
    // The receivers are numeric registry-id handles carried as f64,
    // mirroring the Blob/Response handle ABI. Locals are tagged
    // (module, class_name) by `register_native_instance` in
    // `destructuring.rs`.
    // ─────────────────────────────────────────────────────────────────

    if module == "readable_stream" {
        let recv_handle_raw = lower_expr(ctx, recv)?;
        // Issue #562: subclass instances stash the handle id under
        // `__perry_stream_handle__`; bare numeric handles pass through
        // unchanged. Cheap (one runtime call) and applied uniformly so
        // the FFIs below see a clean registry id either way.
        let recv_handle = ctx.block().call(
            DOUBLE,
            "js_stream_unwrap_handle",
            &[(DOUBLE, &recv_handle_raw)],
        );
        match method {
            "getReader" => {
                let h = ctx.block().call(
                    DOUBLE,
                    "js_readable_stream_get_reader",
                    &[(DOUBLE, &recv_handle)],
                );
                return Ok(Some(h));
            }
            "cancel" => {
                let reason = if !args.is_empty() {
                    lower_expr(ctx, &args[0])?
                } else {
                    double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))
                };
                let blk = ctx.block();
                let promise = blk.call(
                    I64,
                    "js_readable_stream_cancel",
                    &[(DOUBLE, &recv_handle), (DOUBLE, &reason)],
                );
                return Ok(Some(nanbox_pointer_inline(blk, &promise)));
            }
            "tee" => {
                let h =
                    ctx.block()
                        .call(DOUBLE, "js_readable_stream_tee", &[(DOUBLE, &recv_handle)]);
                return Ok(Some(h));
            }
            "pipeTo" => {
                let dest_raw = if !args.is_empty() {
                    lower_expr(ctx, &args[0])?
                } else {
                    double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))
                };
                // Issue #562: `dest` may be a subclass instance — unwrap.
                let dest =
                    ctx.block()
                        .call(DOUBLE, "js_stream_unwrap_handle", &[(DOUBLE, &dest_raw)]);
                let blk = ctx.block();
                let promise = blk.call(
                    I64,
                    "js_readable_stream_pipe_to",
                    &[(DOUBLE, &recv_handle), (DOUBLE, &dest)],
                );
                return Ok(Some(nanbox_pointer_inline(blk, &promise)));
            }
            "pipeThrough" => {
                // pipeThrough(transform) — transform has .readable / .writable.
                // We need both sub-handles. Lower the transform once, then
                // call js_transform_stream_writable / _readable to extract.
                let transform_raw = if !args.is_empty() {
                    lower_expr(ctx, &args[0])?
                } else {
                    double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))
                };
                // Issue #562: `transform` may be a subclass instance — unwrap.
                let transform = ctx.block().call(
                    DOUBLE,
                    "js_stream_unwrap_handle",
                    &[(DOUBLE, &transform_raw)],
                );
                let writable = ctx.block().call(
                    DOUBLE,
                    "js_transform_stream_writable",
                    &[(DOUBLE, &transform)],
                );
                let readable = ctx.block().call(
                    DOUBLE,
                    "js_transform_stream_readable",
                    &[(DOUBLE, &transform)],
                );
                let new_h = ctx.block().call(
                    DOUBLE,
                    "js_readable_stream_pipe_through",
                    &[
                        (DOUBLE, &recv_handle),
                        (DOUBLE, &writable),
                        (DOUBLE, &readable),
                    ],
                );
                return Ok(Some(new_h));
            }
            "locked" => {
                let v = ctx.block().call(
                    DOUBLE,
                    "js_readable_stream_locked",
                    &[(DOUBLE, &recv_handle)],
                );
                return Ok(Some(v));
            }
            // ReadableStreamDefaultController on the same handle:
            "enqueue" => {
                let chunk = if !args.is_empty() {
                    lower_expr(ctx, &args[0])?
                } else {
                    double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))
                };
                let v = ctx.block().call(
                    DOUBLE,
                    "js_readable_stream_controller_enqueue",
                    &[(DOUBLE, &recv_handle), (DOUBLE, &chunk)],
                );
                return Ok(Some(v));
            }
            "close" => {
                let v = ctx.block().call(
                    DOUBLE,
                    "js_readable_stream_controller_close",
                    &[(DOUBLE, &recv_handle)],
                );
                return Ok(Some(v));
            }
            "error" => {
                let reason = if !args.is_empty() {
                    lower_expr(ctx, &args[0])?
                } else {
                    double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))
                };
                let v = ctx.block().call(
                    DOUBLE,
                    "js_readable_stream_controller_error",
                    &[(DOUBLE, &recv_handle), (DOUBLE, &reason)],
                );
                return Ok(Some(v));
            }
            "desiredSize" => {
                let v = ctx.block().call(
                    DOUBLE,
                    "js_readable_stream_controller_desired_size",
                    &[(DOUBLE, &recv_handle)],
                );
                return Ok(Some(v));
            }
            _ => return Ok(None),
        }
    }

    if module == "readable_stream_reader" {
        let recv_handle = lower_expr(ctx, recv)?;
        match method {
            "read" => {
                let blk = ctx.block();
                let promise = blk.call(I64, "js_reader_read", &[(DOUBLE, &recv_handle)]);
                return Ok(Some(nanbox_pointer_inline(blk, &promise)));
            }
            "releaseLock" => {
                let v =
                    ctx.block()
                        .call(DOUBLE, "js_reader_release_lock", &[(DOUBLE, &recv_handle)]);
                return Ok(Some(v));
            }
            "cancel" => {
                let reason = if !args.is_empty() {
                    lower_expr(ctx, &args[0])?
                } else {
                    double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))
                };
                let blk = ctx.block();
                let promise = blk.call(
                    I64,
                    "js_reader_cancel",
                    &[(DOUBLE, &recv_handle), (DOUBLE, &reason)],
                );
                return Ok(Some(nanbox_pointer_inline(blk, &promise)));
            }
            "closed" => {
                let blk = ctx.block();
                let promise = blk.call(I64, "js_reader_closed", &[(DOUBLE, &recv_handle)]);
                return Ok(Some(nanbox_pointer_inline(blk, &promise)));
            }
            _ => return Ok(None),
        }
    }

    if module == "writable_stream" {
        let recv_handle_raw = lower_expr(ctx, recv)?;
        // Issue #562: subclass instances unwrap to a numeric handle.
        let recv_handle = ctx.block().call(
            DOUBLE,
            "js_stream_unwrap_handle",
            &[(DOUBLE, &recv_handle_raw)],
        );
        match method {
            "getWriter" => {
                let h = ctx.block().call(
                    DOUBLE,
                    "js_writable_stream_get_writer",
                    &[(DOUBLE, &recv_handle)],
                );
                return Ok(Some(h));
            }
            "abort" => {
                let reason = if !args.is_empty() {
                    lower_expr(ctx, &args[0])?
                } else {
                    double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))
                };
                let blk = ctx.block();
                let promise = blk.call(
                    I64,
                    "js_writable_stream_abort",
                    &[(DOUBLE, &recv_handle), (DOUBLE, &reason)],
                );
                return Ok(Some(nanbox_pointer_inline(blk, &promise)));
            }
            "close" => {
                let blk = ctx.block();
                let promise = blk.call(I64, "js_writable_stream_close", &[(DOUBLE, &recv_handle)]);
                return Ok(Some(nanbox_pointer_inline(blk, &promise)));
            }
            "locked" => {
                let v = ctx.block().call(
                    DOUBLE,
                    "js_writable_stream_locked",
                    &[(DOUBLE, &recv_handle)],
                );
                return Ok(Some(v));
            }
            _ => return Ok(None),
        }
    }

    if module == "writable_stream_writer" {
        let recv_handle = lower_expr(ctx, recv)?;
        match method {
            "write" => {
                let chunk = if !args.is_empty() {
                    lower_expr(ctx, &args[0])?
                } else {
                    double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))
                };
                let blk = ctx.block();
                let promise = blk.call(
                    I64,
                    "js_writer_write",
                    &[(DOUBLE, &recv_handle), (DOUBLE, &chunk)],
                );
                return Ok(Some(nanbox_pointer_inline(blk, &promise)));
            }
            "close" => {
                let blk = ctx.block();
                let promise = blk.call(I64, "js_writer_close", &[(DOUBLE, &recv_handle)]);
                return Ok(Some(nanbox_pointer_inline(blk, &promise)));
            }
            "abort" => {
                let reason = if !args.is_empty() {
                    lower_expr(ctx, &args[0])?
                } else {
                    double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED))
                };
                let blk = ctx.block();
                let promise = blk.call(
                    I64,
                    "js_writer_abort",
                    &[(DOUBLE, &recv_handle), (DOUBLE, &reason)],
                );
                return Ok(Some(nanbox_pointer_inline(blk, &promise)));
            }
            "releaseLock" => {
                let v =
                    ctx.block()
                        .call(DOUBLE, "js_writer_release_lock", &[(DOUBLE, &recv_handle)]);
                return Ok(Some(v));
            }
            "closed" => {
                let blk = ctx.block();
                let promise = blk.call(I64, "js_writer_closed", &[(DOUBLE, &recv_handle)]);
                return Ok(Some(nanbox_pointer_inline(blk, &promise)));
            }
            "ready" => {
                let blk = ctx.block();
                let promise = blk.call(I64, "js_writer_ready", &[(DOUBLE, &recv_handle)]);
                return Ok(Some(nanbox_pointer_inline(blk, &promise)));
            }
            "desiredSize" => {
                let v =
                    ctx.block()
                        .call(DOUBLE, "js_writer_desired_size", &[(DOUBLE, &recv_handle)]);
                return Ok(Some(v));
            }
            _ => return Ok(None),
        }
    }

    if module == "transform_stream" {
        let recv_handle_raw = lower_expr(ctx, recv)?;
        // Issue #562: subclass instances unwrap to a numeric handle.
        let recv_handle = ctx.block().call(
            DOUBLE,
            "js_stream_unwrap_handle",
            &[(DOUBLE, &recv_handle_raw)],
        );
        match method {
            "readable" => {
                let v = ctx.block().call(
                    DOUBLE,
                    "js_transform_stream_readable",
                    &[(DOUBLE, &recv_handle)],
                );
                return Ok(Some(v));
            }
            "writable" => {
                let v = ctx.block().call(
                    DOUBLE,
                    "js_transform_stream_writable",
                    &[(DOUBLE, &recv_handle)],
                );
                return Ok(Some(v));
            }
            _ => return Ok(None),
        }
    }

    // ── axios: response property access (response.status, .data, .statusText, .headers) ──
    if module == "axios" {
        if let Some(recv) = object {
            let recv_handle = lower_expr(ctx, recv)?;
            let blk = ctx.block();
            // The awaited axios response is a Handle (i64) NaN-boxed via
            // `JsValue::from_object_ptr(handle as *mut ())` (POINTER_TAG |
            // (handle & POINTER_MASK)). Use `unbox_to_i64` to strip the
            // tag and recover the bare handle id; calling
            // `bitcast_double_to_i64` alone leaves the upper-16 tag bits
            // and the runtime's `get_handle::<AxiosResponseHandle>` lookup
            // misses, returning 0 / undefined for every property. (#604
            // followup — only surfaced once the listen() hang was fixed.)
            let h_i64 = unbox_to_i64(blk, &recv_handle);
            match method {
                "status" => {
                    let status = blk.call(DOUBLE, "js_axios_response_status", &[(I64, &h_i64)]);
                    return Ok(Some(status));
                }
                "statusText" => {
                    let str_ptr = blk.call(I64, "js_axios_response_status_text", &[(I64, &h_i64)]);
                    return Ok(Some(nanbox_string_inline(blk, &str_ptr)));
                }
                "data" => {
                    // Use the auto-parsed variant (JSON when the body
                    // looks like JSON, raw string otherwise) so
                    // `r.data.ok` / `r.data[0]` work the same way as
                    // in npm `axios`. The function returns a NaN-boxed
                    // f64 directly; no need to nanbox here. (#604
                    // followup — only surfaced once listen() hang fix
                    // unblocked the axios chain.)
                    let v = blk.call(DOUBLE, "js_axios_response_data_parsed", &[(I64, &h_i64)]);
                    return Ok(Some(v));
                }
                _ => {}
            }
        }
    }

    Ok(None)
}
