// Copyright 2018-2026 the Deno authors. MIT license.

//! Desktop runtime for Deno (libdenort).
//!
//! This is a cdylib that exports the Laufey C ABI (laufey_runtime_init,
//! laufey_runtime_start, laufey_runtime_shutdown) and boots the full Deno
//! standalone runtime. A Laufey backend (CEF, WebView) loads this
//! shared library and provides the browser/window layer.
//!
//! The user's code uses `Deno.serve()` or `export default { fetch }`
//! to serve an HTTP app. The desktop runtime starts it on a local port
//! and navigates the webview to it.

use std::borrow::Cow;
use std::collections::HashMap;
use std::collections::HashSet;
use std::env;
// Only used by the unix-only `apply_pending_update`; gated to avoid an unused
// import on other platforms.
#[cfg(unix)]
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;

use deno_core::anyhow::Context;
use deno_core::anyhow::bail;
use deno_core::error::AnyError;
use deno_core::serde_json;
use deno_core::v8;
use deno_lib::util::net::allocate_random_port;
use deno_lib::util::result::js_error_downcast_ref;
use deno_lib::version::otel_runtime_config;
use deno_runtime::fmt_errors::format_js_error;
use deno_terminal::colors;
use denort::desktop::DesktopApi;
use denort::run::RunOptions;

/// Compile-time check: the laufey crate we're linking against must use the
/// same C ABI version as the prebuilt backend we ship. If laufey bumps
/// `LAUFEY_API_VERSION` without our side updating, `init_api` would reject the
/// backend at startup with `-2`. Catching the mismatch at `cargo build` time
/// makes the failure mode obvious instead of "the desktop app silently won't
/// launch".
const _: () = assert!(
  laufey::LAUFEY_API_VERSION == 30,
  "LAUFEY_API_VERSION mismatch: update this assert and the prebuilt backend release pin in cli/tools/desktop.rs when laufey bumps its API version",
);

/// Laufey-backed implementation of [`denort::desktop::DesktopApi`].
struct WefDesktopApi {
  event_tx: deno_runtime::ops::desktop::DesktopEventTx,
  pending_responses: deno_runtime::ops::desktop::PendingBindResponses,
  closed_windows: Arc<Mutex<HashSet<u32>>>,
  /// IDs of every window currently displayed. Shared with the HMR reload
  /// callback so it can refresh all windows, not just the initial one.
  open_windows: Arc<Mutex<HashSet<u32>>>,
  trays: Arc<Mutex<HashMap<u32, laufey::TrayIcon>>>,
  notifications: Arc<Mutex<HashMap<u32, laufey::NotificationHandle>>>,
  /// Singleton for the unified-mux DevTools window. Without this, every
  /// `openDevtools()` call would spawn another DevTools window.
  devtools_window: Mutex<Option<u32>>,
}

impl WefDesktopApi {
  /// Set up all event handlers on a newly created window, wiring events
  /// into the shared event channel.
  fn setup_window_events(
    &self,
    window: laufey::Window,
    show_on_first_load: bool,
  ) -> laufey::Window {
    let kb_tx = self.event_tx.clone();
    let mouse_click_tx = self.event_tx.clone();
    let mouse_move_tx = self.event_tx.clone();
    let wheel_tx = self.event_tx.clone();
    let cursor_tx = self.event_tx.clone();
    let focus_tx = self.event_tx.clone();
    let resize_tx = self.event_tx.clone();
    let move_tx = self.event_tx.clone();
    let page_load_tx = self.event_tx.clone();
    let close_tx = self.event_tx.clone();
    let closed_windows = self.closed_windows.clone();
    let open_windows_on_close = self.open_windows.clone();
    let shown = Arc::new(std::sync::atomic::AtomicBool::new(false));

    window
      .on_keyboard_event(move |ev| {
        let _ = kb_tx.try_send(
          deno_runtime::ops::desktop::DesktopEvent::KeyboardEvent {
            window_id: ev.window_id,
            r#type: match ev.state {
              laufey::KeyState::Pressed => "keydown".to_string(),
              laufey::KeyState::Released => "keyup".to_string(),
            },
            key: ev.key,
            code: ev.code,
            shift: ev.modifiers.shift,
            control: ev.modifiers.control,
            alt: ev.modifiers.alt,
            meta: ev.modifiers.meta,
            repeat: ev.repeat,
          },
        );
      })
      .on_mouse_click(move |ev| {
        let _ = mouse_click_tx.try_send(
          deno_runtime::ops::desktop::DesktopEvent::MouseClick {
            window_id: ev.window_id,
            state: match ev.state {
              laufey::MouseButtonState::Pressed => "pressed".to_string(),
              laufey::MouseButtonState::Released => "released".to_string(),
            },
            button: match ev.button {
              laufey::MouseButton::Left => 0,
              laufey::MouseButton::Middle => 1,
              laufey::MouseButton::Right => 2,
              laufey::MouseButton::Back => 3,
              laufey::MouseButton::Forward => 4,
              laufey::MouseButton::Other(n) => n,
            },
            client_x: ev.x,
            client_y: ev.y,
            shift: ev.modifiers.shift,
            control: ev.modifiers.control,
            alt: ev.modifiers.alt,
            meta: ev.modifiers.meta,
            click_count: ev.click_count,
          },
        );
      })
      .on_mouse_move(move |ev| {
        let _ = mouse_move_tx.try_send(
          deno_runtime::ops::desktop::DesktopEvent::MouseMove {
            window_id: ev.window_id,
            client_x: ev.x,
            client_y: ev.y,
            shift: ev.modifiers.shift,
            control: ev.modifiers.control,
            alt: ev.modifiers.alt,
            meta: ev.modifiers.meta,
          },
        );
      })
      .on_wheel(move |ev| {
        let _ =
          wheel_tx.try_send(deno_runtime::ops::desktop::DesktopEvent::Wheel {
            window_id: ev.window_id,
            delta_x: ev.delta_x,
            delta_y: ev.delta_y,
            delta_mode: match ev.delta_mode {
              laufey::WheelDeltaMode::Pixel => 0,
              laufey::WheelDeltaMode::Line => 1,
              laufey::WheelDeltaMode::Page => 2,
            },
            client_x: ev.x,
            client_y: ev.y,
            shift: ev.modifiers.shift,
            control: ev.modifiers.control,
            alt: ev.modifiers.alt,
            meta: ev.modifiers.meta,
          });
      })
      .on_cursor_enter_leave(move |ev| {
        let _ = cursor_tx.try_send(
          deno_runtime::ops::desktop::DesktopEvent::CursorEnterLeave {
            window_id: ev.window_id,
            entered: ev.entered,
            client_x: ev.x,
            client_y: ev.y,
            shift: ev.modifiers.shift,
            control: ev.modifiers.control,
            alt: ev.modifiers.alt,
            meta: ev.modifiers.meta,
          },
        );
      })
      .on_focused(move |ev| {
        let _ = focus_tx.try_send(
          deno_runtime::ops::desktop::DesktopEvent::FocusChanged {
            window_id: ev.window_id,
            focused: ev.focused,
          },
        );
      })
      .on_resize(move |ev| {
        let _ = resize_tx.try_send(
          deno_runtime::ops::desktop::DesktopEvent::WindowResize {
            window_id: ev.window_id,
            width: ev.width,
            height: ev.height,
          },
        );
      })
      .on_move(move |ev| {
        let _ = move_tx.try_send(
          deno_runtime::ops::desktop::DesktopEvent::WindowMove {
            window_id: ev.window_id,
            x: ev.x,
            y: ev.y,
          },
        );
      })
      .on_page_load(move |ev| {
        if show_on_first_load && !shown.swap(true, Ordering::AcqRel) {
          laufey::Window::from_id(ev.window_id).show();
        }
        let _ = page_load_tx.try_send(
          deno_runtime::ops::desktop::DesktopEvent::PageLoad {
            window_id: ev.window_id,
          },
        );
      })
      .on_close_requested(move |ev| {
        closed_windows.lock().unwrap().insert(ev.window_id);
        open_windows_on_close.lock().unwrap().remove(&ev.window_id);
        let _ = close_tx.try_send(
          deno_runtime::ops::desktop::DesktopEvent::CloseRequested {
            window_id: ev.window_id,
          },
        );
      })
  }

  /// Create the bootstrap window for the app. Unlike windows constructed from
  /// JS via `BrowserWindow`, this one is created *hidden* and revealed only once
  /// its first navigation has finished loading (wired here via `on_page_load`).
  ///
  /// The runtime navigates this window to the app's `http://127.0.0.1:PORT`
  /// URL only after the loopback server is listening, so creating it visible
  /// up front would leave the user staring at an empty webview — which paints
  /// solid black on Wayland, where the compositor presents the pre-load frame
  /// verbatim. Deferring the reveal to load-finished means the window's first
  /// visible frame already has content. See
  /// https://github.com/denoland/deno/issues/35530.
  fn create_initial_window(&self, width: i32, height: i32) -> u32 {
    let window = laufey::Window::new_with_options(
      width,
      height,
      laufey::WindowOptions {
        frameless: false,
        no_activate: false,
        transparent_titlebar: false,
        hidden: true,
        transparent: false,
      },
    );
    let window = self.setup_window_events(window, true);
    let id = window.id();

    self.open_windows.lock().unwrap().insert(id);
    id
  }
}

impl denort::desktop::DesktopApi for WefDesktopApi {
  fn create_window(
    &self,
    width: i32,
    height: i32,
    frameless: bool,
    no_activate: bool,
    transparent_titlebar: bool,
    transparent: bool,
  ) -> u32 {
    let window = laufey::Window::new_with_options(
      width,
      height,
      laufey::WindowOptions {
        frameless,
        no_activate,
        transparent_titlebar,
        hidden: false,
        transparent,
      },
    );
    let window = self.setup_window_events(window, false);
    let id = window.id();
    self.open_windows.lock().unwrap().insert(id);
    id
  }

  fn close_window(&self, window_id: u32) {
    self.closed_windows.lock().unwrap().insert(window_id);
    self.open_windows.lock().unwrap().remove(&window_id);
    laufey::Window::from_id(window_id).close();
  }

  fn is_closed(&self, window_id: u32) -> bool {
    self.closed_windows.lock().unwrap().contains(&window_id)
  }

  fn set_title(&self, window_id: u32, title: &str) {
    laufey::Window::from_id(window_id).set_title(title);
  }

  fn get_window_size(&self, window_id: u32) -> (i32, i32) {
    laufey::Window::from_id(window_id).get_size()
  }

  fn set_window_size(&self, window_id: u32, width: i32, height: i32) {
    laufey::Window::from_id(window_id).set_size(width, height);
  }

  fn get_window_position(&self, window_id: u32) -> (i32, i32) {
    laufey::Window::from_id(window_id).get_position()
  }

  fn set_window_position(&self, window_id: u32, x: i32, y: i32) {
    laufey::Window::from_id(window_id).set_position(x, y);
  }

