//! Windows desktop PTT backend (SDD-083 + SDD-084).
//!
//! Three-rung ladder per SAD-072: Raw Input first, low-level
//! keyboard / mouse hook fallback, Focused PTT terminal fallback.
//! The terminal fallback is handled by the cross-platform
//! `select()` factory in the parent module returning `None` from
//! `try_select`.
//!
//! Both Raw Input and the low-level hook need a dedicated OS
//! thread because their callbacks fire on the thread that owns the
//! message-only window / hook handle. The thread runs a standard
//! Win32 GetMessageW/TranslateMessage/DispatchMessageW pump until
//! `stop()` posts `WM_QUIT`.
//!
//! Privacy (DEC-027 / SDD-090): translation of the bound key is
//! confined to the OS-callback hot path. The platform-neutral
//! input class (`"keyboard"` / `"mouse-side-button"`) is the only
//! identifier that ever crosses into a `tracing` record. The
//! diagnostics sanitizer would drop a record carrying `vk` /
//! `virtual_key` / `scan_code` even if we tried to log it, but
//! this file never tries.

use std::sync::atomic::{AtomicBool, AtomicIsize, Ordering};
use std::sync::Arc;
use std::thread;

use tracing::{info, warn};
use windows::core::{w, PCWSTR};
use windows::Win32::Foundation::{HMODULE, HWND, LPARAM, LRESULT, WPARAM};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::UI::Input::{
    GetRawInputData, RegisterRawInputDevices, HRAWINPUT, RAWINPUT, RAWINPUTDEVICE, RAWINPUTHEADER,
    RIDEV_INPUTSINK, RIDEV_REMOVE, RID_INPUT, RIM_TYPEKEYBOARD, RIM_TYPEMOUSE,
};
use windows::Win32::UI::WindowsAndMessaging::{
    CallNextHookEx, CreateWindowExW, DefWindowProcW, DispatchMessageW, GetMessageW,
    PostThreadMessageW, RegisterClassExW, SetWindowsHookExW, TranslateMessage, UnhookWindowsHookEx,
    HC_ACTION, HHOOK, HOOKPROC, KBDLLHOOKSTRUCT, MSG, MSLLHOOKSTRUCT, WH_KEYBOARD_LL, WH_MOUSE_LL,
    WINDOW_EX_STYLE, WINDOW_STYLE, WM_INPUT, WM_KEYDOWN, WM_KEYUP, WM_QUIT, WM_SYSKEYDOWN,
    WM_SYSKEYUP, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSEXW, XBUTTON1, XBUTTON2,
};

use super::{AudioTransmitGate, DesktopPttBackend, PttBackendError, PttBinding};
use crate::ptt::{PttBackendDescriptor, PttCapabilityLevel};

mod keymap {
    pub use super::super::windows_keymap::*;
}

/// HWND_MESSAGE constant exposed as a raw isize.
/// `windows-rs` defines it via a different module path across
/// versions; use the documented integer directly.
const HWND_MESSAGE_PTR: isize = -3;

/// Try to construct the highest-capability Windows backend. The
/// ladder evaluation is fixed for the lifetime of the engine; the
/// engine's missed-key-up watchdog + Focused fallback compensate
/// at runtime if a higher rung fails after `start`.
pub fn try_select() -> Option<Box<dyn DesktopPttBackend>> {
    if let Some(b) = WindowsRawInputBackend::try_new() {
        return Some(Box::new(b));
    }
    if let Some(b) = WindowsHookBackend::try_new() {
        return Some(Box::new(b));
    }
    None
}

/// Atomic mirror of the active binding. Shared between the public
/// rebind path and the message-loop / hook thread that needs to
/// read the binding on every key event without taking a lock.
///
/// The fields are individually atomic so the callback path can
/// snapshot them in a few cheap loads. `class` encodes the
/// `PttInputClass` ordinal: 0 = None, 1 = Keyboard, 2 = MouseSideButton.
/// `vk` holds the resolved Windows VK_* code when class==Keyboard;
/// `mouse_btn` holds 4 (XBUTTON1) or 5 (XBUTTON2) when class==MouseSideButton.
#[derive(Default)]
pub(crate) struct AtomicBinding {
    class: AtomicIsize,
    vk: AtomicIsize,
    mouse_btn: AtomicIsize,
}

impl AtomicBinding {
    pub(crate) fn new() -> Self {
        Self::default()
    }

    pub(crate) fn store(&self, class: u8, vk: u16, mouse_btn: u8) {
        // Order: write fields first, class last, so a concurrent
        // reader that sees class != 0 also sees a coherent
        // vk / mouse_btn pair. Acquire/release rather than seqcst
        // because we have no other synchronisation around this.
        self.vk.store(vk as isize, Ordering::Release);
        self.mouse_btn.store(mouse_btn as isize, Ordering::Release);
        self.class.store(class as isize, Ordering::Release);
    }

    pub(crate) fn class(&self) -> u8 {
        self.class.load(Ordering::Acquire) as u8
    }

    pub(crate) fn vk(&self) -> u16 {
        self.vk.load(Ordering::Acquire) as u16
    }

    pub(crate) fn mouse_btn(&self) -> u8 {
        self.mouse_btn.load(Ordering::Acquire) as u8
    }

    pub(crate) fn clear(&self) {
        self.class.store(0, Ordering::Release);
        self.vk.store(0, Ordering::Release);
        self.mouse_btn.store(0, Ordering::Release);
    }
}

/// Convert a `PttBinding` into the (class, vk, mouse_btn) triple.
/// Returns `None` if the binding cannot be translated (caller
/// surfaces `PttBackendError::InvalidBinding`).
pub(crate) fn resolve_binding(binding: &PttBinding) -> Option<(u8, u16, u8)> {
    match binding.input_class {
        super::PttInputClass::None => Some((0, 0, 0)),
        super::PttInputClass::Keyboard => {
            let vk = keymap::key_label_to_vk(&binding.platform_key)?;
            Some((1, vk, 0))
        }
        super::PttInputClass::MouseSideButton => {
            let idx = keymap::mouse_side_button_index(&binding.platform_key)?;
            Some((2, 0, idx))
        }
    }
}

// ---------- Raw-Input backend ----------

/// Per-thread context for the Raw Input message-loop. Stored on
/// the heap; the message window's `GWLP_USERDATA` is not used —
/// the `WndProc` looks up its `RawInputContext` via a thread-local
/// because Windows passes no user pointer on `WM_INPUT`.
///
/// Exposed to the test module so unit tests can call
/// [`dispatch_raw_input`] directly without spinning up a Win32
/// message pump (L0 refactor).
pub(crate) struct RawInputContext {
    pub(crate) gate: AudioTransmitGate,
    pub(crate) binding: Arc<AtomicBinding>,
}

thread_local! {
    /// Per-thread Raw-Input context. Set on entry to the worker
    /// thread; read by `raw_input_wnd_proc` on each `WM_INPUT`.
    static RAWINPUT_CTX: std::cell::RefCell<Option<RawInputContext>> =
        const { std::cell::RefCell::new(None) };
}

/// Raw-Input backend. Preferred Windows rung. Receives keyboard
/// and mouse events globally (RIDEV_INPUTSINK) so the bound key
/// fires even when Chanora is unfocused.
pub struct WindowsRawInputBackend {
    binding: Arc<AtomicBinding>,
    gate: Option<AudioTransmitGate>,
    /// Thread id of the message-loop worker. Used to post
    /// `WM_QUIT` from `stop()` without needing a window handle.
    worker_tid: Arc<AtomicIsize>,
    worker: Option<thread::JoinHandle<()>>,
    /// True once the worker successfully registered the Raw Input
    /// devices. Reported via `descriptor()` so the UI only
    /// advertises Global capability when it's real.
    armed: Arc<AtomicBool>,
    /// True if mouse side-button was the most-recently-bound class.
    /// Cached so descriptor() can report L3 (mouse-button) vs L2
    /// (keyboard) without re-reading the AtomicBinding.
    is_mouse: AtomicBool,
}

