// Host-side FUSE request dispatcher.
//
// Receives a parsed (header, payload) FUSE request and produces a
// reply byte slice. The dispatcher is virtio-agnostic: any transport
// that delivers `(InHeader, payload)` pairs and accepts byte-slice
// replies can drive it. virtio-fs (`devices::virtio::fs`) is one
// transport; a future shared daemon's Unix-socket transport will be
// another.
//
// Per-request flow:
//   1. `dispatch` decodes the opcode, calls the matching handler.
//   2. Handlers receive `&self` (for backend access) + the request
//      header + the payload bytes. They build a `Reply` consisting
//      of OutHeader + opcode-specific bytes.
//   3. The transport writes Reply::bytes verbatim into its reply
//      buffer (for virtio-fs: across writable descriptors).
//
// Threading: handlers are pure functions of (request, &FsBackend);
// the only mutable state on the server itself is the negotiation
// snapshot (set once by INIT, read-mostly afterward). We hold a
// Mutex around the negotiation state so multiple readers can drive
// the same server.

use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
use std::sync::{Arc, Mutex};

use super::backend::{Errno, FsBackend, MemoryFs};
use super::dax::{parse_remove_payload, DaxSession};
use super::protocol::{
    align_up, AttrOut, CreateIn, DirentHeader, EntryOut, FlushIn, ForgetIn, FsyncIn, GetattrIn,
    InHeader, InitIn, InitOut, IoctlIn, IoctlOut, Kstatfs, LinkIn, MkdirIn, Opcode, OpenIn,
    OpenOut, OutHeader, ReadIn, ReleaseIn, Rename2In, RenameIn, SetattrIn, SetupMappingIn,
    StatfsOut, WriteIn, WriteOut, FUSE_IOCTL_UNRESTRICTED, FUSE_KERNEL_MINOR_VERSION,
    FUSE_KERNEL_VERSION, FUSE_MAP_ALIGNMENT, FUSE_SUBMOUNTS,
};

/// Negotiated state from FUSE_INIT, plus tunables.
#[derive(Clone, Copy, Debug)]
struct Negotiated {
    major: u32,
    minor: u32,
    flags: u32,
    max_write: u32,
    /// log2 of the SETUPMAPPING file-offset granule.
    map_alignment_log2: u16,
}

impl Default for Negotiated {
    fn default() -> Self {
        Self {
            major: 0,
            minor: 0,
            flags: 0,
            max_write: 1024 * 1024,
            map_alignment_log2: 14, // 16 KiB
        }
    }
}

/// FUSE server. Owns a `FsBackend` impl and dispatches incoming
/// requests to its methods.
pub struct FuseServer {
    neg: Mutex<Negotiated>,
    backend: Arc<dyn FsBackend>,
    /// DAX session — None for mounts without DAX support (test mounts,
    /// backends that return ENOTSUP from dax_map). The virtio-fs device
    /// constructs and hands this in when it knows the DAX window's
    /// guest physical address.
    dax: Mutex<Option<Arc<DaxSession>>>,
}

impl Default for FuseServer {
    fn default() -> Self {
        Self::new(Arc::new(MemoryFs::new()))
    }
}

/// Result of dispatching one request. The transport copies `bytes`
/// into its reply buffer.
pub struct Reply {
    pub bytes: Vec<u8>,
}

impl Reply {
    /// Build a header-only reply with a specific errno (negated per
    /// FUSE convention — pass -38 for ENOSYS, etc.).
    pub fn header_only(unique: u64, error: i32) -> Self {
        let out = OutHeader {
            len: core::mem::size_of::<OutHeader>() as u32,
            error,
            unique,
        };
        let bytes = unsafe {
            std::slice::from_raw_parts(
                &out as *const OutHeader as *const u8,
                core::mem::size_of::<OutHeader>(),
            )
        }
        .to_vec();
        Reply { bytes }
    }

    /// Build a header + payload reply. `payload` is appended after
    /// the OutHeader; total `len` is computed automatically.
    pub fn with_payload(unique: u64, payload: &[u8]) -> Self {
        let total = core::mem::size_of::<OutHeader>() + payload.len();
        let mut bytes = Vec::with_capacity(total);
        let out = OutHeader {
            len: total as u32,
            error: 0,
            unique,
        };
        unsafe {
            bytes.extend_from_slice(std::slice::from_raw_parts(
                &out as *const OutHeader as *const u8,
                core::mem::size_of::<OutHeader>(),
            ));
        }
        bytes.extend_from_slice(payload);
        Reply { bytes }
    }
}

/// Convert a `Result<Reply, Errno>` into a `Reply` — `Ok` passes
/// through, `Err` becomes a header-only error reply.
fn or_err(unique: u64, r: Result<Reply, Errno>) -> Reply {
    match r {
        Ok(r) => r,
        Err(e) => Reply::header_only(unique, e),
    }
}

/// Read `T` from the start of `buf`, returning `Err(EINVAL)` if too short.
fn read_struct<T: Copy + Default>(buf: &[u8]) -> Result<T, Errno> {
    if buf.len() < core::mem::size_of::<T>() {
        return Err(super::backend::EINVAL);
    }
    Ok(unsafe { core::ptr::read_unaligned(buf.as_ptr() as *const T) })
}

/// Encode an aligned (8-byte) FUSE dirent (header + name + padding)
/// into `out`. Returns the number of bytes written.
fn emit_dirent(out: &mut Vec<u8>, ino: u64, off: u64, typ: u32, name: &[u8]) -> usize {
    let hdr_size = core::mem::size_of::<DirentHeader>();
    let entry_size = align_up(hdr_size + name.len(), 8);
    let start = out.len();
    let hdr = DirentHeader {
        ino,
        off,
        namelen: name.len() as u32,
        typ,
    };
    unsafe {
        out.extend_from_slice(std::slice::from_raw_parts(
            &hdr as *const DirentHeader as *const u8,
            hdr_size,
        ));
    }
    out.extend_from_slice(name);
    out.resize(start + entry_size, 0);
    entry_size
}

impl FuseServer {
    pub fn new(backend: Arc<dyn FsBackend>) -> Self {
        Self {
            neg: Mutex::new(Negotiated::default()),
            backend,
            dax: Mutex::new(None),
        }
    }

    /// Replace the backend. Useful for tests + for hooking up a real
    /// `PosixFs` after construction.
    pub fn set_backend(&mut self, backend: Arc<dyn FsBackend>) {
        self.backend = backend;
    }

    /// Install a DAX session. Once set, FUSE_SETUPMAPPING /
    /// FUSE_REMOVEMAPPING dispatch through the session instead of
    /// replying -ENOSYS.
    pub fn set_dax(&self, session: Arc<DaxSession>) {
        *self.dax.lock().unwrap() = Some(session);
    }

    /// Read-only access to the negotiated protocol — used by the
    /// device emulation to bound things like max_write.
    pub fn negotiated_max_write(&self) -> u32 {
        self.neg.lock().unwrap().max_write
    }

    pub fn negotiated_major(&self) -> u32 {
        self.neg.lock().unwrap().major
    }

    pub fn negotiated_minor(&self) -> u32 {
        self.neg.lock().unwrap().minor
    }

    pub fn negotiated_flags(&self) -> u32 {
        self.neg.lock().unwrap().flags
    }