  fn is_resizable(&self, window_id: u32) -> bool {
    laufey::Window::from_id(window_id).get_resizable()
  }

  fn set_resizable(&self, window_id: u32, resizable: bool) {
    laufey::Window::from_id(window_id).set_resizable(resizable);
  }

  fn is_always_on_top(&self, window_id: u32) -> bool {
    laufey::Window::from_id(window_id).get_always_on_top()
  }

  fn set_always_on_top(&self, window_id: u32, always_on_top: bool) {
    laufey::Window::from_id(window_id).set_always_on_top(always_on_top);
  }

  fn get_window_opacity(&self, window_id: u32) -> f64 {
    laufey::Window::from_id(window_id).get_opacity()
  }

  fn set_window_opacity(&self, window_id: u32, opacity: f64) {
    laufey::Window::from_id(window_id).set_opacity(opacity);
  }

  fn is_visible(&self, window_id: u32) -> bool {
    laufey::Window::from_id(window_id).get_visible()
  }

  fn show(&self, window_id: u32) {
    laufey::Window::from_id(window_id).show();
  }

  fn hide(&self, window_id: u32) {
    laufey::Window::from_id(window_id).hide();
  }

  fn focus(&self, window_id: u32) {
    laufey::Window::from_id(window_id).focus();
  }

  fn open_devtools(&self, window_id: u32, renderer: bool, deno: bool) {
    if let Ok(mux) = env::var("DENO_DESKTOP_MUX_WS") {
      // Reuse an existing DevTools window when one is already open, so
      // repeated `openDevtools()` calls don't pile up windows.
      if let Some(id) = *self.devtools_window.lock().unwrap()
        && !self.closed_windows.lock().unwrap().contains(&id)
      {
        laufey::Window::from_id(id).focus();
        return;
      }

      let (endpoint, frontend) = match (renderer, deno) {
        (true, true) => ("/unified", "inspector.html"),
        (true, false) => ("/cef", "inspector.html"),
        (false, true) => ("/deno", "js_app.html"),
        (false, false) => unreachable!(),
      };
      let url = format!("http://{mux}/devtools/{frontend}?ws={mux}{endpoint}");
      log::info!(
        "[desktop] openDevtools(renderer={renderer}, deno={deno}) → {url}"
      );
      let window = laufey::Window::new(1200, 800);
      window.set_title("Deno Desktop DevTools");
      window.navigate(&url);
      let window = self.setup_window_events(window, false);
      let id = window.id();
      // Track for HMR reload + the singleton check above.
      self.open_windows.lock().unwrap().insert(id);
      *self.devtools_window.lock().unwrap() = Some(id);
      return;
    }
    laufey::Window::from_id(window_id).open_devtools();
  }

  fn execute_js(
    &self,
    window_id: u32,
    script: &str,
    callback: Box<
      dyn FnOnce(
          Result<
            deno_runtime::ops::desktop::DesktopValue,
            deno_runtime::ops::desktop::DesktopValue,
          >,
        ) + Send
        + 'static,
    >,
  ) {
    laufey::Window::from_id(window_id).execute_js(
      script,
      Some(move |result: Result<laufey::Value, laufey::Value>| {
        callback(match result {
          Ok(val) => Ok(laufey_value_to_desktop_value(val)),
          Err(err) => Err(laufey_value_to_desktop_value(err)),
        });
      }),
    );
  }

  fn bind(&self, window_id: u32, name: &str) {
    let tx = self.event_tx.clone();
    let responses = self.pending_responses.clone();
    let name_owned = name.to_string();
    laufey::Window::from_id(window_id).add_binding_async(
      name,
      move |js_call| {
        let tx = tx.clone();
        let responses = responses.clone();
        let name = name_owned.clone();
        async move {
          let args: Vec<serde_json::Value> =
            js_call.args.iter().map(laufey_value_to_json).collect();
          let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
          let call_id =
            deno_runtime::ops::desktop::register_bind_call(&responses, resp_tx);
          let event = deno_runtime::ops::desktop::DesktopEvent::BindCall {
            // Attribute the call to the window the binding was registered on,
            // not to `js_call.window_id`. The backend's per-call renderer id
            // can drift from the id `bind()` recorded the callback under (seen
            // on CEF/Windows when a larger module graph delays startup; see
            // denoland/deno#35647), which would make the runtime-side lookup
            // in `windowBindCallbacks` miss and reject an otherwise-registered
            // call. The registration id always matches that map's key.
            window_id,
            name,
            args: serde_json::Value::Array(args),
            call_id,
          };
          if let Err(err) = tx.try_send(event) {
            let msg = match err {
              tokio::sync::mpsc::error::TrySendError::Full(_) => {
                "event channel saturated".to_string()
              }
              tokio::sync::mpsc::error::TrySendError::Closed(_) => {
                "event channel closed".to_string()
              }
            };
            js_call.reject(laufey::Value::String(msg));
            return;
          }
          match resp_rx.await {
            Ok(Ok(result)) => {
              js_call.resolve(json_to_laufey_value(&result));
            }
            Ok(Err(error)) => {
              js_call.reject(laufey::Value::String(error));
            }
            Err(_) => {
              js_call.reject(laufey::Value::String(
                "bind response channel dropped".to_string(),
              ));
            }
          }
        }
      },
    );
  }

  fn unbind(&self, window_id: u32, name: &str) {
    laufey::Window::from_id(window_id).unbind(name);
  }

  fn navigate(&self, window_id: u32, url: &str) {
    laufey::Window::from_id(window_id).navigate(url);
  }

  fn quit(&self) {
    laufey::quit();
  }

  fn set_application_menu(
    &self,
    window_id: u32,
    menu: Vec<denort::desktop::MenuItem>,
  ) {
    let menu = menu
      .into_iter()
      .map(desktop_menu_item_to_laufey_menu_item)
      .collect::<Vec<_>>();
    let tx = self.event_tx.clone();
    laufey::Window::from_id(window_id).set_menu(&menu, move |id: &str| {
      let _ =
        tx.try_send(deno_runtime::ops::desktop::DesktopEvent::AppMenuClick {
          window_id,
          id: id.to_string(),
        });
    });
  }

  fn show_context_menu(
    &self,
    window_id: u32,
    x: i32,
    y: i32,
    menu: Vec<denort::desktop::MenuItem>,
  ) {
    let menu = menu
      .into_iter()
      .map(desktop_menu_item_to_laufey_menu_item)
      .collect::<Vec<_>>();
    let tx = self.event_tx.clone();
    laufey::Window::from_id(window_id).show_context_menu(
      x,
      y,
      &menu,
      move |id: &str| {
        let _ = tx.try_send(
          deno_runtime::ops::desktop::DesktopEvent::ContextMenuClick {
            window_id,
            id: id.to_string(),
          },
        );
      },
    );
  }

  fn get_raw_window_handle(
    &self,
    window_id: u32,
  ) -> Result<
    (
      raw_window_handle::RawWindowHandle,
      raw_window_handle::RawDisplayHandle,
    ),
    deno_error::JsErrorBox,
  > {
    let window = laufey::Window::from_id(window_id);
    let handle_type = window.get_window_handle_type();
    let raw_win = window.get_window_handle();
    let raw_display = window.get_display_handle();

    let null_window = || {
      deno_error::JsErrorBox::generic("Laufey returned a null window handle")
    };
    let null_display = || {
      deno_error::JsErrorBox::generic("Laufey returned a null display handle")
    };

    match handle_type {
      laufey::LAUFEY_WINDOW_HANDLE_APPKIT => {
        use raw_window_handle::*;
        let win = RawWindowHandle::AppKit(AppKitWindowHandle::new(
          std::ptr::NonNull::new(raw_win).ok_or_else(null_window)?,
        ));
        let display = RawDisplayHandle::AppKit(AppKitDisplayHandle::new());
        Ok((win, display))
      }
      laufey::LAUFEY_WINDOW_HANDLE_WIN32 => {
        use raw_window_handle::*;
        let mut handle = Win32WindowHandle::new(
          std::num::NonZeroIsize::new(raw_win as isize)
            .ok_or_else(null_window)?,
        );
        handle.hinstance = std::num::NonZeroIsize::new(raw_display as isize);
        let win = RawWindowHandle::Win32(handle);
        let display = RawDisplayHandle::Windows(WindowsDisplayHandle::new());
        Ok((win, display))
      }
      laufey::LAUFEY_WINDOW_HANDLE_X11 => {
        use raw_window_handle::*;
        let win = RawWindowHandle::Xlib(XlibWindowHandle::new(raw_win as _));
        let display = RawDisplayHandle::Xlib(XlibDisplayHandle::new(
          std::ptr::NonNull::new(raw_display),
          0,
        ));
        Ok((win, display))
      }
      laufey::LAUFEY_WINDOW_HANDLE_WAYLAND => {
        use raw_window_handle::*;
        let win = RawWindowHandle::Wayland(WaylandWindowHandle::new(
          std::ptr::NonNull::new(raw_win).ok_or_else(null_window)?,
        ));
        let display = RawDisplayHandle::Wayland(WaylandDisplayHandle::new(
          std::ptr::NonNull::new(raw_display).ok_or_else(null_display)?,
        ));
        Ok((win, display))
      }
      other => Err(deno_error::JsErrorBox::generic(format!(
        "unknown Laufey window handle type: {other}",
      ))),
    }
  }

  fn alert(&self, title: &str, message: &str) {
    laufey::alert(title, message);
  }

  fn confirm(&self, title: &str, message: &str) -> bool {
    laufey::confirm(title, message)
  }

  fn prompt(
    &self,
    title: &str,
    message: &str,
    default_value: &str,
  ) -> Option<String> {
    laufey::prompt(title, message, default_value)
  }

  fn set_dock_badge(&self, text: &str) {
    laufey::set_dock_badge(if text.is_empty() { None } else { Some(text) });
  }

  fn bounce_dock(&self, critical: bool) {
    laufey::bounce_dock(if critical {
      laufey::DockBounceType::Critical
    } else {
      laufey::DockBounceType::Informational
    });
  }

  fn set_dock_menu(&self, menu: Option<Vec<denort::desktop::MenuItem>>) {
    match menu {
      Some(menu) => {
        let menu = menu
          .into_iter()
          .map(desktop_menu_item_to_laufey_menu_item)
          .collect::<Vec<_>>();
        let tx = self.event_tx.clone();
        laufey::set_dock_menu(&menu, move |id: &str| {
          let _ = tx.try_send(
            deno_runtime::ops::desktop::DesktopEvent::DockMenuClick {
              id: id.to_string(),
            },
          );
        });
      }
      None => laufey::clear_dock_menu(),
    }
  }

  fn set_dock_visible(&self, visible: bool) {
    laufey::set_dock_visible(visible);
  }