impl WindowsRawInputBackend {
    fn try_new() -> Option<Self> {
        info!(
            target: "chanora_audio",
            "windows ptt: selecting Raw Input backend (RIDEV_INPUTSINK)"
        );
        Some(Self {
            binding: Arc::new(AtomicBinding::new()),
            gate: None,
            worker_tid: Arc::new(AtomicIsize::new(0)),
            worker: None,
            armed: Arc::new(AtomicBool::new(false)),
            is_mouse: AtomicBool::new(false),
        })
    }
}

impl DesktopPttBackend for WindowsRawInputBackend {
    fn descriptor(&self) -> PttBackendDescriptor {
        // If we never armed, we are effectively running as a
        // Focused-equivalent placeholder; report L0 honestly so
        // the UI badge can surface the info-icon explanation
        // sheet (SDD-091). The cross-platform select() factory
        // still hands callers a WindowsRawInputBackend instance
        // because `try_select` does not probe — runtime arm is
        // where we learn if the platform accepts the registration.
        let level = if !self.armed.load(Ordering::Acquire) {
            PttCapabilityLevel::L0Focused
        } else if self.is_mouse.load(Ordering::Acquire) {
            PttCapabilityLevel::L3GlobalWithMouseButtons
        } else {
            PttCapabilityLevel::L2GlobalHoldToTalk
        };
        let class = self.binding.class();
        PttBackendDescriptor {
            level,
            backend_id: "raw-input",
            bound_input_class: match class {
                0 => None,
                2 => Some("mouse-side-button"),
                _ => Some("keyboard"),
            },
        }
    }

    fn start(
        &mut self,
        gate: AudioTransmitGate,
        binding: PttBinding,
    ) -> Result<(), PttBackendError> {
        // Resolve binding before spawning so an invalid binding
        // surfaces synchronously to the caller. PttBinding::none()
        // resolves to class=0 (no firing); a malformed
        // platform_key surfaces as InvalidBinding.
        let resolved = resolve_binding(&binding).ok_or_else(|| {
            PttBackendError::InvalidBinding(format!(
                "windows raw-input cannot resolve binding (class={:?})",
                binding.input_class
            ))
        })?;
        self.binding.store(resolved.0, resolved.1, resolved.2);
        self.is_mouse.store(resolved.0 == 2, Ordering::Release);
        self.gate = Some(gate.clone());

        let binding_for_worker = self.binding.clone();
        let worker_tid = self.worker_tid.clone();
        let armed = self.armed.clone();
        let gate_for_worker = gate.clone();

        let (init_tx, init_rx) = std::sync::mpsc::sync_channel::<bool>(1);
        let handle = thread::Builder::new()
            .name("chanora-rawinput".into())
            .spawn(move || {
                // Stash our thread id so stop() can post WM_QUIT.
                let tid = unsafe { windows::Win32::System::Threading::GetCurrentThreadId() };
                worker_tid.store(tid as isize, Ordering::Release);

                // Wire the per-thread context the WndProc reads
                // for gate access.
                RAWINPUT_CTX.with(|cell| {
                    *cell.borrow_mut() = Some(RawInputContext {
                        gate: gate_for_worker,
                        binding: binding_for_worker,
                    });
                });

                // Run the loop. Pass `init_tx` so the loop can
                // signal readiness AFTER RegisterRawInputDevices
                // succeeds but BEFORE GetMessageW starts blocking
                // — otherwise the main thread's readiness probe
                // times out (the signal would only fire when
                // WM_QUIT was eventually delivered).
                //
                // Also pass `armed` so the loop can flip the flag
                // to true inside the same critical window — the
                // outer `armed.store(ok, ...)` only runs after the
                // message pump returns (i.e. on WM_QUIT), which
                // never happens during normal arming. Without the
                // in-loop set, `descriptor()` would always report
                // L0Focused even though the backend is correctly
                // armed and receiving WM_INPUT events.
                let ok = unsafe { run_raw_input_loop(init_tx, armed.clone()) };
                // Belt-and-braces: if the loop's normal exit
                // (WM_QUIT) happens before stop() runs the outer
                // set, clear the flag here too.
                if !ok {
                    armed.store(false, Ordering::Release);
                }

                // If init failed, exit immediately. If it
                // succeeded, run_raw_input_loop already ran the
                // message pump and returned only when WM_QUIT
                // was received.

                // Clear per-thread context so any later thread
                // reusing the slot doesn't read stale state.
                RAWINPUT_CTX.with(|cell| {
                    *cell.borrow_mut() = None;
                });
            })
            .map_err(|e| PttBackendError::Init(format!("rawinput thread: {e}")))?;

        // Wait briefly for the worker to report whether init
        // succeeded. The arm bool is the real source of truth;
        // this just lets us log accurately.
        match init_rx.recv_timeout(std::time::Duration::from_secs(2)) {
            Ok(true) => {
                // Success — the in-loop log line already wrote
                // "Raw Input devices registered" with full context.
                // Don't double-log; just continue.
            }
            Ok(false) => {
                warn!(
                    target: "chanora_audio",
                    "windows ptt: Raw Input registration failed; descriptor will report L0"
                );
            }
            Err(_) => {
                warn!(
                    target: "chanora_audio",
                    "windows ptt: Raw Input init did not report within 2s; assuming failure"
                );
            }
        }

        self.worker = Some(handle);
        Ok(())
    }

    fn stop(&mut self) {
        let tid = self.worker_tid.load(Ordering::Acquire);
        if tid != 0 {
            unsafe {
                // Post WM_QUIT to the worker thread so its
                // GetMessageW loop returns. Ignore the result;
                // a stale tid just means the thread already
                // exited.
                let _ = PostThreadMessageW(tid as u32, WM_QUIT, WPARAM(0), LPARAM(0));
            }
        }
        if let Some(h) = self.worker.take() {
            let _ = h.join();
        }
        if let Some(g) = self.gate.take() {
            g.set(false);
        }
        self.armed.store(false, Ordering::Release);
        self.binding.clear();
    }

    fn rebind(&mut self, binding: PttBinding) -> Result<(), PttBackendError> {
        let resolved = resolve_binding(&binding).ok_or_else(|| {
            PttBackendError::InvalidBinding(format!(
                "windows raw-input cannot resolve binding (class={:?})",
                binding.input_class
            ))
        })?;
        self.binding.store(resolved.0, resolved.1, resolved.2);
        self.is_mouse.store(resolved.0 == 2, Ordering::Release);
        // Drop the current gate state on rebind: an in-flight
        // press on the prior key should not bleed into the new
        // binding's key-up logic.
        if let Some(g) = &self.gate {
            g.set(false);
        }
        Ok(())
    }
}

impl Drop for WindowsRawInputBackend {
    fn drop(&mut self) {
        self.stop();
    }
}