    /// Dispatch one FUSE request. `payload` is the bytes that follow
    /// the InHeader in the same readable descriptor chain.
    pub fn dispatch(&self, hdr: &InHeader, payload: &[u8]) -> Reply {
        let op = Opcode::from_u32(hdr.opcode);
        trace(|| format!(
            "req op={op:?} unique={} nodeid={} payload_len={}",
            hdr.unique, hdr.nodeid, payload.len()
        ));
        match op {
            Some(Opcode::Init) => self.handle_init(hdr, payload),
            Some(Opcode::Destroy) => self.handle_destroy(hdr),
            Some(Opcode::Lookup) => or_err(hdr.unique, self.handle_lookup(hdr, payload)),
            Some(Opcode::Forget) => self.handle_forget(hdr, payload),
            Some(Opcode::Getattr) => or_err(hdr.unique, self.handle_getattr(hdr, payload)),
            Some(Opcode::Open) => or_err(hdr.unique, self.handle_open(hdr, payload)),
            Some(Opcode::Read) => or_err(hdr.unique, self.handle_read(hdr, payload)),
            Some(Opcode::Release) => or_err(hdr.unique, self.handle_release(hdr, payload)),
            Some(Opcode::Write) => or_err(hdr.unique, self.handle_write(hdr, payload)),
            Some(Opcode::Fsync) => or_err(hdr.unique, self.handle_fsync(hdr, payload)),
            Some(Opcode::Setattr) => or_err(hdr.unique, self.handle_setattr(hdr, payload)),
            Some(Opcode::Rename) => or_err(hdr.unique, self.handle_rename(hdr, payload)),
            Some(Opcode::Rename2) => or_err(hdr.unique, self.handle_rename2(hdr, payload)),
            Some(Opcode::Flush) => or_err(hdr.unique, self.handle_flush(hdr, payload)),
            Some(Opcode::Fsyncdir) => or_err(hdr.unique, self.handle_fsyncdir(hdr, payload)),
            Some(Opcode::Create) => or_err(hdr.unique, self.handle_create(hdr, payload)),
            Some(Opcode::Mkdir) => or_err(hdr.unique, self.handle_mkdir(hdr, payload)),
            Some(Opcode::Unlink) => or_err(hdr.unique, self.handle_unlink(hdr, payload)),
            Some(Opcode::Rmdir) => or_err(hdr.unique, self.handle_rmdir(hdr, payload)),
            Some(Opcode::Symlink) => or_err(hdr.unique, self.handle_symlink(hdr, payload)),
            Some(Opcode::Readlink) => or_err(hdr.unique, self.handle_readlink(hdr)),
            Some(Opcode::Link) => or_err(hdr.unique, self.handle_link(hdr, payload)),
            Some(Opcode::Opendir) => or_err(hdr.unique, self.handle_opendir(hdr, payload)),
            Some(Opcode::Readdir) => or_err(hdr.unique, self.handle_readdir(hdr, payload)),
            Some(Opcode::Releasedir) => or_err(hdr.unique, self.handle_releasedir(hdr, payload)),
            Some(Opcode::Statfs) => or_err(hdr.unique, self.handle_statfs(hdr)),
            Some(Opcode::SetupMapping) => or_err(hdr.unique, self.handle_setup_mapping(hdr, payload)),
            Some(Opcode::RemoveMapping) => or_err(hdr.unique, self.handle_remove_mapping(hdr, payload)),
            Some(Opcode::Ioctl) => or_err(hdr.unique, self.handle_ioctl(hdr, payload)),
            _ => Reply::header_only(hdr.unique, super::backend::ENOSYS),
        }
    }

    // === FUSE_INIT ====================================================

    fn handle_init(&self, hdr: &InHeader, payload: &[u8]) -> Reply {
        let mut req = InitIn::default();
        let n = payload.len().min(core::mem::size_of::<InitIn>());
        if n >= 8 {
            unsafe {
                core::ptr::copy_nonoverlapping(
                    payload.as_ptr(),
                    &mut req as *mut InitIn as *mut u8,
                    n,
                );
            }
        }

        let major = req.major.min(FUSE_KERNEL_VERSION);
        let minor = if major < FUSE_KERNEL_VERSION {
            req.minor
        } else {
            req.minor.min(FUSE_KERNEL_MINOR_VERSION)
        };

        let supported = FUSE_MAP_ALIGNMENT | FUSE_SUBMOUNTS;
        let agreed = (req.flags & supported) | FUSE_MAP_ALIGNMENT;

        let mut neg = self.neg.lock().unwrap();
        neg.major = major;
        neg.minor = minor;
        neg.flags = agreed;

        let out = InitOut {
            major,
            minor,
            max_readahead: req.max_readahead,
            flags: agreed,
            max_background: 16,
            congestion_threshold: 12,
            max_write: neg.max_write,
            time_gran: 1,
            max_pages: 256,
            map_alignment: neg.map_alignment_log2,
            flags2: 0,
            max_stack_depth: 0,
            unused: [0; 6],
        };

        eprintln!(
            "[fuse] INIT: guest v{}.{} flags={:#x} → agreed v{}.{} flags={:#x} (map_align={})",
            req.major, req.minor, req.flags, major, minor, agreed, neg.map_alignment_log2,
        );

        let payload = unsafe {
            std::slice::from_raw_parts(
                &out as *const InitOut as *const u8,
                core::mem::size_of::<InitOut>(),
            )
        };
        Reply::with_payload(hdr.unique, payload)
    }

    fn handle_destroy(&self, hdr: &InHeader) -> Reply {
        eprintln!("[fuse] DESTROY");
        let mut neg = self.neg.lock().unwrap();
        *neg = Negotiated::default();
        Reply::header_only(hdr.unique, 0)
    }

    // === FUSE_LOOKUP ==================================================
    // Payload is a NUL-terminated name. We strip the NUL.

    fn handle_lookup(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
        let name = strip_nul(payload).ok_or(super::backend::EINVAL)?;
        let entry = self.backend.lookup(hdr.nodeid, OsStr::from_bytes(name))?;
        let out = EntryOut {
            nodeid: entry.nodeid,
            generation: entry.generation,
            entry_valid: entry.entry_valid,
            attr_valid: entry.attr_valid,
            entry_valid_nsec: 0,
            attr_valid_nsec: 0,
            attr: entry.attr,
        };
        let bytes = unsafe {
            std::slice::from_raw_parts(
                &out as *const EntryOut as *const u8,
                core::mem::size_of::<EntryOut>(),
            )
        };
        Ok(Reply::with_payload(hdr.unique, bytes))
    }

    // === FUSE_FORGET ==================================================
    // No reply (one-way).

    fn handle_forget(&self, hdr: &InHeader, payload: &[u8]) -> Reply {
        if let Ok(req) = read_struct::<ForgetIn>(payload) {
            self.backend.forget(hdr.nodeid, req.nlookup);
        }
        // FUSE_FORGET has no reply per spec, but our virtio queue
        // contract requires we still emit a 0-length used. The header_only
        // reply with error=0 + len=sizeof(OutHeader) covers that —
        // however some kernels EXPECT no reply at all. The safest
        // approach: emit a header_only with error=0 unique=0 — the
        // kernel ignores unique=0 replies. virtiofsd does the same.
        let _ = hdr;
        Reply::header_only(0, 0)
    }

    // === FUSE_GETATTR =================================================

    fn handle_getattr(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
        let req = read_struct::<GetattrIn>(payload).unwrap_or_default();
        let fh = if req.flags & 1 != 0 { Some(req.fh) } else { None };
        let attr = self.backend.getattr(hdr.nodeid, fh)?;
        let out = AttrOut {
            attr_valid: 1,
            attr_valid_nsec: 0,
            _pad: 0,
            attr,
        };
        let bytes = unsafe {
            std::slice::from_raw_parts(
                &out as *const AttrOut as *const u8,
                core::mem::size_of::<AttrOut>(),
            )
        };
        Ok(Reply::with_payload(hdr.unique, bytes))
    }

    // === FUSE_OPEN ====================================================

    fn handle_open(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
        let req = read_struct::<OpenIn>(payload)?;
        let fh = self.backend.open(hdr.nodeid, req.flags)?;
        trace(|| format!(
            "OPEN: nodeid={} flags={:#x} → fh={fh}",
            hdr.nodeid, req.flags
        ));
        let out = OpenOut {
            fh,
            open_flags: 0,
            _pad: 0,
        };
        let bytes = unsafe {
            std::slice::from_raw_parts(
                &out as *const OpenOut as *const u8,
                core::mem::size_of::<OpenOut>(),
            )
        };
        Ok(Reply::with_payload(hdr.unique, bytes))
    }