  fn create_tray(&self) -> u32 {
    let tray = laufey::TrayIcon::new();
    let tray_id = tray.id();
    if tray_id == 0 {
      return 0;
    }
    let click_tx = self.event_tx.clone();
    let tray = tray.on_click(move || {
      let _ = click_tx.try_send(
        deno_runtime::ops::desktop::DesktopEvent::TrayClick { tray_id },
      );
    });
    let dblclick_tx = self.event_tx.clone();
    tray.set_double_click_handler(move || {
      let _ = dblclick_tx.try_send(
        deno_runtime::ops::desktop::DesktopEvent::TrayDoubleClick { tray_id },
      );
    });
    self.trays.lock().unwrap().insert(tray_id, tray);
    tray_id
  }

  fn destroy_tray(&self, tray_id: u32) {
    self.trays.lock().unwrap().remove(&tray_id);
  }

  fn set_tray_icon(&self, tray_id: u32, png_bytes: &[u8]) {
    if let Some(tray) = self.trays.lock().unwrap().get(&tray_id) {
      tray.set_icon(png_bytes);
    }
  }

  fn set_tray_icon_dark(&self, tray_id: u32, png_bytes: Option<&[u8]>) {
    if let Some(tray) = self.trays.lock().unwrap().get(&tray_id) {
      tray.set_icon_dark(png_bytes.unwrap_or(&[]));
    }
  }

  fn set_tray_tooltip(&self, tray_id: u32, text: Option<&str>) {
    if let Some(tray) = self.trays.lock().unwrap().get(&tray_id) {
      tray.set_tooltip(text);
    }
  }

  fn set_tray_menu(
    &self,
    tray_id: u32,
    menu: Option<Vec<denort::desktop::MenuItem>>,
  ) {
    let trays = self.trays.lock().unwrap();
    let Some(tray) = trays.get(&tray_id) else {
      return;
    };
    match menu {
      Some(menu) => {
        let menu = menu
          .into_iter()
          .map(desktop_menu_item_to_laufey_menu_item)
          .collect::<Vec<_>>();
        let tx = self.event_tx.clone();
        tray.set_menu(&menu, move |id: &str| {
          let _ = tx.try_send(
            deno_runtime::ops::desktop::DesktopEvent::TrayMenuClick {
              tray_id,
              id: id.to_string(),
            },
          );
        });
      }
      None => tray.clear_menu(),
    }
  }

  fn get_tray_bounds(&self, tray_id: u32) -> Option<(i32, i32, i32, i32)> {
    let trays = self.trays.lock().unwrap();
    trays.get(&tray_id)?.get_bounds()
  }

  fn show_notification(
    &self,
    title: &str,
    body: Option<&str>,
    icon: Option<&[u8]>,
    tag: Option<&str>,
    silent: Option<bool>,
    require_interaction: Option<bool>,
  ) -> u32 {
    let mut builder = laufey::Notification::new(title);
    if let Some(body) = body {
      builder = builder.body(body);
    }
    if let Some(icon) = icon {
      builder = builder.icon(icon.to_vec());
    }
    if let Some(tag) = tag {
      builder = builder.tag(tag);
    }
    if let Some(silent) = silent {
      builder = builder.silent(silent);
    }
    if let Some(require) = require_interaction {
      builder = builder.require_interaction(require);
    }

    // The laufey handler closure receives only the event; it needs the
    // notification id to route the event through the desktop channel.
    // We can't know the id until `on_event` returns, so we capture it
    // through a shared slot populated immediately after.
    let id_slot: Arc<std::sync::OnceLock<u32>> =
      Arc::new(std::sync::OnceLock::new());
    let id_for_handler = id_slot.clone();
    let tx = self.event_tx.clone();
    let notifications = self.notifications.clone();

    let handle = builder.on_event(move |event| {
      let Some(&nid) = id_for_handler.get() else {
        return;
      };
      use laufey::NotificationEvent;
      let desktop_event = match event {
        NotificationEvent::Shown => {
          deno_runtime::ops::desktop::DesktopEvent::NotificationShow {
            notification_id: nid,
          }
        }
        NotificationEvent::Clicked => {
          deno_runtime::ops::desktop::DesktopEvent::NotificationClick {
            notification_id: nid,
          }
        }
        NotificationEvent::Closed => {
          deno_runtime::ops::desktop::DesktopEvent::NotificationClose {
            notification_id: nid,
          }
        }
        // The Web Notification API has no "action" event in window context;
        // surface action button clicks as a click event for compatibility.
        NotificationEvent::Action(_) => {
          deno_runtime::ops::desktop::DesktopEvent::NotificationClick {
            notification_id: nid,
          }
        }
      };
      let is_terminal = matches!(event, laufey::NotificationEvent::Closed);
      let _ = tx.try_send(desktop_event);
      if is_terminal {
        notifications.lock().unwrap().remove(&nid);
      }
    });

    let id = handle.id();
    if id == 0 {
      // Backend doesn't support notifications. Emit a synthetic error
      // event so the user can observe the failure.
      let _ = self.event_tx.try_send(
        deno_runtime::ops::desktop::DesktopEvent::NotificationError {
          notification_id: 0,
        },
      );
      return 0;
    }
    let _ = id_slot.set(id);
    self.notifications.lock().unwrap().insert(id, handle);
    id
  }

  fn close_notification(&self, notification_id: u32) {
    if let Some(handle) =
      self.notifications.lock().unwrap().get(&notification_id)
    {
      handle.close();
    }
  }

  fn request_notification_permission(
    &self,
    cb: Box<
      dyn FnOnce(deno_runtime::ops::desktop::PermissionState) + Send + 'static,
    >,
  ) {
    laufey::request_permission(
      laufey::PermissionKind::Notifications,
      move |status| cb(map_permission_status(status)),
    );
  }

  fn query_notification_permission(
    &self,
    cb: Box<
      dyn FnOnce(deno_runtime::ops::desktop::PermissionState) + Send + 'static,
    >,
  ) {
    laufey::query_permission(
      laufey::PermissionKind::Notifications,
      move |status| cb(map_permission_status(status)),
    );
  }
}

fn map_permission_status(
  status: laufey::PermissionStatus,
) -> deno_runtime::ops::desktop::PermissionState {
  use deno_runtime::ops::desktop::PermissionState;
  match status {
    laufey::PermissionStatus::Granted => PermissionState::Granted,
    laufey::PermissionStatus::Denied => PermissionState::Denied,
    laufey::PermissionStatus::Prompt => PermissionState::Prompt,
    laufey::PermissionStatus::Unsupported => PermissionState::Unsupported,
  }
}

fn desktop_menu_item_to_laufey_menu_item(
  item: denort::desktop::MenuItem,
) -> laufey::MenuItem {
  match item {
    denort::desktop::MenuItem::Item {
      label,
      id,
      accelerator,
      enabled,
    } => laufey::MenuItem::Item {
      label,
      id,
      accelerator,
      enabled,
      checked: false,
      icon: None,
      tooltip: None,
    },
    denort::desktop::MenuItem::Submenu { label, items } => {
      laufey::MenuItem::Submenu {
        label,
        items: items
          .into_iter()
          .map(desktop_menu_item_to_laufey_menu_item)
          .collect(),
      }
    }
    denort::desktop::MenuItem::Separator => laufey::MenuItem::Separator,
    denort::desktop::MenuItem::Role { role } => laufey::MenuItem::Role { role },
  }
}

#[allow(dead_code, reason = "kept alongside v8_to_laufey_value for symmetry")]
fn laufey_value_to_v8<'a>(
  scope: &v8::PinScope<'a, '_>,
  val: laufey::Value,
) -> v8::Local<'a, v8::Value> {
  match val {
    laufey::Value::Null => v8::null(scope).into(),
    laufey::Value::Bool(bool) => v8::Boolean::new(scope, bool).into(),
    laufey::Value::Int(int) => v8::Integer::new(scope, int).into(),
    laufey::Value::Double(double) => v8::Number::new(scope, double).into(),
    laufey::Value::String(str) => v8::String::new(scope, &str).unwrap().into(),
    laufey::Value::List(list) => {
      let elements = list
        .into_iter()
        .map(|v| laufey_value_to_v8(scope, v))
        .collect::<Vec<_>>();
      v8::Array::new_with_elements(scope, &elements).into()
    }
    laufey::Value::Dict(dict) => {
      let mut names = Vec::with_capacity(dict.len());
      let mut values = Vec::with_capacity(dict.len());

      for (k, v) in dict {
        names.push(v8::String::new(scope, &k).unwrap().into());
        values.push(laufey_value_to_v8(scope, v));
      }

      let prototype = v8::null(scope).into();
      v8::Object::with_prototype_and_properties(
        scope, prototype, &names, &values,
      )
      .into()
    }
    laufey::Value::Binary(bin) => {
      let len = bin.len();
      let backing_store = v8::ArrayBuffer::new_backing_store_from_vec(bin);
      let backing_store = backing_store.make_shared();
      let ab = v8::ArrayBuffer::with_backing_store(scope, &backing_store);
      let uint8_array = v8::Uint8Array::new(scope, ab, 0, len).unwrap();
      uint8_array.into()
    }
  }
}

#[allow(dead_code, reason = "kept alongside laufey_value_to_v8 for symmetry")]
fn v8_to_laufey_value<'a>(
  scope: &v8::PinScope<'a, '_>,
  val: v8::Local<'a, v8::Value>,
) -> laufey::Value {
  if val.is_null_or_undefined() {
    laufey::Value::Null
  } else if val.is_boolean() {
    laufey::Value::Bool(val.boolean_value(scope))
  } else if val.is_int32() {
    laufey::Value::Int(val.int32_value(scope).unwrap_or(0))
  } else if val.is_number() {
    laufey::Value::Double(val.number_value(scope).unwrap_or(0.0))
  } else if val.is_string() {
    let s = val.to_rust_string_lossy(scope);
    laufey::Value::String(s)
  } else if val.is_array_buffer_view() {
    let view: v8::Local<v8::ArrayBufferView> = val.try_into().unwrap();
    let len = view.byte_length();
    let mut buf = vec![0u8; len];
    view.copy_contents(&mut buf);
    laufey::Value::Binary(buf)
  } else if val.is_array() {
    let arr: v8::Local<v8::Array> = val.try_into().unwrap();
    let len = arr.length();
    let mut list = Vec::with_capacity(len as usize);
    for i in 0..len {
      if let Some(elem) = arr.get_index(scope, i) {
        list.push(v8_to_laufey_value(scope, elem));
      }
    }
    laufey::Value::List(list)
  } else if val.is_object() {
    let obj: v8::Local<v8::Object> = val.try_into().unwrap();
    let mut map = std::collections::HashMap::new();
    if let Some(names) =
      obj.get_own_property_names(scope, v8::GetPropertyNamesArgs::default())
    {
      for i in 0..names.length() {
        if let Some(key) = names.get_index(scope, i) {
          let key_str = key.to_rust_string_lossy(scope);
          if let Some(value) = obj.get(scope, key) {
            map.insert(key_str, v8_to_laufey_value(scope, value));
          }
        }
      }
    }
    laufey::Value::Dict(map)
  } else {
    // Fallback: coerce to string
    laufey::Value::String(val.to_rust_string_lossy(scope))
  }
}