/// Run the Raw Input registration + message pump. Returns true if
/// the registration succeeded and the loop terminated on WM_QUIT;
/// false on any setup failure.
///
/// # Safety
/// Calls into Win32 directly. Must be invoked on the thread that
/// owns the message window (created here).
unsafe fn run_raw_input_loop(
    init_tx: std::sync::mpsc::SyncSender<bool>,
    armed: Arc<AtomicBool>,
) -> bool {
    // Helper that signals the readiness state to the main thread.
    // We send exactly once at the first decisive moment (either an
    // early-fail return or right after a successful
    // RegisterRawInputDevices). Subsequent sends are no-ops.
    let mut signal = Some(init_tx);
    macro_rules! report {
        ($v:expr) => {
            if let Some(tx) = signal.take() {
                let _ = tx.send($v);
            }
        };
    }

    // Create a hidden message-only window. We need it as the
    // hwndTarget on the RAWINPUTDEVICE so RIDEV_INPUTSINK delivery
    // works even when our process has no visible window focus.
    let h_instance = match GetModuleHandleW(None) {
        Ok(h) => h,
        Err(e) => {
            warn!(
                target: "chanora_audio",
                error = %e,
                "windows ptt: GetModuleHandleW failed"
            );
            report!(false);
            return false;
        }
    };
    let class_name = w!("chanora_rawinput_msg");
    let wc = WNDCLASSEXW {
        cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
        lpfnWndProc: Some(raw_input_wnd_proc),
        hInstance: h_instance.into(),
        lpszClassName: class_name,
        ..Default::default()
    };
    // Registration may fail with ERROR_CLASS_ALREADY_EXISTS if
    // start() ran before in this process; that's fine, we proceed
    // to CreateWindowExW which will succeed against the existing
    // class.
    let _atom = RegisterClassExW(&wc);

    let hwnd = unsafe {
        CreateWindowExW(
            WINDOW_EX_STYLE(0),
            class_name,
            PCWSTR::null(),
            WINDOW_STYLE(0),
            0,
            0,
            0,
            0,
            HWND(HWND_MESSAGE_PTR),
            None,
            h_instance,
            None,
        )
    };
    if hwnd.0 == 0 {
        warn!(
            target: "chanora_audio",
            "windows ptt: CreateWindowExW(HWND_MESSAGE) returned null"
        );
        report!(false);
        return false;
    }

    // Register both keyboard and mouse, both with INPUTSINK so we
    // receive events even when unfocused.
    // Generic Desktop usage page = 0x01; keyboard usage = 0x06;
    // mouse usage = 0x02 (per HID spec).
    let devices = [
        RAWINPUTDEVICE {
            usUsagePage: 0x01,
            usUsage: 0x06,
            dwFlags: RIDEV_INPUTSINK,
            hwndTarget: hwnd,
        },
        RAWINPUTDEVICE {
            usUsagePage: 0x01,
            usUsage: 0x02,
            dwFlags: RIDEV_INPUTSINK,
            hwndTarget: hwnd,
        },
    ];
    let reg_ok = RegisterRawInputDevices(&devices, std::mem::size_of::<RAWINPUTDEVICE>() as u32);
    if reg_ok.is_err() {
        warn!(
            target: "chanora_audio",
            "windows ptt: RegisterRawInputDevices failed"
        );
        report!(false);
        return false;
    }

    info!(
        target: "chanora_audio",
        "windows ptt: Raw Input devices registered (keyboard + mouse, INPUTSINK)"
    );
    // Flip armed = true here, inside the loop, BEFORE blocking on
    // GetMessageW. The outer worker closure's armed.store(ok, ...)
    // only runs on WM_QUIT and so never fires during normal use.
    // Without this in-loop store, descriptor() would always report
    // L0Focused even though the backend is correctly receiving
    // WM_INPUT events — the bug that landed the capability badge
    // stuck at L0Focused on Korean Win 11 in TC-2.3.
    armed.store(true, Ordering::Release);
    // Signal readiness NOW (before we block on GetMessageW). The
    // main thread's init probe is waiting for this; the loop runs
    // until WM_QUIT and the return value at end-of-life is no
    // longer used as a readiness signal.
    report!(true);

    // Message pump. GetMessageW returns 0 on WM_QUIT, -1 on error.
    let mut msg = MSG::default();
    loop {
        let r = GetMessageW(&mut msg, None, 0, 0).0;
        if r == 0 {
            // WM_QUIT.
            break;
        }
        if r == -1 {
            warn!(target: "chanora_audio", "windows ptt: GetMessageW returned -1");
            break;
        }
        let _ = TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }

    // Unregister cleanly so a fresh start() in this process
    // doesn't trip over a stale registration.
    let undo = [
        RAWINPUTDEVICE {
            usUsagePage: 0x01,
            usUsage: 0x06,
            dwFlags: RIDEV_REMOVE,
            hwndTarget: HWND(0),
        },
        RAWINPUTDEVICE {
            usUsagePage: 0x01,
            usUsage: 0x02,
            dwFlags: RIDEV_REMOVE,
            hwndTarget: HWND(0),
        },
    ];
    let _ = RegisterRawInputDevices(&undo, std::mem::size_of::<RAWINPUTDEVICE>() as u32);

    true
}

unsafe extern "system" fn raw_input_wnd_proc(
    hwnd: HWND,
    msg: u32,
    wparam: WPARAM,
    lparam: LPARAM,
) -> LRESULT {
    if msg == WM_INPUT {
        handle_wm_input(lparam);
    }
    DefWindowProcW(hwnd, msg, wparam, lparam)
}

unsafe fn handle_wm_input(lparam: LPARAM) {
    let h_raw = HRAWINPUT(lparam.0);
    let mut size: u32 = 0;
    let header_sz = std::mem::size_of::<RAWINPUTHEADER>() as u32;
    // First call: query buffer size.
    GetRawInputData(h_raw, RID_INPUT, None, &mut size, header_sz);
    if size == 0 {
        return;
    }
    let mut buf = vec![0u8; size as usize];
    let got = GetRawInputData(
        h_raw,
        RID_INPUT,
        Some(buf.as_mut_ptr() as *mut _),
        &mut size,
        header_sz,
    );
    if got != size {
        return;
    }
    let raw: &RAWINPUT = &*(buf.as_ptr() as *const RAWINPUT);

    RAWINPUT_CTX.with(|cell| {
        let borrow = cell.borrow();
        if let Some(ctx) = borrow.as_ref() {
            dispatch_raw_input(ctx, raw);
        }
    });
}

/// Pure-logic dispatcher for a decoded `RAWINPUT` message (L0
/// refactor; SDD-083 hot path).
///
/// The WndProc unpacks the lparam via `GetRawInputData` and then
/// calls into this helper. Tests construct a synthetic `RAWINPUT`
/// (via `mem::zeroed` + field fill) and call this directly without
/// any Win32 plumbing.
///
/// Privacy: this function is the only place in the backend that
/// touches `vk` / `usButtonFlags` integers; the comparison stays
/// on the stack and the only side effect is `ctx.gate.set(bool)`.
///
/// # Safety
/// Reads from the `raw.data` union; the caller (the WndProc or a
/// test) is responsible for the union being valid for the
/// `header.dwType` it set.
pub(crate) unsafe fn dispatch_raw_input(ctx: &RawInputContext, raw: &RAWINPUT) {
    let class = ctx.binding.class();
    if class == 0 {
        return;
    }
    match raw.header.dwType {
        t if t == RIM_TYPEKEYBOARD.0 && class == 1 => {
            let kb = &raw.data.keyboard;
            let vk = ctx.binding.vk();
            if kb.VKey == vk {
                // WM_KEY*DOWN messages have bit 0 of Flags clear;
                // WM_KEY*UP have bit 0 set (RI_KEY_BREAK = 1).
                if (kb.Flags & 0x01) == 0 {
                    ctx.gate.set(true);
                } else {
                    ctx.gate.set(false);
                }
            }
        }
        t if t == RIM_TYPEMOUSE.0 && class == 2 => {
            let m = &raw.data.mouse;
            let want = ctx.binding.mouse_btn();
            let flags = m.Anonymous.Anonymous.usButtonFlags as u32;
            // RI_MOUSE_* constants per the Windows SDK. Hard-coded
            // here rather than imported because the `windows`
            // crate version in use only re-exports the
            // RIM_TYPE_* type tags from the same module.
            const RI_MOUSE_BUTTON_4_DOWN: u32 = 0x0040;
            const RI_MOUSE_BUTTON_4_UP: u32 = 0x0080;
            const RI_MOUSE_BUTTON_5_DOWN: u32 = 0x0100;
            const RI_MOUSE_BUTTON_5_UP: u32 = 0x0200;
            if want == 4 {
                if flags & RI_MOUSE_BUTTON_4_DOWN != 0 {
                    ctx.gate.set(true);
                } else if flags & RI_MOUSE_BUTTON_4_UP != 0 {
                    ctx.gate.set(false);
                }
            } else if want == 5 {
                if flags & RI_MOUSE_BUTTON_5_DOWN != 0 {
                    ctx.gate.set(true);
                } else if flags & RI_MOUSE_BUTTON_5_UP != 0 {
                    ctx.gate.set(false);
                }
            }
        }
        _ => {}
    }
}