    // === FUSE_READ ====================================================

    fn handle_read(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
        let req = read_struct::<ReadIn>(payload)?;
        let data = self.backend.read(hdr.nodeid, req.fh, req.offset, req.size)?;
        Ok(Reply::with_payload(hdr.unique, &data))
    }

    // === FUSE_RELEASE =================================================

    fn handle_release(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
        let req = read_struct::<ReleaseIn>(payload)?;
        self.backend.release(hdr.nodeid, req.fh)?;
        Ok(Reply::header_only(hdr.unique, 0))
    }

    // === FUSE_WRITE ===================================================
    //
    // WriteIn header is followed by `size` bytes of payload. The
    // device passes us the full readable chain so the data is in
    // `payload[sizeof(WriteIn)..sizeof(WriteIn)+size]`.

    fn handle_write(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
        let req = read_struct::<WriteIn>(payload)?;
        let hdr_size = core::mem::size_of::<WriteIn>();
        if payload.len() < hdr_size + req.size as usize {
            return Err(super::backend::EINVAL);
        }
        let data = &payload[hdr_size..hdr_size + req.size as usize];
        let written = self.backend.write(hdr.nodeid, req.fh, req.offset, data)?;
        let out = WriteOut {
            size: written,
            _pad: 0,
        };
        let bytes = unsafe {
            std::slice::from_raw_parts(
                &out as *const WriteOut as *const u8,
                core::mem::size_of::<WriteOut>(),
            )
        };
        Ok(Reply::with_payload(hdr.unique, bytes))
    }

    // === FUSE_FSYNC ===================================================

    fn handle_fsync(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
        let req = read_struct::<FsyncIn>(payload)?;
        let datasync = req.fsync_flags & 1 != 0;
        self.backend.fsync(hdr.nodeid, req.fh, datasync)?;
        Ok(Reply::header_only(hdr.unique, 0))
    }

    // === FUSE_SETATTR =================================================
    //
    // SetattrIn.valid tells us which fields are live. fh is meaningful
    // only when FATTR_FH is set in valid; otherwise the backend should
    // operate on the path-resolved node.

    fn handle_setattr(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
        let req = read_struct::<SetattrIn>(payload)?;
        let fh = if req.valid & super::protocol::FATTR_FH != 0 {
            Some(req.fh)
        } else {
            None
        };
        let attr = self.backend.setattr(hdr.nodeid, fh, req)?;
        let out = AttrOut {
            attr_valid: 1,
            attr_valid_nsec: 0,
            _pad: 0,
            attr,
        };
        let bytes = unsafe {
            std::slice::from_raw_parts(
                &out as *const AttrOut as *const u8,
                core::mem::size_of::<AttrOut>(),
            )
        };
        Ok(Reply::with_payload(hdr.unique, bytes))
    }

    // === FUSE_RENAME / FUSE_RENAME2 ===================================
    //
    // Payload: RenameIn{newdir} (or Rename2In{newdir,flags,padding})
    // followed by `old_name\0new_name\0`.

    fn handle_rename(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
        let req = read_struct::<RenameIn>(payload)?;
        let names = &payload[core::mem::size_of::<RenameIn>()..];
        let (old_name, new_name) = split_two_cstrings(names).ok_or(super::backend::EINVAL)?;
        if old_name.is_empty() || new_name.is_empty() {
            return Err(super::backend::EINVAL);
        }
        self.backend.rename(
            hdr.nodeid,
            OsStr::from_bytes(old_name),
            req.newdir,
            OsStr::from_bytes(new_name),
            0,
        )?;
        Ok(Reply::header_only(hdr.unique, 0))
    }

    fn handle_rename2(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
        let req = read_struct::<Rename2In>(payload)?;
        let names = &payload[core::mem::size_of::<Rename2In>()..];
        let (old_name, new_name) = split_two_cstrings(names).ok_or(super::backend::EINVAL)?;
        if old_name.is_empty() || new_name.is_empty() {
            return Err(super::backend::EINVAL);
        }
        self.backend.rename(
            hdr.nodeid,
            OsStr::from_bytes(old_name),
            req.newdir,
            OsStr::from_bytes(new_name),
            req.flags,
        )?;
        Ok(Reply::header_only(hdr.unique, 0))
    }

    // === FUSE_FLUSH / FUSE_FSYNCDIR ===================================
    //
    // Both have no reply body; success = header-only with error=0.

    fn handle_flush(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
        let req = read_struct::<FlushIn>(payload)?;
        self.backend.flush(hdr.nodeid, req.fh)?;
        Ok(Reply::header_only(hdr.unique, 0))
    }

    fn handle_fsyncdir(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
        let req = read_struct::<FsyncIn>(payload)?;
        let datasync = req.fsync_flags & 1 != 0;
        self.backend.fsyncdir(hdr.nodeid, req.fh, datasync)?;
        Ok(Reply::header_only(hdr.unique, 0))
    }

    // === FUSE_CREATE — atomic lookup-or-create + open =================

    fn handle_create(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
        let cin = read_struct::<CreateIn>(payload)?;
        let name = strip_nul(&payload[core::mem::size_of::<CreateIn>()..]).ok_or(super::backend::EINVAL)?;
        let (entry, fh) = self
            .backend
            .create(hdr.nodeid, OsStr::from_bytes(name), cin.mode, cin.flags)?;
        // Reply: EntryOut + OpenOut concatenated.
        let entry_out = EntryOut {
            nodeid: entry.nodeid,
            generation: entry.generation,
            entry_valid: entry.entry_valid,
            attr_valid: entry.attr_valid,
            entry_valid_nsec: 0,
            attr_valid_nsec: 0,
            attr: entry.attr,
        };
        let open_out = OpenOut { fh, open_flags: 0, _pad: 0 };
        let mut buf = Vec::with_capacity(core::mem::size_of::<EntryOut>() + core::mem::size_of::<OpenOut>());
        buf.extend_from_slice(unsafe {
            std::slice::from_raw_parts(
                &entry_out as *const EntryOut as *const u8,
                core::mem::size_of::<EntryOut>(),
            )
        });
        buf.extend_from_slice(unsafe {
            std::slice::from_raw_parts(
                &open_out as *const OpenOut as *const u8,
                core::mem::size_of::<OpenOut>(),
            )
        });
        Ok(Reply::with_payload(hdr.unique, &buf))
    }

    // === FUSE_MKDIR ===================================================

    fn handle_mkdir(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
        let req = read_struct::<MkdirIn>(payload)?;
        let name = strip_nul(&payload[core::mem::size_of::<MkdirIn>()..]).ok_or(super::backend::EINVAL)?;
        let entry = self.backend.mkdir(hdr.nodeid, OsStr::from_bytes(name), req.mode)?;
        let out = EntryOut {
            nodeid: entry.nodeid,
            generation: entry.generation,
            entry_valid: entry.entry_valid,
            attr_valid: entry.attr_valid,
            entry_valid_nsec: 0,
            attr_valid_nsec: 0,
            attr: entry.attr,
        };
        let bytes = unsafe {
            std::slice::from_raw_parts(
                &out as *const EntryOut as *const u8,
                core::mem::size_of::<EntryOut>(),
            )
        };
        Ok(Reply::with_payload(hdr.unique, bytes))
    }

    // === FUSE_UNLINK / FUSE_RMDIR =====================================

    fn handle_unlink(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
        let name = strip_nul(payload).ok_or(super::backend::EINVAL)?;
        self.backend.unlink(hdr.nodeid, OsStr::from_bytes(name))?;
        Ok(Reply::header_only(hdr.unique, 0))
    }