/// Promote this dylib's symbols to the global symbol scope so that
/// native addons loaded via `dlopen` (e.g. next-swc.node) can resolve
/// NAPI function symbols from our library.
///
/// By default, Laufey loads this dylib without `RTLD_GLOBAL`, so its symbols
/// are only visible within the dylib itself. NAPI addons use
/// `-undefined dynamic_lookup` (macOS) and expect NAPI symbols to be
/// in the global symbol table. Re-opening ourselves with `RTLD_GLOBAL`
/// promotes our exports to global scope.
#[cfg(unix)]
fn promote_dylib_symbols_to_global() {
  #[repr(C)]
  struct DlInfo {
    dli_fname: *const std::ffi::c_char,
    dli_fbase: *mut std::ffi::c_void,
    dli_sname: *const std::ffi::c_char,
    dli_saddr: *mut std::ffi::c_void,
  }
  unsafe extern "C" {
    fn dladdr(
      addr: *const std::ffi::c_void,
      info: *mut DlInfo,
    ) -> std::ffi::c_int;
    fn dlopen(
      path: *const std::ffi::c_char,
      flags: std::ffi::c_int,
    ) -> *mut std::ffi::c_void;
  }
  const RTLD_LAZY: std::ffi::c_int = 0x1;
  const RTLD_NOLOAD: std::ffi::c_int = 0x10;
  const RTLD_GLOBAL: std::ffi::c_int = 0x8;

  // SAFETY:
  // - `dladdr` reads the metadata of the function passed in; a known
  //   function pointer in this dylib is always a valid argument.
  // - `dlopen` with RTLD_NOLOAD doesn't load anything new — it only
  //   bumps the refcount of an already-loaded image, then sets
  //   RTLD_GLOBAL on its symbols. We pass the path returned by
  //   `dladdr` (our own dylib) which is guaranteed loaded since
  //   we're executing in it. On NULL return there's nothing for us
  //   to clean up — the global-symbol-promotion just didn't happen
  //   and any NAPI addon needing those symbols will fail with a
  //   clear "symbol not found" later.
  unsafe {
    let mut info: DlInfo = std::mem::zeroed();
    let addr = promote_dylib_symbols_to_global as *const std::ffi::c_void;
    if dladdr(addr, &mut info) != 0 && !info.dli_fname.is_null() {
      let handle =
        dlopen(info.dli_fname, RTLD_LAZY | RTLD_NOLOAD | RTLD_GLOBAL);
      if handle.is_null() {
        log::debug!(
          "[desktop] dlopen(self, RTLD_NOLOAD|RTLD_GLOBAL) returned NULL; \
           NAPI symbols will not be globally visible"
        );
      }
    }
  }
}

/// Get the filesystem path of this dylib using `dladdr`.
#[cfg(unix)]
fn get_dylib_path() -> Option<PathBuf> {
  #[repr(C)]
  struct DlInfo {
    dli_fname: *const std::ffi::c_char,
    dli_fbase: *mut std::ffi::c_void,
    dli_sname: *const std::ffi::c_char,
    dli_saddr: *mut std::ffi::c_void,
  }
  unsafe extern "C" {
    fn dladdr(
      addr: *const std::ffi::c_void,
      info: *mut DlInfo,
    ) -> std::ffi::c_int;
  }
  // SAFETY: `dladdr` is given a valid function pointer and a stack-allocated
  // `DlInfo`; on success we only read `dli_fname` after null-checking it.
  unsafe {
    let mut info: DlInfo = std::mem::zeroed();
    let addr = get_dylib_path as *const std::ffi::c_void;
    if dladdr(addr, &mut info) != 0 && !info.dli_fname.is_null() {
      let c_str = std::ffi::CStr::from_ptr(info.dli_fname);
      Some(PathBuf::from(c_str.to_string_lossy().into_owned()))
    } else {
      None
    }
  }
}

/// Manages pending updates and rollback on startup.
///
/// Uses a sentinel file (`.update-ok`) to detect if the last update
/// booted successfully:
///
/// - `.update` exists → apply it (current → `.backup`, `.update` → current)
/// - `.backup` exists but `.update-ok` doesn't → last update crashed, rollback
/// - `.backup` exists and `.update-ok` exists → previous update succeeded, clean up
///
/// Returns `true` if a rollback occurred (so we can dispatch an event in JS).
// Only invoked from the unix `laufey::main!` startup path; gated to match its
// sole call site so it isn't flagged as dead code on other platforms.
#[cfg(unix)]
#[allow(clippy::print_stderr, reason = "runs before logging is initialized")]
fn apply_pending_update(dylib_path: &Path) -> bool {
  let ext = dylib_path.extension().unwrap_or_default().to_string_lossy();
  let update_path = dylib_path.with_extension(format!("{}.update", ext));
  let backup_path = dylib_path.with_extension(format!("{}.backup", ext));
  let sentinel_path = dylib_path.with_extension(format!("{}.update-ok", ext));

  if update_path.exists() {
    // New update pending — apply it.
    // Remove stale sentinel so we can detect if *this* update fails.
    let _ = std::fs::remove_file(&sentinel_path);
    let _ = std::fs::remove_file(&backup_path);

    // Hard-link (or copy) the live dylib aside as a backup *without*
    // unlinking the original. If the subsequent swap-in of the update
    // fails (perms, disk full, EXDEV), the running dylib is still in place
    // — previously a rename-then-rename pair could leave the app with no
    // dylib at all on rename #2 failure.
    let backup_ok = std::fs::hard_link(dylib_path, &backup_path).is_ok()
      || std::fs::copy(dylib_path, &backup_path).is_ok();
    if !backup_ok {
      eprintln!("[desktop] could not stage backup, skipping update");
      return false;
    }

    if std::fs::rename(&update_path, dylib_path).is_err() {
      // Rename failed (cross-filesystem / perms / etc.). Fall back to a
      // temp-then-rename copy so the dylib is never observed half-written:
      // copy the update to `<dylib>.update.tmp` on the same filesystem as
      // the dylib, then atomic-rename into place. Only on full success do
      // we consume the staged `.update`.
      let tmp_path = dylib_path.with_extension(format!("{}.update.tmp", ext));
      let copy_ok = std::fs::copy(&update_path, &tmp_path).is_ok()
        && std::fs::rename(&tmp_path, dylib_path).is_ok();
      if copy_ok {
        let _ = std::fs::remove_file(&update_path);
      } else {
        // Couldn't apply the update by rename or copy. Leave `.update` in
        // place so the next launch retries, drop the stale `.tmp`, and
        // delete the unused `.backup` — otherwise the next launch would
        // see backup-without-sentinel and trigger a spurious "rollback"
        // even though we never swapped anything in.
        let _ = std::fs::remove_file(&tmp_path);
        let _ = std::fs::remove_file(&backup_path);
        eprintln!(
          "[desktop] failed to apply staged update; will retry on next launch"
        );
      }
    }
    return false;
  }

  if backup_path.exists() && !sentinel_path.exists() {
    // Last update didn't write the sentinel → it crashed. Rollback.
    eprintln!("[desktop] Last update failed to start, rolling back...");
    let _ = std::fs::rename(&backup_path, dylib_path);
    return true;
  }

  if backup_path.exists() && sentinel_path.exists() {
    // Previous update booted fine — clean up backup and sentinel.
    let _ = std::fs::remove_file(&backup_path);
    let _ = std::fs::remove_file(&sentinel_path);
  }

  false
}