// ---------- Low-level hook backend ----------

thread_local! {
    /// Per-thread Hook context. Set on entry to the hook worker
    /// thread; read by `kbd_hook_proc` and `mouse_hook_proc`.
    static HOOK_CTX: std::cell::RefCell<Option<HookContext>> =
        const { std::cell::RefCell::new(None) };
}

pub(crate) struct HookContext {
    pub(crate) gate: AudioTransmitGate,
    pub(crate) binding: Arc<AtomicBinding>,
}

/// Low-level hook backend. Used only when Raw Input fails.
/// Installs `WH_KEYBOARD_LL` + `WH_MOUSE_LL` global hooks; hook
/// procs translate the event and drive the transmit gate.
pub struct WindowsHookBackend {
    binding: Arc<AtomicBinding>,
    gate: Option<AudioTransmitGate>,
    worker_tid: Arc<AtomicIsize>,
    worker: Option<thread::JoinHandle<()>>,
    armed: Arc<AtomicBool>,
    is_mouse: AtomicBool,
}

impl WindowsHookBackend {
    fn try_new() -> Option<Self> {
        warn!(
            target: "chanora_audio",
            "windows ptt: falling back to low-level keyboard hook (WH_KEYBOARD_LL)"
        );
        Some(Self {
            binding: Arc::new(AtomicBinding::new()),
            gate: None,
            worker_tid: Arc::new(AtomicIsize::new(0)),
            worker: None,
            armed: Arc::new(AtomicBool::new(false)),
            is_mouse: AtomicBool::new(false),
        })
    }
}

impl DesktopPttBackend for WindowsHookBackend {
    fn descriptor(&self) -> PttBackendDescriptor {
        let level = if !self.armed.load(Ordering::Acquire) {
            PttCapabilityLevel::L0Focused
        } else if self.is_mouse.load(Ordering::Acquire) {
            PttCapabilityLevel::L3GlobalWithMouseButtons
        } else {
            PttCapabilityLevel::L2GlobalHoldToTalk
        };
        let class = self.binding.class();
        PttBackendDescriptor {
            level,
            backend_id: "low-level-hook",
            bound_input_class: match class {
                0 => None,
                2 => Some("mouse-side-button"),
                _ => Some("keyboard"),
            },
        }
    }

    fn start(
        &mut self,
        gate: AudioTransmitGate,
        binding: PttBinding,
    ) -> Result<(), PttBackendError> {
        let resolved = resolve_binding(&binding).ok_or_else(|| {
            PttBackendError::InvalidBinding(format!(
                "windows hook cannot resolve binding (class={:?})",
                binding.input_class
            ))
        })?;
        self.binding.store(resolved.0, resolved.1, resolved.2);
        self.is_mouse.store(resolved.0 == 2, Ordering::Release);
        self.gate = Some(gate.clone());

        let binding_for_worker = self.binding.clone();
        let worker_tid = self.worker_tid.clone();
        let armed = self.armed.clone();
        let gate_for_worker = gate.clone();

        let (init_tx, init_rx) = std::sync::mpsc::sync_channel::<bool>(1);
        let handle = thread::Builder::new()
            .name("chanora-llhook".into())
            .spawn(move || {
                let tid = unsafe { windows::Win32::System::Threading::GetCurrentThreadId() };
                worker_tid.store(tid as isize, Ordering::Release);

                HOOK_CTX.with(|cell| {
                    *cell.borrow_mut() = Some(HookContext {
                        gate: gate_for_worker,
                        binding: binding_for_worker,
                    });
                });

                let ok = unsafe { run_hook_loop(init_tx, armed.clone()) };
                if !ok {
                    armed.store(false, Ordering::Release);
                }

                HOOK_CTX.with(|cell| {
                    *cell.borrow_mut() = None;
                });
            })
            .map_err(|e| PttBackendError::Init(format!("llhook thread: {e}")))?;

        match init_rx.recv_timeout(std::time::Duration::from_secs(2)) {
            Ok(true) => {
                // Success — the in-loop log line already wrote
                // "low-level hooks installed"; no need to repeat.
            }
            Ok(false) => warn!(
                target: "chanora_audio",
                "windows ptt: SetWindowsHookEx failed; descriptor will report L0"
            ),
            Err(_) => warn!(
                target: "chanora_audio",
                "windows ptt: hook init did not report within 2s; assuming failure"
            ),
        }

        self.worker = Some(handle);
        Ok(())
    }

    fn stop(&mut self) {
        let tid = self.worker_tid.load(Ordering::Acquire);
        if tid != 0 {
            unsafe {
                let _ = PostThreadMessageW(tid as u32, WM_QUIT, WPARAM(0), LPARAM(0));
            }
        }
        if let Some(h) = self.worker.take() {
            let _ = h.join();
        }
        if let Some(g) = self.gate.take() {
            g.set(false);
        }
        self.armed.store(false, Ordering::Release);
        self.binding.clear();
    }

    fn rebind(&mut self, binding: PttBinding) -> Result<(), PttBackendError> {
        let resolved = resolve_binding(&binding).ok_or_else(|| {
            PttBackendError::InvalidBinding(format!(
                "windows hook cannot resolve binding (class={:?})",
                binding.input_class
            ))
        })?;
        self.binding.store(resolved.0, resolved.1, resolved.2);
        self.is_mouse.store(resolved.0 == 2, Ordering::Release);
        if let Some(g) = &self.gate {
            g.set(false);
        }
        Ok(())
    }
}

impl Drop for WindowsHookBackend {
    fn drop(&mut self) {
        self.stop();
    }
}