    fn handle_rmdir(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
        let name = strip_nul(payload).ok_or(super::backend::EINVAL)?;
        self.backend.rmdir(hdr.nodeid, OsStr::from_bytes(name))?;
        Ok(Reply::header_only(hdr.unique, 0))
    }

    // === FUSE_SYMLINK / FUSE_READLINK / FUSE_LINK =====================
    //
    // Symlink payload: NUL-terminated `name` then NUL-terminated `target`.
    // Readlink: empty payload — node is `hdr.nodeid`.
    // Link payload: `LinkIn { old_nodeid }` then NUL-terminated `name`.

    fn handle_symlink(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
        let (name, target) = split_two_cstrings(payload).ok_or(super::backend::EINVAL)?;
        if name.is_empty() || target.is_empty() {
            return Err(super::backend::EINVAL);
        }
        let entry = self.backend.symlink(
            hdr.nodeid,
            OsStr::from_bytes(name),
            OsStr::from_bytes(target),
        )?;
        let out = EntryOut {
            nodeid: entry.nodeid,
            generation: entry.generation,
            entry_valid: entry.entry_valid,
            attr_valid: entry.attr_valid,
            entry_valid_nsec: 0,
            attr_valid_nsec: 0,
            attr: entry.attr,
        };
        let bytes = unsafe {
            std::slice::from_raw_parts(
                &out as *const EntryOut as *const u8,
                core::mem::size_of::<EntryOut>(),
            )
        };
        Ok(Reply::with_payload(hdr.unique, bytes))
    }

    fn handle_readlink(&self, hdr: &InHeader) -> Result<Reply, Errno> {
        let target = self.backend.readlink(hdr.nodeid)?;
        // FUSE wire format for READLINK: raw bytes, no trailing NUL.
        Ok(Reply::with_payload(hdr.unique, &target))
    }

    fn handle_link(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
        let req = read_struct::<LinkIn>(payload)?;
        let name = strip_nul(&payload[core::mem::size_of::<LinkIn>()..])
            .ok_or(super::backend::EINVAL)?;
        let entry = self
            .backend
            .link(req.old_nodeid, hdr.nodeid, OsStr::from_bytes(name))?;
        let out = EntryOut {
            nodeid: entry.nodeid,
            generation: entry.generation,
            entry_valid: entry.entry_valid,
            attr_valid: entry.attr_valid,
            entry_valid_nsec: 0,
            attr_valid_nsec: 0,
            attr: entry.attr,
        };
        let bytes = unsafe {
            std::slice::from_raw_parts(
                &out as *const EntryOut as *const u8,
                core::mem::size_of::<EntryOut>(),
            )
        };
        Ok(Reply::with_payload(hdr.unique, bytes))
    }

    // === FUSE_OPENDIR =================================================

    fn handle_opendir(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
        let req = read_struct::<OpenIn>(payload)?;
        let fh = self.backend.opendir(hdr.nodeid, req.flags)?;
        let out = OpenOut {
            fh,
            open_flags: 0,
            _pad: 0,
        };
        let bytes = unsafe {
            std::slice::from_raw_parts(
                &out as *const OpenOut as *const u8,
                core::mem::size_of::<OpenOut>(),
            )
        };
        Ok(Reply::with_payload(hdr.unique, bytes))
    }

    // === FUSE_READDIR =================================================
    //
    // Encodes a stream of `DirentHeader + name + zero-padding` into the
    // reply payload, capped at the guest's `size` request. Each entry's
    // `off` is `i+1` where `i` is the entry index — the kernel passes
    // the last `off` back to us on the next call to continue.

    fn handle_readdir(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
        let req = read_struct::<ReadIn>(payload)?;
        let entries = self
            .backend
            .readdir(hdr.nodeid, req.fh, req.offset, req.size)?;
        let mut out: Vec<u8> = Vec::with_capacity(req.size as usize);
        let cap = req.size as usize;
        for (i, e) in entries.iter().enumerate() {
            let needed = align_up(core::mem::size_of::<DirentHeader>() + e.name.len(), 8);
            if out.len() + needed > cap {
                break;
            }
            // off = req.offset + i + 1 — kernel will pass this back on
            // the next readdir call to resume after this entry.
            let off = req.offset + i as u64 + 1;
            emit_dirent(&mut out, e.ino, off, e.typ, &e.name);
        }
        Ok(Reply::with_payload(hdr.unique, &out))
    }

    // === FUSE_RELEASEDIR ==============================================

    fn handle_releasedir(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
        let req = read_struct::<ReleaseIn>(payload)?;
        self.backend.releasedir(hdr.nodeid, req.fh)?;
        Ok(Reply::header_only(hdr.unique, 0))
    }

    // === FUSE_SETUPMAPPING / FUSE_REMOVEMAPPING — the DAX path =======

    fn handle_setup_mapping(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
        let req = read_struct::<SetupMappingIn>(payload)?;
        let dax = self
            .dax
            .lock()
            .unwrap()
            .clone()
            .ok_or(super::backend::ENOSYS)?;
        dax.setup(hdr.nodeid, &req)?;
        Ok(Reply::header_only(hdr.unique, 0))
    }

    fn handle_remove_mapping(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
        let entries = parse_remove_payload(payload)?;
        let dax = self
            .dax
            .lock()
            .unwrap()
            .clone()
            .ok_or(super::backend::ENOSYS)?;
        // The guest expects ALL entries to unmap; collect the first
        // error but try the rest so a misformed batch doesn't leak
        // mappings.
        let mut first_err: Option<Errno> = None;
        for e in &entries {
            if let Err(err) = dax.remove(hdr.nodeid, e) {
                first_err.get_or_insert(err);
            }
        }
        match first_err {
            None => Ok(Reply::header_only(hdr.unique, 0)),
            Some(e) => Err(e),
        }
    }

    // === FUSE_IOCTL ===================================================
    //
    // Forwards a guest `ioctl(fd, request, arg)` call to the backend
    // when `fd` refers to a virtio-fs file. Used today for one thing:
    // Apple's Rosetta-in-VM, whose `rosettad` daemon issues
    // `ioctl(fd, 0x80806123, &buf128)` on a file in the Rosetta share
    // at startup to learn its AOT cache layout. VZ-backed VMs get this
    // ioctl handled inside Apple's compiled VZ virtio-fs server; on
    // HVF we handle it here via the backend trait.

    fn handle_ioctl(&self, hdr: &InHeader, payload: &[u8]) -> Result<Reply, Errno> {
        let req = read_struct::<IoctlIn>(payload)?;
        let header_size = core::mem::size_of::<IoctlIn>();
        // Input bytes follow the IoctlIn struct (only for _IOC_WRITE
        // and _IOC_RDWR ioctls). Clamp at the buffer to defend against
        // a guest claiming more bytes than it actually sent.
        let in_end = header_size
            .saturating_add(req.in_size as usize)
            .min(payload.len());
        let in_data = if in_end > header_size {
            &payload[header_size..in_end]
        } else {
            &[][..]
        };
        // We don't support FUSE_IOCTL_UNRESTRICTED (rich iovecs). The
        // simple ioctls Rosetta needs are all "single in-buffer +
        // single out-buffer" which fits the basic FUSE_IOCTL contract.
        // If a future ioctl needs the unrestricted path we can extend.
        if (req.flags & FUSE_IOCTL_UNRESTRICTED) != 0 {
            return Err(super::backend::ENOSYS);
        }
        let (result, mut out_bytes) =
            self.backend
                .ioctl(hdr.nodeid, req.fh, req.cmd, in_data, req.out_size)?;
        // Clamp out_bytes to what the guest asked for. If shorter, the
        // kernel only copies that many bytes to the user buffer.
        if out_bytes.len() > req.out_size as usize {
            out_bytes.truncate(req.out_size as usize);
        }
        let out_hdr = IoctlOut {
            result,
            flags: 0,
            in_iovs: 0,
            out_iovs: 0,
        };
        let mut buf = Vec::with_capacity(
            core::mem::size_of::<IoctlOut>() + out_bytes.len(),
        );
        unsafe {
            buf.extend_from_slice(std::slice::from_raw_parts(
                &out_hdr as *const IoctlOut as *const u8,
                core::mem::size_of::<IoctlOut>(),
            ));
        }
        buf.extend_from_slice(&out_bytes);
        Ok(Reply::with_payload(hdr.unique, &buf))
    }