laufey::main!(|| {
  // Apply any pending update before anything else.
  #[cfg(unix)]
  #[allow(clippy::print_stderr, reason = "runs before logging is initialized")]
  let update_rolled_back = {
    match std::panic::catch_unwind(|| {
      if let Some(ref dylib_path) = get_dylib_path() {
        eprintln!("[desktop] dylib path: {:?}", dylib_path);
        apply_pending_update(dylib_path)
      } else {
        eprintln!("[desktop] could not determine dylib path");
        false
      }
    }) {
      Ok(v) => v,
      Err(e) => {
        eprintln!("[desktop] update check panicked: {:?}", e);
        false
      }
    }
  };
  #[cfg(not(unix))]
  let update_rolled_back = false;

  // Make NAPI symbols visible to native addons (e.g. next-swc).
  #[cfg(unix)]
  promote_dylib_symbols_to_global();

  // Guard against re-entry: when a framework dev server (e.g. Next.js)
  // forks child/worker processes, they re-execute this dylib. Detect
  // forked workers and run them headless (no Laufey window).
  //
  // A forked worker is recognized by the *combination* of:
  //   1. argv shaped like `<exe> run [flags…] script.js …` (i.e.
  //      `extract_fork_script_path` returns `Some`), OR
  //   2. argv shaped like `<exe> run …` *and* one of the worker env
  //      vars set by the parent dev server (NODE_CHANNEL_FD,
  //      NEXT_PRIVATE_WORKER).
  //
  // The bare env-var check used to be enough, but a user shell that
  // already had NODE_CHANNEL_FD set (e.g. running inside another forked
  // process, Jest, pnpm) would silently take the headless path and
  // never show a window. Requiring the `run` argv shape rules that out:
  // the Laufey backend never invokes us with `run` as argv[1].
  let args: Vec<_> = env::args_os().collect();
  let argv_run = args
    .get(1)
    .and_then(|a| a.to_str())
    .map(|s| s == "run")
    .unwrap_or(false);
  let is_worker = extract_fork_script_path(&args).is_some()
    || (argv_run
      && (env::var("NODE_CHANNEL_FD").is_ok()
        || env::var("NEXT_PRIVATE_WORKER").is_ok()));
  if is_worker {
    run_on_runtime_thread(run_headless_worker);
    return;
  }

  // Set up panic hook for desktop error reporting.  The error reporting
  // URL is only known after binary metadata is parsed (in run_desktop),
  // so the hook reads from a global that gets set later.
  {
    let orig_hook = std::panic::take_hook();
    #[allow(clippy::print_stderr, reason = "panic hook")]
    std::panic::set_hook(Box::new(move |panic_info| {
      use deno_runtime::ops::desktop::error_report_config;
      use deno_runtime::ops::desktop::send_error_report;

      if let Some((url, app_version)) = error_report_config() {
        let message = if let Some(s) =
          panic_info.payload().downcast_ref::<&str>()
        {
          (*s).to_string()
        } else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
          s.clone()
        } else {
          "Deno runtime panicked".to_string()
        };

        let location = panic_info
          .location()
          .map(|l| format!("at {}:{}:{}", l.file(), l.line(), l.column()));

        let body = deno_core::serde_json::json!({
          "version": 1,
          "message": message,
          "stack": location,
          "appVersion": app_version,
          "platform": env::consts::OS,
          "arch": env::consts::ARCH,
        });
        send_error_report(url, &body.to_string());
      }

      eprintln!(
        "\n============================================================"
      );
      eprintln!("Deno has panicked. This is a bug in Deno. Please report this");
      eprintln!("at https://github.com/denoland/deno/issues/new.");
      eprintln!();
      eprintln!("Platform: {} {}", env::consts::OS, env::consts::ARCH);
      eprintln!();

      orig_hook(panic_info);
      deno_runtime::exit(1);
    }));
  }

  denort::init_logging(None, None);

  deno_runtime::deno_permissions::mark_standalone();

  rustls::crypto::aws_lc_rs::default_provider()
    .install_default()
    .unwrap();

  laufey::set_js_namespace("bindings");

  // Allocate the desktop serve port and publish it via DENO_SERVE_ADDRESS
  // BEFORE the tokio runtime is built. Once the runtime spins up its
  // mio IO thread (and, optionally, the inspector server thread),
  // `setenv` is no longer thread-safe on glibc — Rust 1.81+ marks it
  // unsafe for that reason. We're still single-threaded up to here:
  // the worker-fork path has already returned, and the init calls
  // above (init_logging, mark_standalone, rustls install_default,
  // set_js_namespace) don't spawn threads.
  let desktop_serve_port = match allocate_random_port() {
    Ok(p) => p,
    Err(e) => {
      log::error!("[desktop] failed to allocate serve port: {}", e);
      return;
    }
  };
  // SAFETY: see the block comment above — single-threaded at this point.
  unsafe {
    std::env::set_var(
      "DENO_SERVE_ADDRESS",
      format!("tcp:127.0.0.1:{}", desktop_serve_port),
    );
  }

  // Read the embedded standalone section, extract the VFS, and chdir
  // into the extraction dir — all BEFORE the tokio runtime starts.
  // chdir is process-wide; doing it after the runtime build (and any
  // worker / async tasks it spawns) would race with code that resolves
  // relative paths.
  let args: Vec<_> = env::args_os().collect();
  let data = match denort::binary::extract_standalone_with_finder(
    Cow::Owned(args),
    find_section_in_dylib,
  ) {
    Ok(data) => data,
    Err(e) => {
      log::error!("[desktop] failed to read standalone section: {:?}", e);
      return;
    }
  };
  if data.metadata.self_extracting.is_some() {
    if let Err(e) =
      denort::binary::extract_vfs_to_disk(&data.vfs, &data.root_path)
    {
      log::error!("[desktop] failed to extract VFS: {:?}", e);
      return;
    }
    // Frameworks like Next.js look for build output (e.g. .next/)
    // relative to CWD.
    if let Err(e) = std::env::set_current_dir(&data.root_path) {
      log::error!(
        "[desktop] failed to chdir to {}: {}",
        data.root_path.display(),
        e
      );
      return;
    }
  }

  // Everything above must stay on this (still effectively single-threaded)
  // loader thread — see the setenv comment. The runtime itself moves to a
  // dedicated thread with a real stack; the loader thread just parks in
  // `join` (inside `run_on_runtime_thread`) until the app exits.
  run_on_runtime_thread(move || {
    let rt = deno_runtime::tokio_util::create_basic_runtime();

    rt.block_on(async {
      log::debug!("[desktop] run_desktop starting");
      match run_desktop(update_rolled_back, desktop_serve_port, data).await {
        Ok(()) => log::debug!("[desktop] run_desktop completed OK"),
        Err(error) => {
          // A failure raised while the app's main module was still loading or
          // first evaluating is tagged `DesktopStartupError` (see
          // `denort::run`). It carries its own pre-formatted message.
          let startup =
            error.downcast_ref::<denort::run::DesktopStartupError>();
          let error_string = match startup {
            Some(startup) => startup.message.clone(),
            None => match js_error_downcast_ref(&error) {
              Some(js_error) => format_js_error(js_error, None),
              None => format!("{:?}", error),
            },
          };
          let message = error_string.trim_start_matches("error: ");
          log::error!("{}: {}", colors::red_bold("error"), message);
          let is_startup = startup.is_some();
          // A `DesktopStartupError` wraps the formatted message rather than the
          // original `JsError`, so only consult `js_error_downcast_ref` for the
          // untagged (post-load) case.
          let is_js_error =
            !is_startup && js_error_downcast_ref(&error).is_some();
          if should_show_native_error_dialog(is_startup, is_js_error) {
            laufey::alert("Application Error", message);
          }
        }
      }
    });
  });
});

/// Decide whether the desktop shell should pop a native error dialog for a
/// failure that propagated out of the runtime.
///
/// - Startup failures (tagged `DesktopStartupError`; see its docs) are
///   surfaced here because the app's own `error` / `unhandledrejection`
///   listeners never ran to report them.
/// - Non-JS crashes are surfaced too.
/// - A post-load JS error is left to the app's own listeners, which already
///   show a dialog; alerting again here would just duplicate it.
fn should_show_native_error_dialog(
  is_startup: bool,
  is_js_error: bool,
) -> bool {
  is_startup || !is_js_error
}

/// Stack size for the thread the Deno runtime runs on. Laufey invokes
/// `laufey_runtime_start` on its RuntimeLoader thread, which gets the
/// platform-default stack for secondary threads — only 512KB on macOS.
/// That is far too small for the runtime: synchronous module parsing (swc)
/// alone recurses past it on real-world module graphs (e.g. a SvelteKit
/// dev server's graph) and kills the process with SIGBUS. 8MB matches the
/// headroom the CLI gets from the OS main thread.
const RUNTIME_THREAD_STACK_SIZE: usize = 8 * 1024 * 1024;

/// Run `f` to completion on a dedicated thread with a stack large enough
/// for the Deno runtime (see `RUNTIME_THREAD_STACK_SIZE`), keeping the
/// calling (loader) thread parked until it finishes.
fn run_on_runtime_thread<F: FnOnce() + Send + 'static>(f: F) {
  let thread = std::thread::Builder::new()
    .name("deno-desktop-runtime".to_string())
    .stack_size(RUNTIME_THREAD_STACK_SIZE)
    .spawn(f)
    .expect("failed to spawn desktop runtime thread");
  if thread.join().is_err() {
    // The runtime thread panicked. The panic hook normally exits the
    // process itself; this is a backstop in case it returned.
    deno_runtime::exit(1);
  }
}

/// Run as a headless worker (no Laufey window). Used when a framework dev
/// server forks child processes that re-execute this dylib.
fn run_headless_worker() {
  denort::init_logging(None, None);
  deno_runtime::deno_permissions::mark_standalone();
  rustls::crypto::aws_lc_rs::default_provider()
    .install_default()
    .unwrap();

  let rt = deno_runtime::tokio_util::create_basic_runtime();

  rt.block_on(async {
    let args: Vec<_> = env::args_os().collect();
    log::debug!("[worker] args: {:?}", args);
    #[allow(
      clippy::disallowed_methods,
      reason = "debug trace of the worker's current directory"
    )]
    let cwd = env::current_dir();
    log::debug!("[worker] cwd: {:?}", cwd);

    // Detect if this is a child_process.fork() invocation.
    // fork() translates args to: ["run", "-A", "--unstable-...", "script.js", ...]
    // Extract the script path so the forked worker runs the correct module
    // instead of the embedded entrypoint.
    let fork_module = extract_fork_script_path(&args);
    log::debug!("[worker] fork_module: {:?}", fork_module);

    let data = match denort::binary::extract_standalone_with_finder(
      Cow::Owned(args),
      find_section_in_dylib,
    ) {
      Ok(data) => data,
      Err(e) => {
        log::error!("Worker failed to load standalone data: {:?}", e);
        return;
      }
    };

    denort::load_env_vars(&data.metadata.env_vars_from_env_file);

    let sys = if data.metadata.self_extracting.is_some() {
      // VFS should already be extracted by the parent process.
      // In dev mode (external dev server via DENO_DESKTOP_DEV_URL or
      // in-runtime dev server via DENO_DESKTOP_FRAMEWORK_DEV), keep the
      // source directory as CWD (inherited from parent). In production
      // mode, set CWD to extraction directory.
      if env::var("DENO_DESKTOP_DEV_URL").is_err()
        && env::var("DENO_DESKTOP_FRAMEWORK_DEV").is_err()
      {
        let _ = std::env::set_current_dir(&data.root_path);
      }
      denort::file_system::DenoRtSys::new_self_extracting(data.vfs.clone())
    } else {
      denort::file_system::DenoRtSys::new(data.vfs.clone())
    };

    let options = denort::run::RunOptions {
      override_main_module: fork_module,
      ..Default::default()
    };

    log::debug!("[worker] starting run_with_options");

    match denort::run::run_with_options(
      Arc::new(sys.clone()),
      sys,
      data,
      options,
    )
    .await
    {
      Ok(exit_code) => {
        log::debug!(
          "[worker] run_with_options completed with exit code: {}",
          exit_code
        );
      }
      Err(error) => {
        let error_string = match js_error_downcast_ref(&error) {
          Some(js_error) => format_js_error(js_error, None),
          None => format!("{:?}", error),
        };
        log::debug!("[worker] run_with_options error: {}", error_string);
        log::error!(
          "{}: {}",
          colors::red_bold("error"),
          error_string.trim_start_matches("error: ")
        );
      }
    }
    log::debug!("[worker] block_on finished");
  });
  log::debug!("[worker] run_headless_worker returning");
}

/// Extract the script path from fork'd process arguments.
///
/// When `child_process.fork(scriptPath)` is called, the args are translated
/// to Deno CLI args: `["<exe>", "run", "-A", "--unstable-...", "script.js", ...]`
/// This function finds the script path (first non-flag arg after "run").
fn extract_fork_script_path(
  args: &[std::ffi::OsString],
) -> Option<deno_core::url::Url> {
  let args: Vec<String> = args
    .iter()
    .filter_map(|a| a.to_str().map(String::from))
    .collect();

  // Skip argv[0] (the executable), expect "run" as the subcommand
  if args.len() < 3 || args[1] != "run" {
    return None;
  }

  // Find the first arg after "run" that isn't a flag
  for arg in &args[2..] {
    if arg.starts_with('-') {
      continue;
    }
    // This is the script path
    let path = PathBuf::from(arg);
    let path = if path.is_absolute() {
      path
    } else {
      #[allow(
        clippy::disallowed_methods,
        reason = "denort_desktop has no resolve_cwd helper; resolves a relative fork-script path"
      )]
      let cwd = env::current_dir().ok()?;
      cwd.join(path)
    };
    return deno_core::url::Url::from_file_path(path).ok();
  }
  None
}

/// Convert a laufey::Value to a DesktopValue for direct V8 conversion.
fn laufey_value_to_desktop_value(
  v: laufey::Value,
) -> deno_runtime::ops::desktop::DesktopValue {
  use deno_runtime::ops::desktop::DesktopValue;
  match v {
    laufey::Value::Null => DesktopValue::Null,
    laufey::Value::Bool(b) => DesktopValue::Bool(b),
    laufey::Value::Int(i) => DesktopValue::Int(i),
    laufey::Value::Double(d) => DesktopValue::Double(d),
    laufey::Value::String(s) => DesktopValue::String(s),
    laufey::Value::List(l) => DesktopValue::List(
      l.into_iter().map(laufey_value_to_desktop_value).collect(),
    ),
    laufey::Value::Dict(d) => DesktopValue::Dict(
      d.into_iter()
        .map(|(k, v)| (k, laufey_value_to_desktop_value(v)))
        .collect(),
    ),
    laufey::Value::Binary(b) => DesktopValue::Binary(b),
  }
}