/// Install WH_KEYBOARD_LL + WH_MOUSE_LL and run a message loop
/// until WM_QUIT. Returns true if both hooks installed and the
/// loop ran; false on any installation failure.
///
/// # Safety
/// Calls Win32 directly; must run on the thread that owns the
/// hook handles.
unsafe fn run_hook_loop(
    init_tx: std::sync::mpsc::SyncSender<bool>,
    armed: Arc<AtomicBool>,
) -> bool {
    let mut signal = Some(init_tx);
    macro_rules! report {
        ($v:expr) => {
            if let Some(tx) = signal.take() {
                let _ = tx.send($v);
            }
        };
    }

    let h_instance: HMODULE = match GetModuleHandleW(None) {
        Ok(h) => h,
        Err(e) => {
            warn!(
                target: "chanora_audio",
                error = %e,
                "windows ptt: GetModuleHandleW failed (hook)"
            );
            report!(false);
            return false;
        }
    };

    let kbd_proc: HOOKPROC = Some(kbd_hook_proc);
    let mouse_proc: HOOKPROC = Some(mouse_hook_proc);

    let kbd_hook = match SetWindowsHookExW(WH_KEYBOARD_LL, kbd_proc, h_instance, 0) {
        Ok(h) => h,
        Err(e) => {
            warn!(
                target: "chanora_audio",
                error = %e,
                "windows ptt: SetWindowsHookExW(WH_KEYBOARD_LL) failed"
            );
            report!(false);
            return false;
        }
    };
    let mouse_hook = match SetWindowsHookExW(WH_MOUSE_LL, mouse_proc, h_instance, 0) {
        Ok(h) => h,
        Err(e) => {
            warn!(
                target: "chanora_audio",
                error = %e,
                "windows ptt: SetWindowsHookExW(WH_MOUSE_LL) failed"
            );
            let _ = UnhookWindowsHookEx(kbd_hook);
            report!(false);
            return false;
        }
    };

    info!(
        target: "chanora_audio",
        "windows ptt: low-level hooks installed (WH_KEYBOARD_LL + WH_MOUSE_LL)"
    );
    // Flip armed = true here, before blocking on GetMessageW.
    // Same rationale as run_raw_input_loop.
    armed.store(true, Ordering::Release);
    // Signal readiness now, before blocking on GetMessageW. The
    // return value at end-of-loop is no longer used by the init
    // probe.
    report!(true);

    let mut msg = MSG::default();
    loop {
        let r = GetMessageW(&mut msg, None, 0, 0).0;
        if r == 0 {
            break;
        }
        if r == -1 {
            warn!(target: "chanora_audio", "windows ptt: GetMessageW returned -1 (hook)");
            break;
        }
        let _ = TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }

    let _ = UnhookWindowsHookEx(kbd_hook);
    let _ = UnhookWindowsHookEx(mouse_hook);
    true
}

unsafe extern "system" fn kbd_hook_proc(code: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
    if code == HC_ACTION as i32 {
        let kb = &*(lparam.0 as *const KBDLLHOOKSTRUCT);
        HOOK_CTX.with(|cell| {
            let borrow = cell.borrow();
            if let Some(ctx) = borrow.as_ref() {
                dispatch_hook_keyboard(ctx, wparam, kb);
            }
        });
    }
    CallNextHookEx(HHOOK(0), code, wparam, lparam)
}

/// Pure-logic dispatcher for a low-level keyboard hook event (L0
/// refactor; SDD-084 hot path). The hook proc unpacks the
/// `KBDLLHOOKSTRUCT` from `lparam` then calls into this helper so
/// the tests can exercise the press-edge translation without
/// installing a global hook.
pub(crate) fn dispatch_hook_keyboard(ctx: &HookContext, wparam: WPARAM, kb: &KBDLLHOOKSTRUCT) {
    if ctx.binding.class() != 1 {
        return;
    }
    if kb.vkCode as u16 != ctx.binding.vk() {
        return;
    }
    let w = wparam.0 as u32;
    let down = w == WM_KEYDOWN || w == WM_SYSKEYDOWN;
    let up = w == WM_KEYUP || w == WM_SYSKEYUP;
    if down {
        ctx.gate.set(true);
    } else if up {
        ctx.gate.set(false);
    }
}

unsafe extern "system" fn mouse_hook_proc(code: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
    if code == HC_ACTION as i32 {
        let m = &*(lparam.0 as *const MSLLHOOKSTRUCT);
        HOOK_CTX.with(|cell| {
            let borrow = cell.borrow();
            if let Some(ctx) = borrow.as_ref() {
                dispatch_hook_mouse(ctx, wparam, m);
            }
        });
    }
    CallNextHookEx(HHOOK(0), code, wparam, lparam)
}

/// Pure-logic dispatcher for a low-level mouse hook event (L0
/// refactor; SDD-084 hot path). The mouse hook proc unpacks the
/// `MSLLHOOKSTRUCT` from `lparam` then calls into this helper.
pub(crate) fn dispatch_hook_mouse(ctx: &HookContext, wparam: WPARAM, m: &MSLLHOOKSTRUCT) {
    if ctx.binding.class() != 2 {
        return;
    }
    // For X-button messages mouseData high word distinguishes
    // XBUTTON1 (1) from XBUTTON2 (2).
    let xbutton = (m.mouseData >> 16) as u16;
    let want = ctx.binding.mouse_btn();
    let want_xbutton = if want == 4 {
        XBUTTON1
    } else if want == 5 {
        XBUTTON2
    } else {
        0
    };
    if xbutton != want_xbutton || want_xbutton == 0 {
        return;
    }
    match wparam.0 as u32 {
        WM_XBUTTONDOWN => ctx.gate.set(true),
        WM_XBUTTONUP => ctx.gate.set(false),
        _ => {}
    }
}