    // === FUSE_STATFS ==================================================

    fn handle_statfs(&self, hdr: &InHeader) -> Result<Reply, Errno> {
        let s = self.backend.statfs(hdr.nodeid)?;
        let out = StatfsOut {
            st: Kstatfs {
                blocks: s.blocks,
                bfree: s.bfree,
                bavail: s.bavail,
                files: s.files,
                ffree: s.ffree,
                bsize: s.bsize,
                namelen: s.namelen,
                frsize: s.frsize,
                _pad: 0,
                spare: [0; 6],
            },
        };
        let bytes = unsafe {
            std::slice::from_raw_parts(
                &out as *const StatfsOut as *const u8,
                core::mem::size_of::<StatfsOut>(),
            )
        };
        Ok(Reply::with_payload(hdr.unique, bytes))
    }
}

/// File-based trace. Set `SUPERMACHINE_FUSE_TRACE` to a file path
/// (or `1` / `stderr` for the worker's stderr) to enable. The
/// closure form lets us skip the format work in the common
/// trace-disabled case.
fn trace<F: FnOnce() -> String>(make_msg: F) {
    use std::io::Write;
    let Some(target) = std::env::var_os("SUPERMACHINE_FUSE_TRACE") else { return };
    let s = make_msg();
    let target = target.to_string_lossy().into_owned();
    if target == "1" || target == "stderr" {
        eprintln!("[fuse] {s}");
        return;
    }
    if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(&target) {
        let _ = writeln!(f, "[fuse] {s}");
    }
}

/// Split a FUSE wire payload of `cstring1\0cstring2\0` into the two
/// component byte slices (NUL bytes excluded). Returns `None` if the
/// buffer doesn't contain at least one NUL — the second slice may be
/// empty (no trailing NUL) but the caller can reject that.
fn split_two_cstrings(buf: &[u8]) -> Option<(&[u8], &[u8])> {
    let first_nul = buf.iter().position(|&b| b == 0)?;
    let name = &buf[..first_nul];
    let rest = &buf[first_nul + 1..];
    // Strip optional trailing NUL from the target — both Linux's FUSE
    // client (which double-NUL-terminates) and pedantic generators
    // are accepted.
    let target = if !rest.is_empty() && rest[rest.len() - 1] == 0 {
        &rest[..rest.len() - 1]
    } else {
        rest
    };
    Some((name, target))
}