/// Convert a laufey::Value to a serde_json::Value for channel transport.
fn laufey_value_to_json(v: &laufey::Value) -> serde_json::Value {
  match v {
    laufey::Value::Null => serde_json::Value::Null,
    laufey::Value::Bool(b) => serde_json::Value::Bool(*b),
    laufey::Value::Int(i) => serde_json::json!(*i),
    laufey::Value::Double(d) => serde_json::json!(*d),
    laufey::Value::String(s) => serde_json::Value::String(s.clone()),
    laufey::Value::List(l) => {
      serde_json::Value::Array(l.iter().map(laufey_value_to_json).collect())
    }
    laufey::Value::Dict(d) => {
      let mut map = serde_json::Map::new();
      for (k, v) in d {
        map.insert(k.clone(), laufey_value_to_json(v));
      }
      serde_json::Value::Object(map)
    }
    laufey::Value::Binary(b) => serde_json::json!(b),
  }
}

/// Convert a serde_json::Value to a laufey::Value for the menu template.
fn json_to_laufey_value(v: &serde_json::Value) -> laufey::Value {
  match v {
    serde_json::Value::Null => laufey::Value::Null,
    serde_json::Value::Bool(b) => laufey::Value::Bool(*b),
    serde_json::Value::Number(n) => {
      if let Some(i) = n.as_i64() {
        laufey::Value::Int(i as i32)
      } else {
        laufey::Value::Double(n.as_f64().unwrap_or(0.0))
      }
    }
    serde_json::Value::String(s) => laufey::Value::String(s.clone()),
    serde_json::Value::Array(arr) => {
      laufey::Value::List(arr.iter().map(json_to_laufey_value).collect())
    }
    serde_json::Value::Object(obj) => {
      let mut map = std::collections::HashMap::new();
      for (k, v) in obj {
        map.insert(k.clone(), json_to_laufey_value(v));
      }
      laufey::Value::Dict(map)
    }
  }
}

/// Find the embedded data section in this dylib (not the main executable).
fn find_section_in_dylib() -> Result<&'static [u8], AnyError> {
  match libsui::find_section_in_current_image("d3n0l4nd")
    .context("Failed reading standalone binary section from dylib.")
  {
    Ok(Some(data)) => Ok(data),
    Ok(None) => {
      bail!("Could not find standalone binary section in dylib.")
    }
    Err(err) => Err(err),
  }
}

async fn run_desktop(
  update_rolled_back: bool,
  desktop_serve_port: u16,
  data: denort::binary::StandaloneData,
) -> Result<(), AnyError> {
  // Make the error reporting URL available to the panic hook.
  if let Some(ref url) = data.metadata.error_reporting_url {
    deno_runtime::ops::desktop::set_error_report_config(
      url.clone(),
      data.metadata.app_version.clone(),
    );
  }

  // The VFS extract + chdir for self-extracting bundles happens in
  // `laufey::main!` before the tokio runtime is built — chdir is
  // process-wide and isn't safe to do once async tasks are running.
  let sys = if data.metadata.self_extracting.is_some() {
    denort::file_system::DenoRtSys::new_self_extracting(data.vfs.clone())
  } else {
    denort::file_system::DenoRtSys::new(data.vfs.clone())
  };

  deno_runtime::deno_telemetry::init(
    &sys,
    otel_runtime_config(),
    data.metadata.otel_config.clone(),
  )?;
  denort::init_logging(
    data.metadata.log_level,
    Some(data.metadata.otel_config.clone()),
  );
  denort::load_env_vars(&data.metadata.env_vars_from_env_file);

  // Wire up the Deno-side inspector when launched under
  // `deno desktop --inspect[-brk|-wait]`. The parent process binds the
  // user-visible port and runs a multiplexer that fronts both this
  // inspector and the CEF renderer's debug port; we just listen on the
  // internal port that the parent allocated for us.
  // Surface a malformed value loudly: previously a typoed port silently
  // disabled the inspector and the user wondered why DevTools showed
  // nothing. Bail rather than no-op so the failure is visible.
  let inspect_internal_port = match env::var(
    "DENO_DESKTOP_INSPECT_INTERNAL_PORT",
  ) {
    Ok(s) => match s.parse::<std::net::SocketAddr>() {
      Ok(addr) => Some(addr),
      Err(e) => {
        bail!(
          "DENO_DESKTOP_INSPECT_INTERNAL_PORT={s:?} is not a valid SocketAddr: {e}"
        );
      }
    },
    Err(_) => None,
  };
  let inspect_brk = env::var("DENO_DESKTOP_INSPECT_BRK").is_ok();
  let inspect_wait = env::var("DENO_DESKTOP_INSPECT_WAIT").is_ok();
  if let Some(addr) = inspect_internal_port {
    deno_runtime::deno_inspector_server::create_inspector_server(
      addr,
      "deno-desktop",
      // Don't print the ws:// URL ourselves — DevTools attaches via the
      // parent mux's user-visible port.
      deno_runtime::deno_inspector_server::InspectPublishUid {
        console: false,
        http: true,
      },
    )?;
    log::debug!("[desktop] inspector server bound on {addr}");
  }

  // DENO_SERVE_ADDRESS is published by `laufey::main!` before the
  // tokio runtime is built — see the comment there for why we can't
  // do it from here. `desktop_serve_port` is the port we put into it.

  // Enable HMR if DENO_DESKTOP_HMR is set to a directory path
  // (set by `deno compile --desktop --hmr`).
  let hmr_watch_dir = env::var("DENO_DESKTOP_HMR").ok().map(PathBuf::from);

  // Framework dev servers handle their own HMR via websocket.
  // For non-framework apps, V8-level HMR reloads the webview.
  //
  // Two framework-dev shapes exist (see `run_desktop_hmr` in
  // cli/tools/desktop.rs):
  // - DENO_DESKTOP_DEV_URL: the CLI spawned an external dev-server process
  //   and parsed its URL; navigate there directly.
  // - DENO_DESKTOP_FRAMEWORK_DEV: the embedded entrypoint boots the dev
  //   server inside this runtime on the desktop serve port (so server code
  //   keeps `Deno.desktop`, #35899); use the regular serve-port poll.
  let external_dev_url = env::var("DENO_DESKTOP_DEV_URL").ok();
  let is_framework_dev = external_dev_url.is_some()
    || env::var("DENO_DESKTOP_FRAMEWORK_DEV").is_ok();

  // In dev mode, restore CWD to the source directory so the framework
  // dev server watches the original source files, not the extracted VFS.
  if is_framework_dev && let Ok(source_dir) = env::var("DENO_DESKTOP_HMR") {
    std::env::set_current_dir(&source_dir)?;
  }

  // Shared initial window ID for navigate_fut and HMR reload.
  let initial_window_id = Arc::new(AtomicU32::new(0));
  let initial_window_id_for_navigate = initial_window_id.clone();

  // Track every live window so HMR can refresh secondary windows too,
  // not just the initial one. The same Arc is handed to WefDesktopApi
  // below; create_window / close_window keep it in sync.
  let open_windows: Arc<Mutex<HashSet<u32>>> =
    Arc::new(Mutex::new(HashSet::new()));
  let open_windows_for_api = open_windows.clone();
  let open_windows_for_hmr = open_windows.clone();

  let hmr_on_reload: Option<denort::hmr::HmrReloadCallback> =
    if hmr_watch_dir.is_some() && !is_framework_dev {
      Some(Box::new(move || {
        let ids: Vec<u32> = open_windows_for_hmr
          .lock()
          .unwrap()
          .iter()
          .copied()
          .collect();
        for id in ids {
          laufey::Window::from_id(id)
            .execute_js::<fn(Result<laufey::Value, laufey::Value>)>(
              "location.reload()",
              None,
            );
        }
      }))
    } else {
      None
    };

  // Desktop extension: provides Deno.desktop.* APIs + auto-update
  #[cfg(unix)]
  let auto_update_state = get_dylib_path().map(|p| {
    denort::desktop::AutoUpdateState {
      dylib_path: p,
      app_version: data.metadata.app_version.clone(),
      rolled_back: update_rolled_back, // from laufey::main! startup check
    }
  });
  #[cfg(not(unix))]
  let auto_update_state: Option<denort::desktop::AutoUpdateState> = {
    // Auto-update rollback is only wired up on unix; consume the flag so the
    // parameter isn't flagged as unused on other platforms.
    let _ = update_rolled_back;
    None
  };

  let auto_update_version = auto_update_state
    .as_ref()
    .and_then(|s| s.app_version.clone());
  let auto_update_rolled_back =
    auto_update_state.as_ref().is_some_and(|s| s.rolled_back);

  // App name (deno.json `desktop.app.name`, baked in at compile time) used as
  // the default window title. Moved into the op_state_init closure below.
  let app_name = data.metadata.app_name.clone();

  let run_opts = RunOptions {
    auto_serve: true,
    serve_port: Some(desktop_serve_port),
    serve_host: Some("127.0.0.1".to_string()),
    hmr_watch_dir: if is_framework_dev {
      None
    } else {
      hmr_watch_dir
    },
    hmr_on_reload,
    op_state_init: Some(Box::new(move |state| {
      let (event_tx, event_rx) =
        denort::desktop::create_desktop_event_channel();
      let pending_responses = denort::desktop::PendingBindResponses::new();
      let api = WefDesktopApi {
        event_tx: event_tx.0.clone(),
        pending_responses: pending_responses.clone(),
        closed_windows: Arc::new(Mutex::new(HashSet::new())),
        open_windows: open_windows_for_api.clone(),
        trays: Arc::new(Mutex::new(HashMap::new())),
        notifications: Arc::new(Mutex::new(HashMap::new())),
        devtools_window: Mutex::new(None),
      };

      // Forward macOS dock-reopen callbacks (clicking the dock icon while
      // no windows are visible) into the shared event channel so JS can
      // observe them as `Deno.dock` "reopen" events.
      {
        let reopen_tx = event_tx.0.clone();
        laufey::on_dock_reopen(move |has_visible_windows| {
          let _ = reopen_tx.try_send(
            deno_runtime::ops::desktop::DesktopEvent::DockReopen {
              has_visible_windows,
            },
          );
        });
      }

      // Create the initial window (hidden) and wire up event handlers. It is
      // revealed from its `on_page_load` handler once content has painted, so
      // the user never sees the empty pre-navigation frame (issue #35530).
      let window_id = api.create_initial_window(800, 600);
      initial_window_id.store(window_id, Ordering::Release);

      // Title the initial window with the app name up front, so an app that
      // never constructs its own `BrowserWindow` still shows "MyApp" rather
      // than the backend's internal default (`laufey_webview`). The
      // `BrowserWindow` constructor applies the same default to any further
      // untitled windows via `DesktopAppName` in op state.
      if let Some(name) = app_name.as_deref().filter(|n| !n.is_empty()) {
        api.set_title(window_id, name);
      }

      denort::desktop::init_desktop_state(
        state,
        Box::new(api),
        auto_update_state,
      );
      state.put(event_rx);
      state.put(event_tx);
      state.put(pending_responses);
      state.put(denort::desktop::InitialWindowId(std::sync::Mutex::new(
        Some(window_id),
      )));
      if let Some(name) = app_name.filter(|n| !n.is_empty()) {
        state.put(deno_runtime::ops::desktop::DesktopAppName(name));
      }
    })),
    override_main_module: None,
    auto_update_version,
    auto_update_rolled_back,
    error_reporting_url: data.metadata.error_reporting_url.clone(),
    release_base_url: data.metadata.release_base_url.clone(),
    inspect_brk,
    inspect_wait,
    is_inspecting: inspect_internal_port.is_some(),
  };

  // Run the Deno runtime and Laufey event loop concurrently.
  // We spawn the runtime first, wait for the server to be ready,
  // then navigate the webview.
  let poll_serve_port = external_dev_url.is_none();
  let url = external_dev_url
    .unwrap_or_else(|| format!("http://127.0.0.1:{}", desktop_serve_port));
  log::debug!("[desktop] starting runtime and laufey event loop");
  let run_fut =
    denort::run::run_with_options(Arc::new(sys.clone()), sys, data, run_opts);
  let laufey_fut = laufey::run();

  // Wait for the server to be ready, then navigate the initial window.
  // Do a full HTTP request instead of just a TCP connect — frameworks
  // like Vite accept connections before they're ready to serve.
  let wait_for_debugger = inspect_brk || inspect_wait;
  let mux_addr = env::var("DENO_DESKTOP_MUX_WS").ok();
  let navigate_fut = async move {
    use tokio::io::AsyncReadExt;
    use tokio::io::AsyncWriteExt;

    // When --inspect-wait or --inspect-brk: block until a DevTools
    // client has connected to the mux. This prevents the renderer
    // from racing ahead while the developer is still opening DevTools.
    if wait_for_debugger && let Some(ref mux) = mux_addr {
      log::info!("[desktop] Waiting for debugger to attach on ws://{mux} …");
      loop {
        if let Ok(mut stream) =
          tokio::net::TcpStream::connect(mux.as_str()).await
        {
          let req = format!(
            "GET /debugger-attached HTTP/1.1\r\nHost: {mux}\r\nConnection: close\r\n\r\n",
          );
          if stream.write_all(req.as_bytes()).await.is_ok() {
            let mut buf = vec![0u8; 128];
            if let Ok(n) = stream.read(&mut buf).await {
              let resp = String::from_utf8_lossy(&buf[..n]);
              if resp.starts_with("HTTP/1.1 200")
                || resp.starts_with("HTTP/1.0 200")
              {
                log::info!("[desktop] Debugger attached");
                break;
              }
            }
          }
        }
        tokio::time::sleep(std::time::Duration::from_millis(200)).await;
      }
    }

    let id = initial_window_id_for_navigate.load(Ordering::Acquire);
    if poll_serve_port {
      let mut server_ready = false;
      for i in 0..60 {
        if let Ok(mut stream) =
          tokio::net::TcpStream::connect(("127.0.0.1", desktop_serve_port))
            .await
        {
          let req = format!(
            "GET / HTTP/1.1\r\nHost: 127.0.0.1:{}\r\nConnection: close\r\n\r\n",
            desktop_serve_port
          );
          if stream.write_all(req.as_bytes()).await.is_ok() {
            let mut buf = vec![0u8; 256];
            if let Ok(n) = stream.read(&mut buf).await {
              let response = String::from_utf8_lossy(&buf[..n]);
              if response.starts_with("HTTP/1.1 2")
                || response.starts_with("HTTP/1.1 3")
                || response.starts_with("HTTP/1.0 2")
                || response.starts_with("HTTP/1.0 3")
              {
                log::debug!(
                  "[desktop] Server ready after {} attempts, navigating to {}",
                  i + 1,
                  &url
                );
                server_ready = true;
                break;
              }
            }
          }
        }
        tokio::time::sleep(std::time::Duration::from_millis(250)).await;
      }
      if !server_ready {
        log::warn!("Server not ready after 15s, navigating anyway");
      }
    }
    laufey::Window::from_id(id).navigate(&url);

    // The window was created hidden and is normally revealed from its
    // `on_page_load` handler the moment content paints. If that load never
    // finishes (e.g. the initial navigation errors), fall back to showing it
    // anyway so the user isn't left with no window at all. `show()` is
    // idempotent, so racing the on_page_load reveal is harmless.
    tokio::time::sleep(std::time::Duration::from_secs(10)).await;
    laufey::Window::from_id(id).show();
  };

  // Hold the JoinHandle so we can abort it when the runtime / Laufey
  // event loop ends — otherwise the navigate poll keeps trying for up
  // to 15s after window close, writing warnings to a stderr that may
  // already be torn down.
  let navigate_handle = tokio::spawn(navigate_fut);

  tokio::select! {
    result = run_fut => {
      match result {
        Ok(exit_code) => {
          log::debug!("[desktop] Deno runtime exited with code {}", exit_code);
        }
        Err(err) => {
          log::error!("[desktop] Deno runtime error: {:?}", err);
          navigate_handle.abort();
          return Err(err);
        }
      }
    }
    _ = laufey_fut => {
      log::debug!("[desktop] Laufey event loop ended (window closed)");
    }
  }
  navigate_handle.abort();

  Ok(())
}