// ---------- Tests (Windows-only) ----------
//
// These unit tests exercise the pure-logic dispatchers refactored
// out of the WndProc / hook procs (L0). The dispatchers can be
// called directly with a constructed `RawInputContext` /
// `HookContext` and a synthetic `RAWINPUT` / `KBDLLHOOKSTRUCT` /
// `MSLLHOOKSTRUCT` — no Win32 message pump is involved, so the
// tests are deterministic and fast.

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ptt_backends::PttInputClass;

    fn fresh_binding() -> Arc<AtomicBinding> {
        Arc::new(AtomicBinding::new())
    }

    // ---- L2: AtomicBinding ----

    #[test]
    fn atomic_binding_default_is_all_zero() {
        let b = AtomicBinding::new();
        assert_eq!(b.class(), 0);
        assert_eq!(b.vk(), 0);
        assert_eq!(b.mouse_btn(), 0);
    }

    #[test]
    fn atomic_binding_store_round_trip() {
        let b = AtomicBinding::new();
        b.store(1, 0x42, 0);
        assert_eq!(b.class(), 1);
        assert_eq!(b.vk(), 0x42);
        assert_eq!(b.mouse_btn(), 0);

        b.store(2, 0, 5);
        assert_eq!(b.class(), 2);
        assert_eq!(b.vk(), 0);
        assert_eq!(b.mouse_btn(), 5);
    }

    #[test]
    fn atomic_binding_clear_resets_all_fields() {
        let b = AtomicBinding::new();
        b.store(1, 0x42, 7);
        b.clear();
        assert_eq!(b.class(), 0);
        assert_eq!(b.vk(), 0);
        assert_eq!(b.mouse_btn(), 0);
    }

    #[test]
    fn atomic_binding_u8_and_u16_no_torn_reads() {
        // Atomic semantics on individual fields preclude torn
        // reads. Smoke-check by toggling a non-zero value.
        let b = AtomicBinding::new();
        for v in [0u16, 1, 0xFF, 0xFFFE, 0x7F00] {
            b.store(1, v, 0);
            assert_eq!(b.vk(), v);
        }
    }

    #[test]
    fn atomic_binding_concurrent_single_writer_single_reader() {
        use std::sync::atomic::{AtomicBool, Ordering};
        use std::thread;
        let b = Arc::new(AtomicBinding::new());
        let stop = Arc::new(AtomicBool::new(false));

        let bw = b.clone();
        let stopw = stop.clone();
        let writer = thread::spawn(move || {
            for i in 0..10_000u32 {
                if i % 2 == 0 {
                    bw.store(1, 0x41, 0);
                } else {
                    bw.store(2, 0, 5);
                }
                if stopw.load(Ordering::Relaxed) {
                    break;
                }
            }
        });

        let br = b.clone();
        let stopr = stop.clone();
        let reader = thread::spawn(move || {
            for _ in 0..10_000 {
                let c = br.class();
                let vk = br.vk();
                let m = br.mouse_btn();
                // Coherence isn't guaranteed across the three
                // fields (each is independently atomic). Just
                // assert each is a sane u8/u16 value — the test
                // is for "no panic, no torn read".
                assert!(c <= 2);
                let _ = vk;
                let _ = m;
                if stopr.load(Ordering::Relaxed) {
                    break;
                }
            }
        });

        writer.join().unwrap();
        stop.store(true, Ordering::Relaxed);
        reader.join().unwrap();
    }

    #[test]
    fn atomic_binding_many_readers_single_writer() {
        use std::thread;
        let b = Arc::new(AtomicBinding::new());
        b.store(1, 0x42, 0);
        let mut handles = Vec::new();
        for _ in 0..4 {
            let br = b.clone();
            handles.push(thread::spawn(move || {
                for _ in 0..5_000 {
                    let c = br.class();
                    assert!(c <= 2);
                }
            }));
        }
        let bw = b.clone();
        let writer = thread::spawn(move || {
            for i in 0..5_000u16 {
                bw.store(1, i, 0);
            }
        });
        writer.join().unwrap();
        for h in handles {
            h.join().unwrap();
        }
    }

    // ---- L3: resolve_binding dispatcher ----

    fn binding(class: PttInputClass, key: &str) -> PttBinding {
        PttBinding {
            input_class: class,
            platform_key: key.to_string(),
        }
    }

    #[test]
    fn resolve_binding_none_returns_zero_triple() {
        // PttInputClass::None is the "no binding" path. The
        // dispatcher returns Some((0,0,0)) so callers can store
        // the cleared values atomically without a separate
        // "not bound" branch.
        let r = resolve_binding(&binding(PttInputClass::None, ""));
        assert_eq!(r, Some((0, 0, 0)));
    }

    #[test]
    fn resolve_binding_keyboard_space() {
        let r = resolve_binding(&binding(PttInputClass::Keyboard, "Space"));
        assert_eq!(r, Some((1, 0x20, 0)));
    }

    #[test]
    fn resolve_binding_keyboard_letter_a() {
        let r = resolve_binding(&binding(PttInputClass::Keyboard, "A"));
        assert_eq!(r, Some((1, 0x41, 0)));
    }

    #[test]
    fn resolve_binding_keyboard_f10() {
        let r = resolve_binding(&binding(PttInputClass::Keyboard, "F10"));
        assert_eq!(r, Some((1, 0x79, 0)));
    }

    #[test]
    fn resolve_binding_keyboard_gibberish_returns_none() {
        let r = resolve_binding(&binding(PttInputClass::Keyboard, "GibberishKey"));
        assert_eq!(r, None);
    }

    #[test]
    fn resolve_binding_keyboard_empty_returns_none() {
        let r = resolve_binding(&binding(PttInputClass::Keyboard, ""));
        assert_eq!(r, None);
    }

    #[test]
    fn resolve_binding_mouse_back_button() {
        let r = resolve_binding(&binding(
            PttInputClass::MouseSideButton,
            "mouse-side-button:8",
        ));
        assert_eq!(r, Some((2, 0, 4)));
    }

    #[test]
    fn resolve_binding_mouse_forward_button() {
        let r = resolve_binding(&binding(
            PttInputClass::MouseSideButton,
            "mouse-side-button:16",
        ));
        assert_eq!(r, Some((2, 0, 5)));
    }

    #[test]
    fn resolve_binding_mouse_unknown_bitmask_returns_none() {
        let r = resolve_binding(&binding(
            PttInputClass::MouseSideButton,
            "mouse-side-button:99",
        ));
        assert_eq!(r, None);
    }

    #[test]
    fn resolve_binding_mouse_empty_returns_none() {
        let r = resolve_binding(&binding(PttInputClass::MouseSideButton, ""));
        assert_eq!(r, None);
    }

    #[test]
    fn resolve_binding_mismatched_class_and_key_returns_none() {
        // Keyboard class + mouse-side-button key string: the
        // keymap parses the string as a key label and finds no
        // match → None.
        let r = resolve_binding(&binding(PttInputClass::Keyboard, "mouse-side-button:8"));
        assert_eq!(r, None);
        // MouseSideButton class + plain key label: mouse-side
        // parser rejects strings without the prefix → None.
        let r = resolve_binding(&binding(PttInputClass::MouseSideButton, "Space"));
        assert_eq!(r, None);
    }

    // ---- L4: Backend state machine ----

    #[test]
    fn raw_input_backend_descriptor_before_start_is_l0() {
        let b = WindowsRawInputBackend::try_new().expect("backend constructible");
        let d = b.descriptor();
        assert_eq!(d.level, PttCapabilityLevel::L0Focused);
        assert_eq!(d.backend_id, "raw-input");
        assert_eq!(d.bound_input_class, None);
    }

    #[test]
    fn hook_backend_descriptor_before_start_is_l0() {
        let b = WindowsHookBackend::try_new().expect("backend constructible");
        let d = b.descriptor();
        assert_eq!(d.level, PttCapabilityLevel::L0Focused);
        assert_eq!(d.backend_id, "low-level-hook");
        assert_eq!(d.bound_input_class, None);
    }

    #[test]
    fn raw_input_backend_descriptor_after_arm_keyboard_is_l2() {
        // Synthesise an "armed" state without actually starting
        // the message pump by writing the AtomicBool + binding
        // directly. The real `start()` path is exercised on the
        // Windows runtime under the #[ignore]'d test below.
        let b = WindowsRawInputBackend::try_new().unwrap();
        b.binding.store(1, 0x42, 0);
        b.is_mouse.store(false, Ordering::Release);
        b.armed.store(true, Ordering::Release);
        let d = b.descriptor();
        assert_eq!(d.level, PttCapabilityLevel::L2GlobalHoldToTalk);
        assert_eq!(d.bound_input_class, Some("keyboard"));
    }

    #[test]
    fn raw_input_backend_descriptor_after_arm_mouse_is_l3() {
        let b = WindowsRawInputBackend::try_new().unwrap();
        b.binding.store(2, 0, 4);
        b.is_mouse.store(true, Ordering::Release);
        b.armed.store(true, Ordering::Release);
        let d = b.descriptor();
        assert_eq!(d.level, PttCapabilityLevel::L3GlobalWithMouseButtons);
        assert_eq!(d.bound_input_class, Some("mouse-side-button"));
    }

    #[test]
    fn hook_backend_descriptor_after_arm_keyboard_is_l2() {
        let b = WindowsHookBackend::try_new().unwrap();
        b.binding.store(1, 0x42, 0);
        b.is_mouse.store(false, Ordering::Release);
        b.armed.store(true, Ordering::Release);
        let d = b.descriptor();
        assert_eq!(d.level, PttCapabilityLevel::L2GlobalHoldToTalk);
        assert_eq!(d.bound_input_class, Some("keyboard"));
    }

    #[test]
    fn hook_backend_descriptor_after_arm_mouse_is_l3() {
        let b = WindowsHookBackend::try_new().unwrap();
        b.binding.store(2, 0, 5);
        b.is_mouse.store(true, Ordering::Release);
        b.armed.store(true, Ordering::Release);
        let d = b.descriptor();
        assert_eq!(d.level, PttCapabilityLevel::L3GlobalWithMouseButtons);
        assert_eq!(d.bound_input_class, Some("mouse-side-button"));
    }

    #[test]
    fn raw_input_backend_start_rejects_invalid_binding() {
        let mut b = WindowsRawInputBackend::try_new().unwrap();
        let gate = AudioTransmitGate::new(false);
        let bad = binding(PttInputClass::Keyboard, "Banana");
        let r = b.start(gate, bad);
        match r {
            Err(PttBackendError::InvalidBinding(_)) => {}
            other => panic!("expected InvalidBinding, got {other:?}"),
        }
    }

    #[test]
    fn hook_backend_start_rejects_invalid_binding() {
        let mut b = WindowsHookBackend::try_new().unwrap();
        let gate = AudioTransmitGate::new(false);
        let bad = binding(PttInputClass::Keyboard, "Banana");
        let r = b.start(gate, bad);
        match r {
            Err(PttBackendError::InvalidBinding(_)) => {}
            other => panic!("expected InvalidBinding, got {other:?}"),
        }
    }

    #[test]
    fn raw_input_backend_rebind_to_none_leaves_class_zero() {
        let b = WindowsRawInputBackend::try_new().unwrap();
        b.binding.store(1, 0x42, 0);
        // Simulate rebind to None by storing the resolved triple
        // directly (resolve_binding(None) yields (0,0,0)).
        let resolved = resolve_binding(&PttBinding::none()).unwrap();
        b.binding.store(resolved.0, resolved.1, resolved.2);
        assert_eq!(b.binding.class(), 0);
        assert_eq!(b.binding.vk(), 0);
        assert_eq!(b.binding.mouse_btn(), 0);
    }

    #[test]
    fn raw_input_backend_stop_is_idempotent_when_never_started() {
        let mut b = WindowsRawInputBackend::try_new().unwrap();
        b.stop();
        b.stop();
        assert!(!b.armed.load(Ordering::Acquire));
        assert_eq!(b.binding.class(), 0);
    }

    #[test]
    fn hook_backend_stop_is_idempotent_when_never_started() {
        let mut b = WindowsHookBackend::try_new().unwrap();
        b.stop();
        b.stop();
        assert!(!b.armed.load(Ordering::Acquire));
        assert_eq!(b.binding.class(), 0);
    }

    /// Full start → descriptor → stop cycle on the real Windows
    /// runtime. Catches the regression where `armed` was only
    /// flipped after `WM_QUIT` (i.e. never during normal use), so
    /// `descriptor()` reported `L0Focused` even though the
    /// `RegisterRawInputDevices` call had succeeded.
    ///
    /// Not ignored — must run on every Windows test pass.
    #[test]
    fn raw_input_backend_start_flips_armed_to_l2() {
        let mut b = WindowsRawInputBackend::try_new().unwrap();
        let gate = AudioTransmitGate::new(false);
        b.start(gate, binding(PttInputClass::Keyboard, "Space"))
            .expect("real RegisterRawInputDevices should succeed on a Windows desktop");
        // The worker thread flips armed inside the message-pump
        // loop. Give it a tiny window to do so.
        std::thread::sleep(std::time::Duration::from_millis(80));
        let d = b.descriptor();
        assert_eq!(
            d.level,
            PttCapabilityLevel::L2GlobalHoldToTalk,
            "armed must flip to true while the loop is running"
        );
        assert_eq!(d.backend_id, "raw-input");
        assert_eq!(d.bound_input_class, Some("keyboard"));
        b.stop();
    }

    /// Same as above but for the WH_KEYBOARD_LL / WH_MOUSE_LL hook
    /// backend. Catches the parallel armed-flag regression there.
    #[test]
    fn hook_backend_start_flips_armed_to_l2() {
        let mut b = WindowsHookBackend::try_new().unwrap();
        let gate = AudioTransmitGate::new(false);
        b.start(gate, binding(PttInputClass::Keyboard, "Space"))
            .expect("real SetWindowsHookExW should succeed on a Windows desktop");
        std::thread::sleep(std::time::Duration::from_millis(80));
        let d = b.descriptor();
        assert_eq!(
            d.level,
            PttCapabilityLevel::L2GlobalHoldToTalk,
            "armed must flip to true while the hook is running"
        );
        assert_eq!(d.backend_id, "low-level-hook");
        b.stop();
    }

    // ---- L5: dispatch_raw_input ----

    /// Construct a zero-filled RAWINPUT then fill the header and
    /// the keyboard / mouse union member as needed.
    unsafe fn zeroed_raw_input() -> RAWINPUT {
        std::mem::zeroed::<RAWINPUT>()
    }

    #[test]
    fn dispatch_raw_input_keyboard_match_press_then_release_drives_gate() {
        let bind = fresh_binding();
        bind.store(1, 0x41, 0);
        let gate = AudioTransmitGate::new(false);
        let ctx = RawInputContext {
            gate: gate.clone(),
            binding: bind,
        };
        unsafe {
            let mut raw = zeroed_raw_input();
            raw.header.dwType = RIM_TYPEKEYBOARD.0;
            raw.data.keyboard.VKey = 0x41;
            raw.data.keyboard.Flags = 0; // down
            dispatch_raw_input(&ctx, &raw);
            assert!(gate.load(), "down edge must set the gate");

            raw.data.keyboard.Flags = 1; // up (RI_KEY_BREAK)
            dispatch_raw_input(&ctx, &raw);
            assert!(!gate.load(), "up edge must clear the gate");
        }
    }

    #[test]
    fn dispatch_raw_input_keyboard_non_matching_vk_leaves_gate_unchanged() {
        let bind = fresh_binding();
        bind.store(1, 0x41, 0);
        let gate = AudioTransmitGate::new(false);
        let ctx = RawInputContext {
            gate: gate.clone(),
            binding: bind,
        };
        unsafe {
            let mut raw = zeroed_raw_input();
            raw.header.dwType = RIM_TYPEKEYBOARD.0;
            raw.data.keyboard.VKey = 0x42; // bound is A, this is B
            raw.data.keyboard.Flags = 0;
            dispatch_raw_input(&ctx, &raw);
            assert!(!gate.load(), "non-matching key must not fire the gate");
        }
    }

    #[test]
    fn dispatch_raw_input_no_binding_class_ignores_all_events() {
        let bind = fresh_binding(); // class=0
        let gate = AudioTransmitGate::new(false);
        let ctx = RawInputContext {
            gate: gate.clone(),
            binding: bind,
        };
        unsafe {
            let mut raw = zeroed_raw_input();
            raw.header.dwType = RIM_TYPEKEYBOARD.0;
            raw.data.keyboard.VKey = 0x41;
            raw.data.keyboard.Flags = 0;
            dispatch_raw_input(&ctx, &raw);
            assert!(!gate.load());
        }
    }

    #[test]
    fn dispatch_raw_input_mouse_button4_press_release_drives_gate() {
        let bind = fresh_binding();
        bind.store(2, 0, 4);
        let gate = AudioTransmitGate::new(false);
        let ctx = RawInputContext {
            gate: gate.clone(),
            binding: bind,
        };
        // RI_MOUSE_BUTTON_4_DOWN = 0x0040, _UP = 0x0080.
        unsafe {
            let mut raw = zeroed_raw_input();
            raw.header.dwType = RIM_TYPEMOUSE.0;
            raw.data.mouse.Anonymous.Anonymous.usButtonFlags = 0x0040;
            dispatch_raw_input(&ctx, &raw);
            assert!(gate.load());

            raw.data.mouse.Anonymous.Anonymous.usButtonFlags = 0x0080;
            dispatch_raw_input(&ctx, &raw);
            assert!(!gate.load());
        }
    }

    #[test]
    fn dispatch_raw_input_mouse_button5_press_release_drives_gate() {
        let bind = fresh_binding();
        bind.store(2, 0, 5);
        let gate = AudioTransmitGate::new(false);
        let ctx = RawInputContext {
            gate: gate.clone(),
            binding: bind,
        };
        // RI_MOUSE_BUTTON_5_DOWN = 0x0100, _UP = 0x0200.
        unsafe {
            let mut raw = zeroed_raw_input();
            raw.header.dwType = RIM_TYPEMOUSE.0;
            raw.data.mouse.Anonymous.Anonymous.usButtonFlags = 0x0100;
            dispatch_raw_input(&ctx, &raw);
            assert!(gate.load());

            raw.data.mouse.Anonymous.Anonymous.usButtonFlags = 0x0200;
            dispatch_raw_input(&ctx, &raw);
            assert!(!gate.load());
        }
    }

    #[test]
    fn dispatch_raw_input_mouse_button4_event_ignored_when_bound_to_button5() {
        let bind = fresh_binding();
        bind.store(2, 0, 5); // bound to XBUTTON2
        let gate = AudioTransmitGate::new(false);
        let ctx = RawInputContext {
            gate: gate.clone(),
            binding: bind,
        };
        unsafe {
            let mut raw = zeroed_raw_input();
            raw.header.dwType = RIM_TYPEMOUSE.0;
            raw.data.mouse.Anonymous.Anonymous.usButtonFlags = 0x0040; // BTN4 down
            dispatch_raw_input(&ctx, &raw);
            assert!(!gate.load());
            raw.data.mouse.Anonymous.Anonymous.usButtonFlags = 0x0080; // BTN4 up
            dispatch_raw_input(&ctx, &raw);
            assert!(!gate.load());
        }
    }

    #[test]
    fn dispatch_raw_input_hid_type_is_ignored() {
        // dwType = RIM_TYPEHID = 2 in the Win32 SDK. Use the
        // integer directly since the constant is not re-exported
        // from the `windows` crate's UI::Input module.
        const RIM_TYPEHID: u32 = 2;
        let bind = fresh_binding();
        bind.store(1, 0x41, 0);
        let gate = AudioTransmitGate::new(false);
        let ctx = RawInputContext {
            gate: gate.clone(),
            binding: bind,
        };
        unsafe {
            let mut raw = zeroed_raw_input();
            raw.header.dwType = RIM_TYPEHID;
            dispatch_raw_input(&ctx, &raw);
            assert!(!gate.load());
        }
    }

    // ---- L6: dispatch_hook_keyboard + dispatch_hook_mouse ----

    fn zeroed_kbd() -> KBDLLHOOKSTRUCT {
        unsafe { std::mem::zeroed() }
    }

    fn zeroed_mouse() -> MSLLHOOKSTRUCT {
        unsafe { std::mem::zeroed() }
    }

    #[test]
    fn dispatch_hook_keyboard_match_keydown_then_keyup_drives_gate() {
        let bind = fresh_binding();
        bind.store(1, 0x41, 0);
        let gate = AudioTransmitGate::new(false);
        let ctx = HookContext {
            gate: gate.clone(),
            binding: bind,
        };
        let mut kb = zeroed_kbd();
        kb.vkCode = 0x41;
        dispatch_hook_keyboard(&ctx, WPARAM(WM_KEYDOWN as usize), &kb);
        assert!(gate.load());
        dispatch_hook_keyboard(&ctx, WPARAM(WM_KEYUP as usize), &kb);
        assert!(!gate.load());
    }

    #[test]
    fn dispatch_hook_keyboard_match_syskeydown_and_syskeyup_drives_gate() {
        let bind = fresh_binding();
        bind.store(1, 0x12, 0); // VK_MENU (Alt)
        let gate = AudioTransmitGate::new(false);
        let ctx = HookContext {
            gate: gate.clone(),
            binding: bind,
        };
        let mut kb = zeroed_kbd();
        kb.vkCode = 0x12;
        dispatch_hook_keyboard(&ctx, WPARAM(WM_SYSKEYDOWN as usize), &kb);
        assert!(gate.load());
        dispatch_hook_keyboard(&ctx, WPARAM(WM_SYSKEYUP as usize), &kb);
        assert!(!gate.load());
    }

    #[test]
    fn dispatch_hook_keyboard_non_matching_vk_leaves_gate_unchanged() {
        let bind = fresh_binding();
        bind.store(1, 0x41, 0);
        let gate = AudioTransmitGate::new(false);
        let ctx = HookContext {
            gate: gate.clone(),
            binding: bind,
        };
        let mut kb = zeroed_kbd();
        kb.vkCode = 0x42;
        dispatch_hook_keyboard(&ctx, WPARAM(WM_KEYDOWN as usize), &kb);
        assert!(!gate.load());
    }

    #[test]
    fn dispatch_hook_keyboard_ignored_when_class_is_not_keyboard() {
        let bind = fresh_binding();
        bind.store(2, 0, 4); // mouse class
        let gate = AudioTransmitGate::new(false);
        let ctx = HookContext {
            gate: gate.clone(),
            binding: bind,
        };
        let mut kb = zeroed_kbd();
        kb.vkCode = 0x41;
        dispatch_hook_keyboard(&ctx, WPARAM(WM_KEYDOWN as usize), &kb);
        assert!(!gate.load());
    }

    #[test]
    fn dispatch_hook_mouse_match_xbutton1_press_release_drives_gate() {
        let bind = fresh_binding();
        bind.store(2, 0, 4);
        let gate = AudioTransmitGate::new(false);
        let ctx = HookContext {
            gate: gate.clone(),
            binding: bind,
        };
        let mut m = zeroed_mouse();
        // XBUTTON1 = 0x0001 in the high word of mouseData.
        m.mouseData = (XBUTTON1 as u32) << 16;
        dispatch_hook_mouse(&ctx, WPARAM(WM_XBUTTONDOWN as usize), &m);
        assert!(gate.load());
        dispatch_hook_mouse(&ctx, WPARAM(WM_XBUTTONUP as usize), &m);
        assert!(!gate.load());
    }

    #[test]
    fn dispatch_hook_mouse_match_xbutton2_press_release_drives_gate() {
        let bind = fresh_binding();
        bind.store(2, 0, 5);
        let gate = AudioTransmitGate::new(false);
        let ctx = HookContext {
            gate: gate.clone(),
            binding: bind,
        };
        let mut m = zeroed_mouse();
        m.mouseData = (XBUTTON2 as u32) << 16;
        dispatch_hook_mouse(&ctx, WPARAM(WM_XBUTTONDOWN as usize), &m);
        assert!(gate.load());
        dispatch_hook_mouse(&ctx, WPARAM(WM_XBUTTONUP as usize), &m);
        assert!(!gate.load());
    }

    #[test]
    fn dispatch_hook_mouse_wrong_xbutton_is_ignored() {
        let bind = fresh_binding();
        bind.store(2, 0, 4); // bound to XBUTTON1
        let gate = AudioTransmitGate::new(false);
        let ctx = HookContext {
            gate: gate.clone(),
            binding: bind,
        };
        let mut m = zeroed_mouse();
        m.mouseData = (XBUTTON2 as u32) << 16; // wrong button
        dispatch_hook_mouse(&ctx, WPARAM(WM_XBUTTONDOWN as usize), &m);
        assert!(!gate.load());
    }
}