/// Strip a trailing NUL byte from a name buffer (FUSE wire format).
/// Returns None for empty / NUL-only inputs.
fn strip_nul(buf: &[u8]) -> Option<&[u8]> {
    if buf.is_empty() {
        return None;
    }
    let end = if buf[buf.len() - 1] == 0 {
        buf.len() - 1
    } else {
        buf.len()
    };
    if end == 0 {
        None
    } else {
        Some(&buf[..end])
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::fuse::backend::MemoryFs;
    use crate::fuse::protocol::{FUSE_KERNEL_MINOR_VERSION, FUSE_ROOT_ID, S_IFDIR, S_IFMT, S_IFREG};

    fn make_server() -> (FuseServer, Arc<MemoryFs>) {
        let fs = Arc::new(MemoryFs::new());
        let srv = FuseServer::new(fs.clone());
        (srv, fs)
    }

    fn make_in_header(op: Opcode, unique: u64, nodeid: u64, payload_len: usize) -> InHeader {
        InHeader {
            len: (core::mem::size_of::<InHeader>() + payload_len) as u32,
            opcode: op as u32,
            unique,
            nodeid,
            uid: 1000,
            gid: 1000,
            pid: 42,
            padding: 0,
        }
    }

    fn extract_out_header(bytes: &[u8]) -> OutHeader {
        unsafe { core::ptr::read_unaligned(bytes.as_ptr() as *const OutHeader) }
    }

    #[test]
    fn init_then_lookup_root_child() {
        let (srv, fs) = make_server();
        let f = fs.add_file(FUSE_ROOT_ID, b"foo.txt", b"hi".to_vec());

        // First INIT.
        let init_in = InitIn {
            major: FUSE_KERNEL_VERSION,
            minor: FUSE_KERNEL_MINOR_VERSION,
            max_readahead: 0,
            flags: FUSE_MAP_ALIGNMENT,
            flags2: 0,
            unused: [0; 11],
        };
        let payload = unsafe {
            std::slice::from_raw_parts(
                &init_in as *const InitIn as *const u8,
                core::mem::size_of::<InitIn>(),
            )
        };
        let hdr = make_in_header(Opcode::Init, 1, 0, payload.len());
        let init_reply = srv.dispatch(&hdr, payload);
        assert_eq!(extract_out_header(&init_reply.bytes).error, 0);

        // LOOKUP "foo.txt" under root.
        let name = b"foo.txt\0";
        let hdr = make_in_header(Opcode::Lookup, 2, FUSE_ROOT_ID, name.len());
        let reply = srv.dispatch(&hdr, name);
        let oh = extract_out_header(&reply.bytes);
        assert_eq!(oh.error, 0, "LOOKUP returned errno");
        let entry_out: EntryOut = unsafe {
            core::ptr::read_unaligned(
                reply.bytes.as_ptr().add(core::mem::size_of::<OutHeader>()) as *const EntryOut,
            )
        };
        assert_eq!(entry_out.nodeid, f);
        assert_eq!(entry_out.attr.size, 2);
        assert_eq!(entry_out.attr.mode & S_IFMT, S_IFREG);
    }

    #[test]
    fn lookup_miss_returns_enoent() {
        let (srv, _) = make_server();
        let name = b"nope\0";
        let hdr = make_in_header(Opcode::Lookup, 7, FUSE_ROOT_ID, name.len());
        let reply = srv.dispatch(&hdr, name);
        assert_eq!(extract_out_header(&reply.bytes).error, super::super::backend::ENOENT);
    }

    #[test]
    fn open_then_read_returns_file_bytes() {
        let (srv, fs) = make_server();
        let payload = (0u8..=255).collect::<Vec<_>>();
        let f = fs.add_file(FUSE_ROOT_ID, b"data.bin", payload.clone());

        // OPEN
        let open_in = OpenIn {
            flags: 0,
            open_flags: 0,
        };
        let hdr = make_in_header(
            Opcode::Open,
            3,
            f,
            core::mem::size_of::<OpenIn>(),
        );
        let raw = unsafe {
            std::slice::from_raw_parts(
                &open_in as *const OpenIn as *const u8,
                core::mem::size_of::<OpenIn>(),
            )
        };
        let reply = srv.dispatch(&hdr, raw);
        assert_eq!(extract_out_header(&reply.bytes).error, 0);
        let open_out: OpenOut = unsafe {
            core::ptr::read_unaligned(
                reply.bytes.as_ptr().add(core::mem::size_of::<OutHeader>()) as *const OpenOut,
            )
        };
        let fh = open_out.fh;

        // READ 100 bytes at offset 50.
        let read_in = ReadIn {
            fh,
            offset: 50,
            size: 100,
            read_flags: 0,
            lock_owner: 0,
            flags: 0,
            _pad: 0,
        };
        let raw = unsafe {
            std::slice::from_raw_parts(
                &read_in as *const ReadIn as *const u8,
                core::mem::size_of::<ReadIn>(),
            )
        };
        let hdr = make_in_header(Opcode::Read, 4, f, raw.len());
        let reply = srv.dispatch(&hdr, raw);
        let oh = extract_out_header(&reply.bytes);
        assert_eq!(oh.error, 0);
        let data = &reply.bytes[core::mem::size_of::<OutHeader>()..];
        assert_eq!(data.len(), 100);
        assert_eq!(data, &payload[50..150]);

        // RELEASE
        let rel = ReleaseIn {
            fh,
            flags: 0,
            release_flags: 0,
            lock_owner: 0,
        };
        let raw = unsafe {
            std::slice::from_raw_parts(
                &rel as *const ReleaseIn as *const u8,
                core::mem::size_of::<ReleaseIn>(),
            )
        };
        let hdr = make_in_header(Opcode::Release, 5, f, raw.len());
        let reply = srv.dispatch(&hdr, raw);
        assert_eq!(extract_out_header(&reply.bytes).error, 0);
    }

    #[test]
    fn opendir_readdir_releasedir_round_trip() {
        let (srv, fs) = make_server();
        fs.add_file(FUSE_ROOT_ID, b"a", b"x".to_vec());
        fs.add_file(FUSE_ROOT_ID, b"bb", b"y".to_vec());
        fs.add_dir(FUSE_ROOT_ID, b"ccc");

        // OPENDIR
        let open_in = OpenIn {
            flags: 0,
            open_flags: 0,
        };
        let raw = unsafe {
            std::slice::from_raw_parts(
                &open_in as *const OpenIn as *const u8,
                core::mem::size_of::<OpenIn>(),
            )
        };
        let hdr = make_in_header(Opcode::Opendir, 8, FUSE_ROOT_ID, raw.len());
        let reply = srv.dispatch(&hdr, raw);
        assert_eq!(extract_out_header(&reply.bytes).error, 0);
        let open_out: OpenOut = unsafe {
            core::ptr::read_unaligned(
                reply.bytes.as_ptr().add(core::mem::size_of::<OutHeader>()) as *const OpenOut,
            )
        };
        let fh = open_out.fh;

        // READDIR
        let read_in = ReadIn {
            fh,
            offset: 0,
            size: 4096,
            read_flags: 0,
            lock_owner: 0,
            flags: 0,
            _pad: 0,
        };
        let raw = unsafe {
            std::slice::from_raw_parts(
                &read_in as *const ReadIn as *const u8,
                core::mem::size_of::<ReadIn>(),
            )
        };
        let hdr = make_in_header(Opcode::Readdir, 9, FUSE_ROOT_ID, raw.len());
        let reply = srv.dispatch(&hdr, raw);
        assert_eq!(extract_out_header(&reply.bytes).error, 0);

        // Walk the dirent stream.
        let mut cur = &reply.bytes[core::mem::size_of::<OutHeader>()..];
        let mut names: Vec<(Vec<u8>, u32)> = Vec::new();
        while cur.len() >= core::mem::size_of::<DirentHeader>() {
            let dh: DirentHeader = unsafe { core::ptr::read_unaligned(cur.as_ptr() as *const DirentHeader) };
            let nm_start = core::mem::size_of::<DirentHeader>();
            let nm_end = nm_start + dh.namelen as usize;
            let name = cur[nm_start..nm_end].to_vec();
            names.push((name, dh.typ));
            let entry_size = align_up(nm_end, 8);
            cur = &cur[entry_size..];
        }
        assert_eq!(names.len(), 3);
        let by_name: std::collections::HashMap<&[u8], u32> =
            names.iter().map(|(n, t)| (n.as_slice(), *t)).collect();
        assert_eq!(by_name[&b"a"[..]], crate::fuse::protocol::DT_REG);
        assert_eq!(by_name[&b"bb"[..]], crate::fuse::protocol::DT_REG);
        assert_eq!(by_name[&b"ccc"[..]], crate::fuse::protocol::DT_DIR);

        // RELEASEDIR
        let rel = ReleaseIn {
            fh,
            flags: 0,
            release_flags: 0,
            lock_owner: 0,
        };
        let raw = unsafe {
            std::slice::from_raw_parts(
                &rel as *const ReleaseIn as *const u8,
                core::mem::size_of::<ReleaseIn>(),
            )
        };
        let hdr = make_in_header(Opcode::Releasedir, 10, FUSE_ROOT_ID, raw.len());
        let reply = srv.dispatch(&hdr, raw);
        assert_eq!(extract_out_header(&reply.bytes).error, 0);
    }

    #[test]
    fn getattr_returns_attr_of_root() {
        let (srv, _) = make_server();
        let getattr_in = GetattrIn::default();
        let raw = unsafe {
            std::slice::from_raw_parts(
                &getattr_in as *const GetattrIn as *const u8,
                core::mem::size_of::<GetattrIn>(),
            )
        };
        let hdr = make_in_header(Opcode::Getattr, 11, FUSE_ROOT_ID, raw.len());
        let reply = srv.dispatch(&hdr, raw);
        assert_eq!(extract_out_header(&reply.bytes).error, 0);
        let attr_out: AttrOut = unsafe {
            core::ptr::read_unaligned(
                reply.bytes.as_ptr().add(core::mem::size_of::<OutHeader>()) as *const AttrOut,
            )
        };
        assert_eq!(attr_out.attr.ino, FUSE_ROOT_ID);
        assert_eq!(attr_out.attr.mode & S_IFMT, S_IFDIR);
    }

    #[test]
    fn statfs_returns_filesystem_stats() {
        let (srv, _) = make_server();
        let hdr = make_in_header(Opcode::Statfs, 12, FUSE_ROOT_ID, 0);
        let reply = srv.dispatch(&hdr, &[]);
        assert_eq!(extract_out_header(&reply.bytes).error, 0);
        let st: StatfsOut = unsafe {
            core::ptr::read_unaligned(
                reply.bytes.as_ptr().add(core::mem::size_of::<OutHeader>()) as *const StatfsOut,
            )
        };
        assert_eq!(st.st.bsize, 4096);
    }

    #[test]
    fn forget_decrements_lookup_count() {
        let (srv, fs) = make_server();
        let f = fs.add_file(FUSE_ROOT_ID, b"x", b"data".to_vec());
        // Bump lookup count via LOOKUP.
        let name = b"x\0";
        let hdr = make_in_header(Opcode::Lookup, 1, FUSE_ROOT_ID, name.len());
        let _ = srv.dispatch(&hdr, name);

        // FORGET it.
        let forget = ForgetIn { nlookup: 1 };
        let raw = unsafe {
            std::slice::from_raw_parts(
                &forget as *const ForgetIn as *const u8,
                core::mem::size_of::<ForgetIn>(),
            )
        };
        let hdr = make_in_header(Opcode::Forget, 2, f, raw.len());
        let reply = srv.dispatch(&hdr, raw);
        // FORGET reply has unique=0 (kernel ignores).
        assert_eq!(extract_out_header(&reply.bytes).unique, 0);
    }

    #[test]
    fn symlink_dispatches_through_backend_default_erofs() {
        // The dispatcher now wires SYMLINK through to the backend.
        // MemoryFs (our default test backend) doesn't override symlink
        // so the trait default fires — EROFS, signalling "this backend
        // is read-only with respect to symlinks". Confirms dispatch is
        // hooked up; the POSIX backend tests cover the success path.
        let (srv, _) = make_server();
        // Payload: name="link\0" target="foo\0".
        let mut payload = Vec::new();
        payload.extend_from_slice(b"link\0");
        payload.extend_from_slice(b"foo\0");
        let hdr = make_in_header(Opcode::Symlink, 99, FUSE_ROOT_ID, payload.len());
        let reply = srv.dispatch(&hdr, &payload);
        assert_eq!(extract_out_header(&reply.bytes).error, super::super::backend::EROFS);
    }

    #[test]
    fn readlink_dispatches_through_backend_default_einval() {
        let (srv, _) = make_server();
        let hdr = make_in_header(Opcode::Readlink, 100, FUSE_ROOT_ID, 0);
        let reply = srv.dispatch(&hdr, &[]);
        // Trait default for non-symlink-aware backends: EINVAL,
        // matching readlink(2) on a non-symlink.
        assert_eq!(extract_out_header(&reply.bytes).error, super::super::backend::EINVAL);
    }

    #[test]
    fn link_dispatches_through_backend_default_erofs() {
        let (srv, _) = make_server();
        // Payload: LinkIn { old_nodeid } + NUL-terminated name.
        let lin = LinkIn { old_nodeid: 5 };
        let mut payload = Vec::new();
        unsafe {
            payload.extend_from_slice(std::slice::from_raw_parts(
                &lin as *const LinkIn as *const u8,
                core::mem::size_of::<LinkIn>(),
            ));
        }
        payload.extend_from_slice(b"newname\0");
        let hdr = make_in_header(Opcode::Link, 101, FUSE_ROOT_ID, payload.len());
        let reply = srv.dispatch(&hdr, &payload);
        assert_eq!(extract_out_header(&reply.bytes).error, super::super::backend::EROFS);
    }

    #[test]
    fn setattr_dispatches_through_backend_default_erofs() {
        // MemoryFs doesn't override setattr → trait default fires.
        // Confirms dispatch + payload parse work; POSIX backend tests
        // cover the success path.
        let (srv, _) = make_server();
        let req = crate::fuse::protocol::SetattrIn {
            valid: crate::fuse::protocol::FATTR_MODE,
            padding: 0,
            fh: 0,
            size: 0,
            lock_owner: 0,
            atime: 0,
            mtime: 0,
            ctime: 0,
            atimensec: 0,
            mtimensec: 0,
            ctimensec: 0,
            mode: 0o600,
            unused4: 0,
            uid: 0,
            gid: 0,
            unused5: 0,
        };
        let raw = unsafe {
            std::slice::from_raw_parts(
                &req as *const crate::fuse::protocol::SetattrIn as *const u8,
                core::mem::size_of::<crate::fuse::protocol::SetattrIn>(),
            )
        };
        let hdr = make_in_header(Opcode::Setattr, 300, FUSE_ROOT_ID, raw.len());
        let reply = srv.dispatch(&hdr, raw);
        assert_eq!(extract_out_header(&reply.bytes).error, super::super::backend::EROFS);
    }

    #[test]
    fn rename_dispatches_through_backend_default_erofs() {
        let (srv, _) = make_server();
        let req = crate::fuse::protocol::RenameIn { newdir: FUSE_ROOT_ID };
        let mut payload = Vec::new();
        unsafe {
            payload.extend_from_slice(std::slice::from_raw_parts(
                &req as *const crate::fuse::protocol::RenameIn as *const u8,
                core::mem::size_of::<crate::fuse::protocol::RenameIn>(),
            ));
        }
        payload.extend_from_slice(b"old\0new\0");
        let hdr = make_in_header(Opcode::Rename, 301, FUSE_ROOT_ID, payload.len());
        let reply = srv.dispatch(&hdr, &payload);
        assert_eq!(extract_out_header(&reply.bytes).error, super::super::backend::EROFS);
    }

    #[test]
    fn flush_dispatches_through_backend_default_ok() {
        // MemoryFs uses the trait default: Ok(()) — close(2) close-time
        // hint with no buffered state to commit.
        let (srv, fs) = make_server();
        let f = fs.add_file(FUSE_ROOT_ID, b"f", b"x".to_vec());
        let fh = fs.open(f, 0).unwrap();
        let req = FlushIn {
            fh,
            unused: 0,
            padding: 0,
            lock_owner: 0,
        };
        let raw = unsafe {
            std::slice::from_raw_parts(
                &req as *const FlushIn as *const u8,
                core::mem::size_of::<FlushIn>(),
            )
        };
        let hdr = make_in_header(Opcode::Flush, 302, f, raw.len());
        let reply = srv.dispatch(&hdr, raw);
        assert_eq!(extract_out_header(&reply.bytes).error, 0);
    }

    #[test]
    fn fsyncdir_dispatches_through_backend_default_ok() {
        let (srv, fs) = make_server();
        let dh = fs.opendir(FUSE_ROOT_ID, 0).unwrap();
        let req = FsyncIn {
            fh: dh,
            fsync_flags: 0,
            _pad: 0,
        };
        let raw = unsafe {
            std::slice::from_raw_parts(
                &req as *const FsyncIn as *const u8,
                core::mem::size_of::<FsyncIn>(),
            )
        };
        let hdr = make_in_header(Opcode::Fsyncdir, 303, FUSE_ROOT_ID, raw.len());
        let reply = srv.dispatch(&hdr, raw);
        assert_eq!(extract_out_header(&reply.bytes).error, 0);
    }

    #[test]
    fn unsupported_opcode_replies_enosys() {
        let (srv, _) = make_server();
        // FUSE_INTERRUPT isn't dispatched; verify the fallback works.
        let hdr = make_in_header(Opcode::Interrupt, 99, FUSE_ROOT_ID, 0);
        let reply = srv.dispatch(&hdr, &[]);
        assert_eq!(extract_out_header(&reply.bytes).error, super::super::backend::ENOSYS);
    }

    #[test]
    fn setup_mapping_without_dax_returns_enosys() {
        let (srv, _) = make_server();
        let req = SetupMappingIn {
            fh: 1,
            foffset: 0,
            len: 0x4000,
            flags: super::super::protocol::FUSE_SETUPMAPPING_FLAG_READ,
            moffset: 0,
        };
        let raw = unsafe {
            std::slice::from_raw_parts(
                &req as *const SetupMappingIn as *const u8,
                core::mem::size_of::<SetupMappingIn>(),
            )
        };
        let hdr = make_in_header(Opcode::SetupMapping, 13, FUSE_ROOT_ID, raw.len());
        let reply = srv.dispatch(&hdr, raw);
        assert_eq!(extract_out_header(&reply.bytes).error, super::super::backend::ENOSYS);
    }

    /// End-to-end test: configure a DAX session (with a stub backend
    /// + mock mapper), dispatch FUSE_SETUPMAPPING, verify the mapper
    /// recorded the right call, then dispatch FUSE_REMOVEMAPPING and
    /// verify the slot is gone.
    #[test]
    fn write_then_read_round_trip() {
        let (srv, fs) = make_server();
        let f = fs.add_file(FUSE_ROOT_ID, b"out.bin", vec![0u8; 16]);

        // OPEN
        let open_in = OpenIn { flags: 0, open_flags: 0 };
        let raw = unsafe {
            std::slice::from_raw_parts(
                &open_in as *const OpenIn as *const u8,
                core::mem::size_of::<OpenIn>(),
            )
        };
        let hdr = make_in_header(Opcode::Open, 100, f, raw.len());
        let reply = srv.dispatch(&hdr, raw);
        let open_out: OpenOut = unsafe {
            core::ptr::read_unaligned(
                reply.bytes.as_ptr().add(core::mem::size_of::<OutHeader>()) as *const OpenOut,
            )
        };
        let fh = open_out.fh;

        // WRITE 8 bytes at offset 4.
        let payload_bytes: [u8; 8] = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22];
        let write_in = WriteIn {
            fh,
            offset: 4,
            size: payload_bytes.len() as u32,
            write_flags: 0,
            lock_owner: 0,
            flags: 0,
            _pad: 0,
        };
        let mut buf = Vec::new();
        buf.extend_from_slice(unsafe {
            std::slice::from_raw_parts(
                &write_in as *const WriteIn as *const u8,
                core::mem::size_of::<WriteIn>(),
            )
        });
        buf.extend_from_slice(&payload_bytes);
        let hdr = make_in_header(Opcode::Write, 101, f, buf.len());
        let reply = srv.dispatch(&hdr, &buf);
        assert_eq!(extract_out_header(&reply.bytes).error, 0);
        let write_out: WriteOut = unsafe {
            core::ptr::read_unaligned(
                reply.bytes.as_ptr().add(core::mem::size_of::<OutHeader>()) as *const WriteOut,
            )
        };
        assert_eq!(write_out.size, 8);

        // READ 16 bytes from offset 0 — first 4 should be zeros, next 8 our payload.
        let read_in = ReadIn {
            fh,
            offset: 0,
            size: 16,
            read_flags: 0,
            lock_owner: 0,
            flags: 0,
            _pad: 0,
        };
        let raw = unsafe {
            std::slice::from_raw_parts(
                &read_in as *const ReadIn as *const u8,
                core::mem::size_of::<ReadIn>(),
            )
        };
        let hdr = make_in_header(Opcode::Read, 102, f, raw.len());
        let reply = srv.dispatch(&hdr, raw);
        let data = &reply.bytes[core::mem::size_of::<OutHeader>()..];
        assert_eq!(&data[..4], &[0, 0, 0, 0]);
        assert_eq!(&data[4..12], &payload_bytes[..]);
    }

    #[test]
    fn fsync_round_trip() {
        let (srv, fs) = make_server();
        let f = fs.add_file(FUSE_ROOT_ID, b"f", vec![0u8; 4]);

        let open_in = OpenIn { flags: 0, open_flags: 0 };
        let raw = unsafe {
            std::slice::from_raw_parts(
                &open_in as *const OpenIn as *const u8,
                core::mem::size_of::<OpenIn>(),
            )
        };
        let hdr = make_in_header(Opcode::Open, 200, f, raw.len());
        let reply = srv.dispatch(&hdr, raw);
        let open_out: OpenOut = unsafe {
            core::ptr::read_unaligned(
                reply.bytes.as_ptr().add(core::mem::size_of::<OutHeader>()) as *const OpenOut,
            )
        };

        let fsync_in = FsyncIn { fh: open_out.fh, fsync_flags: 0, _pad: 0 };
        let raw = unsafe {
            std::slice::from_raw_parts(
                &fsync_in as *const FsyncIn as *const u8,
                core::mem::size_of::<FsyncIn>(),
            )
        };
        let hdr = make_in_header(Opcode::Fsync, 201, f, raw.len());
        let reply = srv.dispatch(&hdr, raw);
        assert_eq!(extract_out_header(&reply.bytes).error, 0);
    }

    #[test]
    fn setup_then_remove_mapping_via_dax_session() {
        use crate::fuse::dax::{DaxSession, MockCall, MockHvfMapper};
        use crate::fuse::protocol::{
            FUSE_SETUPMAPPING_FLAG_READ, FUSE_SETUPMAPPING_FLAG_WRITE,
        };
        use crate::fuse::backend::{Entry, DirEntry as BDirEntry, StatFs};
        use std::ffi::OsStr;
        use std::sync::Mutex;

        struct DaxStubBackend {
            inner: MemoryFs,
            next_va: Mutex<usize>,
        }
        impl FsBackend for DaxStubBackend {
            fn lookup(&self, p: u64, n: &OsStr) -> Result<Entry, super::super::backend::Errno> { self.inner.lookup(p, n) }
            fn forget(&self, n: u64, nl: u64) { self.inner.forget(n, nl) }
            fn getattr(&self, n: u64, f: Option<u64>) -> Result<crate::fuse::protocol::Attr, super::super::backend::Errno> { self.inner.getattr(n, f) }
            fn open(&self, n: u64, f: u32) -> Result<u64, super::super::backend::Errno> { self.inner.open(n, f) }
            fn read(&self, n: u64, h: u64, o: u64, s: u32) -> Result<Vec<u8>, super::super::backend::Errno> { self.inner.read(n, h, o, s) }
            fn release(&self, n: u64, h: u64) -> Result<(), super::super::backend::Errno> { self.inner.release(n, h) }
            fn opendir(&self, n: u64, f: u32) -> Result<u64, super::super::backend::Errno> { self.inner.opendir(n, f) }
            fn readdir(&self, n: u64, h: u64, o: u64, s: u32) -> Result<Vec<BDirEntry>, super::super::backend::Errno> { self.inner.readdir(n, h, o, s) }
            fn releasedir(&self, n: u64, h: u64) -> Result<(), super::super::backend::Errno> { self.inner.releasedir(n, h) }
            fn statfs(&self, n: u64) -> Result<StatFs, super::super::backend::Errno> { self.inner.statfs(n) }
            fn dax_map(&self, _n: u64, _f: u64, _fo: u64, len: u64, _p: u32) -> Result<*mut u8, super::super::backend::Errno> {
                let mut g = self.next_va.lock().unwrap();
                let va = *g;
                *g += len as usize;
                Ok(va as *mut u8)
            }
            fn dax_unmap(&self, _n: u64, _va: *mut u8, _len: u64) -> Result<(), super::super::backend::Errno> { Ok(()) }
        }

        let backend = Arc::new(DaxStubBackend {
            inner: MemoryFs::new(),
            next_va: Mutex::new(0x40_0000_0000usize),
        });
        let f = backend.inner.add_file(FUSE_ROOT_ID, b"a", vec![0u8; 0x4000]);
        let mapper = Arc::new(MockHvfMapper::new());
        let session = Arc::new(DaxSession::new(
            0x80_0000_0000,
            0x4_0000_0000,
            backend.clone(),
            mapper.clone(),
        ));
        let srv = FuseServer::new(backend);
        srv.set_dax(session);

        // SETUPMAPPING
        let setup = SetupMappingIn {
            fh: 1,
            foffset: 0,
            len: 0x4000,
            flags: FUSE_SETUPMAPPING_FLAG_READ | FUSE_SETUPMAPPING_FLAG_WRITE,
            moffset: 0x2_0000,
        };
        let raw = unsafe {
            std::slice::from_raw_parts(
                &setup as *const SetupMappingIn as *const u8,
                core::mem::size_of::<SetupMappingIn>(),
            )
        };
        let hdr = make_in_header(Opcode::SetupMapping, 20, f, raw.len());
        let reply = srv.dispatch(&hdr, raw);
        assert_eq!(extract_out_header(&reply.bytes).error, 0, "SETUPMAPPING ok");

        // Mapper got the map call at the right gpa.
        let calls = mapper.calls();
        assert_eq!(calls.len(), 1);
        match &calls[0] {
            MockCall::Map { gpa, len, .. } => {
                assert_eq!(*gpa, 0x80_0000_0000 + 0x2_0000);
                assert_eq!(*len, 0x4000);
            }
            _ => panic!(),
        }

        // REMOVEMAPPING: header (count=1) + one RemoveMappingOne.
        use crate::fuse::protocol::{RemoveMappingIn, RemoveMappingOne};
        let rm_hdr = RemoveMappingIn { count: 1, _pad: 0 };
        let rm_one = RemoveMappingOne {
            moffset: 0x2_0000,
            len: 0x4000,
        };
        let mut buf = Vec::new();
        buf.extend_from_slice(unsafe {
            std::slice::from_raw_parts(
                &rm_hdr as *const RemoveMappingIn as *const u8,
                core::mem::size_of::<RemoveMappingIn>(),
            )
        });
        buf.extend_from_slice(unsafe {
            std::slice::from_raw_parts(
                &rm_one as *const RemoveMappingOne as *const u8,
                core::mem::size_of::<RemoveMappingOne>(),
            )
        });
        let hdr = make_in_header(Opcode::RemoveMapping, 21, f, buf.len());
        let reply = srv.dispatch(&hdr, &buf);
        assert_eq!(extract_out_header(&reply.bytes).error, 0, "REMOVEMAPPING ok");

        let calls = mapper.calls();
        assert_eq!(calls.len(), 2);
        match &calls[1] {
            MockCall::Unmap { gpa, len } => {
                assert_eq!(*gpa, 0x80_0000_0000 + 0x2_0000);
                assert_eq!(*len, 0x4000);
            }
            _ => panic!(),
        }
    }
}