#[cfg(test)]
mod tests {
  use std::ffi::OsString;

  use deno_runtime::ops::desktop::PermissionState;

  use super::desktop_menu_item_to_laufey_menu_item;
  use super::extract_fork_script_path;
  use super::json_to_laufey_value;
  use super::laufey_value_to_desktop_value;
  use super::laufey_value_to_json;
  use super::map_permission_status;
  use super::should_show_native_error_dialog;

  // --- should_show_native_error_dialog ---
  //
  // See `should_show_native_error_dialog` / `DesktopStartupError` for the
  // rationale. In short: startup failures must be surfaced (the app never got
  // to report them), while post-load JS errors are left to the app's own
  // listeners so we don't show a duplicate dialog.

  #[test]
  fn startup_js_error_is_surfaced() {
    // A failed import / link error / top-level throw during the load phase
    // is tagged as a startup error and never reaches the app's listeners,
    // so the shell must show the dialog itself.
    assert!(should_show_native_error_dialog(true, false));
  }

  #[test]
  fn post_load_js_error_is_left_to_the_app() {
    // An uncaught error after the app loaded is reported by the app's own
    // error/unhandledrejection listeners: a second dialog here would just
    // duplicate it.
    assert!(!should_show_native_error_dialog(false, true));
  }

  #[test]
  fn non_js_crash_is_surfaced() {
    // A non-JS runtime crash has no JS listener to report it.
    assert!(should_show_native_error_dialog(false, false));
  }

  // --- extract_fork_script_path ---
  //
  // Framework dev servers (e.g. Next.js) call `child_process.fork()` to
  // re-exec this binary as a worker process. We detect that pattern by
  // argv shape: `<exe> run [flags…] script.js …`. Mis-classifying the
  // main launch as a worker would mean no window appears at all.

  fn args(parts: &[&str]) -> Vec<OsString> {
    parts.iter().map(|s| OsString::from(*s)).collect()
  }

  #[test]
  fn fork_argv_shape_returns_script_url() {
    let v =
      extract_fork_script_path(&args(&["myapp", "run", "/tmp/worker.js"]));
    let url = v.expect("argv with `run <abs-script>` is a worker");
    assert!(url.as_str().ends_with("worker.js"));
    assert_eq!(url.scheme(), "file");
  }

  #[test]
  fn fork_argv_skips_flags_before_script() {
    let v = extract_fork_script_path(&args(&[
      "myapp",
      "run",
      "--allow-net",
      "-A",
      "/tmp/worker.js",
    ]));
    assert!(v.is_some(), "flags before the script path must be skipped");
  }

  #[test]
  fn fork_argv_rejects_non_run_subcommand() {
    // Only `<exe> run …` is the worker shape. The main desktop launch
    // never has a subcommand at argv[1] — laufey passes no argv at all.
    assert!(extract_fork_script_path(&args(&["myapp"])).is_none());
    assert!(
      extract_fork_script_path(&args(&["myapp", "task", "build"])).is_none()
    );
    assert!(extract_fork_script_path(&args(&["myapp", "test"])).is_none());
  }

  #[test]
  fn fork_argv_run_with_only_flags_is_not_a_fork() {
    // `<exe> run --help` has no script path — don't claim this is a
    // worker.
    let v = extract_fork_script_path(&args(&["myapp", "run", "--help"]));
    assert!(
      v.is_none(),
      "argv with only flags after `run` is not a fork worker"
    );
  }

  #[test]
  fn fork_argv_bare_run_is_not_a_fork() {
    // `<exe> run` with no further args isn't enough to be a fork.
    assert!(extract_fork_script_path(&args(&["myapp", "run"])).is_none());
  }

  // --- map_permission_status ---

  #[test]
  fn permission_status_maps_one_to_one() {
    // Every laufey PermissionStatus must round-trip to the matching
    // deno PermissionState. Off-by-one swaps here would surface in JS
    // as "granted shows up as denied" — silent and very confusing.
    assert!(matches!(
      map_permission_status(laufey::PermissionStatus::Granted),
      PermissionState::Granted
    ));
    assert!(matches!(
      map_permission_status(laufey::PermissionStatus::Denied),
      PermissionState::Denied
    ));
    assert!(matches!(
      map_permission_status(laufey::PermissionStatus::Prompt),
      PermissionState::Prompt
    ));
    assert!(matches!(
      map_permission_status(laufey::PermissionStatus::Unsupported),
      PermissionState::Unsupported
    ));
  }

  // --- desktop_menu_item_to_laufey_menu_item ---

  #[test]
  fn menu_item_conversion_preserves_fields() {
    let item = denort::desktop::MenuItem::Item {
      label: "Save".into(),
      id: Some("file.save".into()),
      accelerator: Some("CmdOrCtrl+S".into()),
      enabled: true,
    };
    match desktop_menu_item_to_laufey_menu_item(item) {
      laufey::MenuItem::Item {
        label,
        id,
        accelerator,
        enabled,
        ..
      } => {
        assert_eq!(label, "Save");
        assert_eq!(id.as_deref(), Some("file.save"));
        assert_eq!(accelerator.as_deref(), Some("CmdOrCtrl+S"));
        assert!(enabled);
      }
      _ => panic!("expected Item"),
    }
  }

  #[test]
  fn menu_item_conversion_recurses_into_submenus() {
    let item = denort::desktop::MenuItem::Submenu {
      label: "File".into(),
      items: vec![
        denort::desktop::MenuItem::Item {
          label: "Open".into(),
          id: Some("open".into()),
          accelerator: None,
          enabled: false,
        },
        denort::desktop::MenuItem::Separator,
        denort::desktop::MenuItem::Role {
          role: "quit".into(),
        },
      ],
    };
    let converted = desktop_menu_item_to_laufey_menu_item(item);
    let laufey::MenuItem::Submenu { items, .. } = converted else {
      panic!("expected Submenu");
    };
    assert_eq!(items.len(), 3);
    // First child is an Item with enabled=false preserved.
    match &items[0] {
      laufey::MenuItem::Item { enabled, label, .. } => {
        assert_eq!(label, "Open");
        assert!(!enabled, "enabled=false must propagate through recursion");
      }
      _ => panic!("first child should be Item"),
    }
    assert!(matches!(items[1], laufey::MenuItem::Separator));
    match &items[2] {
      laufey::MenuItem::Role { role } => assert_eq!(role, "quit"),
      _ => panic!("third child should be Role"),
    }
  }

  // --- laufey_value_to_desktop_value / laufey_value_to_json / json_to_laufey_value ---

  #[test]
  fn laufey_value_to_desktop_value_covers_every_variant() {
    use deno_runtime::ops::desktop::DesktopValue;
    assert!(matches!(
      laufey_value_to_desktop_value(laufey::Value::Null),
      DesktopValue::Null
    ));
    assert!(matches!(
      laufey_value_to_desktop_value(laufey::Value::Bool(true)),
      DesktopValue::Bool(true)
    ));
    assert!(matches!(
      laufey_value_to_desktop_value(laufey::Value::Int(7)),
      DesktopValue::Int(7)
    ));
    assert!(matches!(
      laufey_value_to_desktop_value(laufey::Value::Double(1.5)),
      DesktopValue::Double(d) if d == 1.5
    ));
    match laufey_value_to_desktop_value(laufey::Value::String("hi".into())) {
      DesktopValue::String(s) => assert_eq!(s, "hi"),
      _ => panic!(),
    }
    match laufey_value_to_desktop_value(laufey::Value::Binary(vec![1, 2, 3])) {
      DesktopValue::Binary(b) => assert_eq!(b, vec![1, 2, 3]),
      _ => panic!(),
    }
  }

  #[test]
  fn laufey_value_to_desktop_value_recurses() {
    use deno_runtime::ops::desktop::DesktopValue;
    let v = laufey::Value::List(vec![
      laufey::Value::Int(1),
      laufey::Value::List(vec![laufey::Value::String("nested".into())]),
    ]);
    let dv = laufey_value_to_desktop_value(v);
    let DesktopValue::List(outer) = dv else {
      panic!("outer must be List")
    };
    assert_eq!(outer.len(), 2);
    let DesktopValue::List(inner) = &outer[1] else {
      panic!("second element must be a nested List")
    };
    match &inner[0] {
      DesktopValue::String(s) => assert_eq!(s, "nested"),
      _ => panic!("inner element should be String"),
    }
  }

  #[test]
  fn laufey_value_to_json_roundtrip() {
    use std::collections::HashMap;
    let mut dict = HashMap::new();
    dict.insert("name".to_string(), laufey::Value::String("ada".into()));
    dict.insert("count".to_string(), laufey::Value::Int(42));
    dict.insert("ok".to_string(), laufey::Value::Bool(true));
    let v = laufey::Value::Dict(dict);
    let j = laufey_value_to_json(&v);
    assert_eq!(j["name"], "ada");
    assert_eq!(j["count"], 42);
    assert_eq!(j["ok"], true);

    // Round-trip back through json_to_laufey_value to confirm symmetry on
    // the simple types.
    let back = json_to_laufey_value(&j);
    let laufey::Value::Dict(d) = back else {
      panic!("round-trip must yield Dict")
    };
    match d.get("name") {
      Some(laufey::Value::String(s)) => assert_eq!(s, "ada"),
      _ => panic!("name must round-trip as String"),
    }
    match d.get("count") {
      // json_to_laufey_value maps integer numbers to Int via as_i64() — so
      // it should land in the Int branch.
      Some(laufey::Value::Int(42)) => {}
      _ => panic!("count round-trip should yield laufey::Value::Int(42)"),
    }
  }

  #[test]
  fn json_to_laufey_value_distinguishes_int_and_double() {
    let n_int = serde_json::json!(42);
    let n_float = serde_json::json!(1.5);
    assert!(matches!(
      json_to_laufey_value(&n_int),
      laufey::Value::Int(42)
    ));
    assert!(matches!(
      json_to_laufey_value(&n_float),
      laufey::Value::Double(d) if d == 1.5
    ));
  }

  // --- apply_pending_update ---
  //
  // State machine: presence/absence of `<dylib>.update`, `.backup`, and
  // `.update-ok` sentinel decides what apply_pending_update does next.
  // The four combinations have non-obvious effects (commit / rollback /
  // cleanup / no-op) and the consequences of getting them wrong are
  // serious (booting a half-applied dylib, or boot-looping on rollback).
  // Tests below exercise each branch using a tempdir as the live dylib
  // location.

  // apply_pending_update is unix-only (see its #[cfg(unix)] gate), so its
  // tests and helpers live in a unix-only submodule.
  #[cfg(unix)]
  mod pending_update_tests {
    use super::super::apply_pending_update;

    fn touch(path: &std::path::Path, content: &str) {
      std::fs::write(path, content).unwrap();
    }

    fn read(path: &std::path::Path) -> String {
      std::fs::read_to_string(path).unwrap()
    }

    fn paths(
      tmp: &std::path::Path,
    ) -> (
      std::path::PathBuf,
      std::path::PathBuf,
      std::path::PathBuf,
      std::path::PathBuf,
    ) {
      let dylib = tmp.join("app.dylib");
      let update = tmp.join("app.dylib.update");
      let backup = tmp.join("app.dylib.backup");
      let sentinel = tmp.join("app.dylib.update-ok");
      (dylib, update, backup, sentinel)
    }

    #[test]
    fn pending_update_no_files_is_noop() {
      let tmp = tempfile::tempdir().unwrap();
      let (dylib, _, _, _) = paths(tmp.path());
      touch(&dylib, "live");
      let rolled_back = apply_pending_update(&dylib);
      assert!(!rolled_back, "no .update / .backup → no rollback");
      assert_eq!(read(&dylib), "live", "dylib must be untouched");
    }

    #[test]
    fn pending_update_swaps_in_new_dylib_when_update_exists() {
      let tmp = tempfile::tempdir().unwrap();
      let (dylib, update, backup, sentinel) = paths(tmp.path());
      touch(&dylib, "old");
      touch(&update, "new");
      let rolled_back = apply_pending_update(&dylib);

      assert!(!rolled_back, "applying a fresh update is not a rollback");
      assert_eq!(
        read(&dylib),
        "new",
        "dylib must now hold the update contents"
      );
      assert!(backup.exists(), "live dylib must be preserved as .backup");
      assert_eq!(read(&backup), "old", "backup must be the *previous* dylib");
      // Stale sentinel from a prior update is cleared so the next launch
      // can detect THIS update's success/failure.
      assert!(!sentinel.exists());
      // The staged update file is consumed.
      assert!(!update.exists());
    }

    #[test]
    fn pending_update_clears_stale_sentinel_before_swap() {
      let tmp = tempfile::tempdir().unwrap();
      let (dylib, update, backup, sentinel) = paths(tmp.path());
      touch(&dylib, "old");
      touch(&update, "new");
      touch(&sentinel, "ok"); // sentinel left over from a previous launch
      touch(&backup, "very-old"); // and a backup, also stale
      apply_pending_update(&dylib);
      // After applying a new update: backup is the just-replaced 'old',
      // sentinel was cleared. The previous backup ("very-old") must not
      // persist, or rollback would resurrect a two-version-old dylib.
      assert_eq!(read(&backup), "old");
      assert!(!sentinel.exists());
    }

    #[test]
    fn pending_update_rolls_back_when_sentinel_missing() {
      // .backup present + no sentinel = previous update booted but crashed
      // before writing the sentinel. Rollback to backup.
      let tmp = tempfile::tempdir().unwrap();
      let (dylib, _, backup, _) = paths(tmp.path());
      touch(&dylib, "new-but-broken");
      touch(&backup, "old-but-known-good");
      let rolled_back = apply_pending_update(&dylib);
      assert!(rolled_back, "missing sentinel must trigger rollback");
      assert_eq!(
        read(&dylib),
        "old-but-known-good",
        "rollback must restore the backup contents over the live dylib"
      );
    }

    #[test]
    fn pending_update_cleans_up_after_successful_boot() {
      // .backup + .update-ok sentinel = previous update was applied AND
      // booted at least once. Clean up; we don't need to keep the backup
      // around forever.
      let tmp = tempfile::tempdir().unwrap();
      let (dylib, _, backup, sentinel) = paths(tmp.path());
      touch(&dylib, "new");
      touch(&backup, "old");
      touch(&sentinel, "ok");
      let rolled_back = apply_pending_update(&dylib);
      assert!(!rolled_back);
      assert!(!backup.exists(), "successful-boot path must delete .backup");
      assert!(
        !sentinel.exists(),
        "successful-boot path must delete sentinel"
      );
      assert_eq!(read(&dylib), "new", "dylib untouched on cleanup path");
    }

    #[test]
    fn pending_update_with_only_sentinel_is_noop() {
      // Sentinel without backup or update — orphaned. Don't roll back
      // (there's nothing to roll back to) and don't touch the dylib.
      let tmp = tempfile::tempdir().unwrap();
      let (dylib, _, _, sentinel) = paths(tmp.path());
      touch(&dylib, "live");
      touch(&sentinel, "ok");
      let rolled_back = apply_pending_update(&dylib);
      assert!(!rolled_back);
      assert_eq!(read(&dylib), "live");
    }
  }

  #[test]
  fn json_to_laufey_value_handles_nested_arrays_and_objects() {
    let j = serde_json::json!({
      "list": [1, 2, 3],
      "nested": {"key": "value"},
      "null": null,
    });
    let v = json_to_laufey_value(&j);
    let laufey::Value::Dict(d) = v else {
      panic!("expected Dict")
    };
    assert!(matches!(d.get("null"), Some(laufey::Value::Null)));
    match d.get("list") {
      Some(laufey::Value::List(items)) => assert_eq!(items.len(), 3),
      _ => panic!("list must convert to List"),
    }
    match d.get("nested") {
      Some(laufey::Value::Dict(inner)) => match inner.get("key") {
        Some(laufey::Value::String(s)) => assert_eq!(s, "value"),
        _ => panic!("nested.key must be String"),
      },
      _ => panic!("nested must convert to Dict"),
    }
  }
}
