//! TUI tab framework — common contract types for tab modules.
//!
//! Every TUI tab module exports a `pub const tab = struct { ... };`
//! that conforms to the contract documented here. A small comptime
//! walker in `src/tui.zig` discovers tabs via the `tab_modules`
//! anonymous struct literal and uses these types to wire dispatch,
//! the help overlay, and the status-line hint.
//!
//! ## Contract (per-tab)
//!
//! ```zig
//! pub const Action = enum { /* tab-local keybind actions */ };
//! pub const State  = struct { /* tab-private state */ };
//!
//! pub const meta: framework.TabMeta(Action) = .{
//!     .label = "...",
//!     .default_bindings = &.{ ... },
//!     .action_labels = std.enums.EnumArray(Action, []const u8).init(.{ ... }),
//!     .status_hints = &.{ ... },
//! };
//!
//! pub const tab = struct {
//!     pub const ActionT = Action;
//!     pub const StateT = State;
//!
//!     // ── Lifecycle hooks (required) ──────────────────────────
//!     pub fn init(state: *State, app: *App) !void { ... }
//!     pub fn deinit(state: *State, app: *App) void { ... }
//!     pub fn activate(state: *State, app: *App) !void { ... }
//!     pub fn deactivate(state: *State, app: *App) void { ... }
//!     pub fn reload(state: *State, app: *App) !void { ... }
//!     pub fn tick(state: *State, app: *App, frame: u64) void { ... }
//!
//!     // ── Action dispatch (required) ──────────────────────────
//!     pub fn handleAction(state: *State, app: *App, action: Action) void { ... }
//!
//!     // ── Event hooks (each optional) ─────────────────────────
//!     // Tabs that don't care about an event class simply omit the
//!     // method. The framework's dispatcher checks `@hasDecl` and
//!     // skips the call entirely. Each method returns `bool` —
//!     // true means "consumed; don't fall through to global
//!     // handling."
//!     //
//!     // Chrome ownership: the framework owns the tab bar (row 0)
//!     // and filters chrome-region events out before dispatching.
//!     // Tab handlers receive only events the framework didn't
//!     // claim — so `handleMouse` will never see a row-0 click,
//!     // and there's no need for tab handlers to test `row == 0`
//!     // themselves. (Future: a per-tab "claim chrome region" hook
//!     // would let a tab opt in to drawing into chrome and
//!     // receiving its events; for now, chrome is framework-only.)
//!     pub fn handleKey(state: *State, app: *App, key: vaxis.Key) bool { ... }
//!     pub fn handleMouse(state: *State, app: *App, mouse: vaxis.Mouse) bool { ... }
//!     pub fn handlePaste(state: *State, app: *App, text: []const u8) bool { ... }
//!     pub fn statusOverride(state: *State, app: *App) ?framework.StatusOverride { ... }
//!
//!     /// Optional: does this tab currently have async work in
//!     /// flight that needs poll-driven redraws? While the ACTIVE
//!     /// tab answers true, the App keeps a one-shot vxfw Tick
//!     /// timer armed (~100ms cadence) so the draw loop wakes up
//!     /// and runs the tab's `tick` hook even with no user input —
//!     /// without it, async results would sit invisible until the
//!     /// next keypress.
//!     ///
//!     /// Answer from your own state (e.g. "my observation panel
//!     /// has pending checks"); do NOT cache the answer anywhere on
//!     /// App. The framework derives the polling decision fresh
//!     /// after every event, so tab switches and work completion
//!     /// are picked up automatically with zero choreography.
//!     /// Inactive tabs are never asked — switching away pauses
//!     /// UI polling while background work continues.
//!     pub fn wantsPollTick(state: *State, app: *App) bool { ... }
//!
//!     // ── Context-change hooks (optional) ─────────────────────
//!     // Fire when a global context this tab depends on changes.
//!     // Tabs that don't care simply omit the method. Contrast with
//!     // `reload` (drops data AND triggers a fetch); these hooks
//!     // drop data but DON'T trigger a fetch — the fetch happens
//!     // lazily on next `activate`.
//!     pub fn onSymbolChange(state: *State, app: *App) void { ... }
//!
//!     /// Fired when the portfolio file is reloaded (user pressed
//!     /// `r`/F5, file watcher triggered, etc.). Every tab that
//!     /// holds derived state pointing into the previous portfolio
//!     /// (cached `findings_view`, analysis `result`, projection
//!     /// caches, row indices, account list) MUST drop it here —
//!     /// the underlying portfolio data has already been freed by
//!     /// the time this is called.
//!     ///
//!     /// Tabs that don't hold portfolio-derived state simply omit
//!     /// the hook. Broadcast via `App.broadcast`; called BEFORE
//!     /// the new portfolio is loaded so tabs see a clean-slate
//!     /// state, not a half-populated one.
//!     pub fn onPortfolioReload(state: *State, app: *App) void { ... }
//!
//!     /// Fired when the user invokes a global scroll-to-extreme
//!     /// action (`g`/`G`). Tabs with a cursor reset it to match
//!     /// the new scroll position. Tabs without a cursor omit
//!     /// this hook.
//!     pub fn onScroll(state: *State, app: *App, where: ScrollEdge) void { ... }
//!
//!     /// Fired when the user invokes a relative cursor-move via
//!     /// keyboard (`j`/`k`, ↑/↓). `delta` is signed: positive =
//!     /// down, negative = up. Magnitude is 1 per keypress. Tabs
//!     /// with a row cursor step it, clamp to row count, and
//!     /// ensure visibility; return `true` to consume. Tabs without
//!     /// a cursor (or with empty rows) return `false` so the
//!     /// framework falls through to scroll-by-`delta` instead.
//!     ///
//!     /// **Mouse wheel events go through `onWheelMove`, NOT this
//!     /// hook.** A tab that wants wheel-as-cursor-move (the legacy
//!     /// portfolio-tab convention) implements `onWheelMove` to
//!     /// delegate to `onCursorMove`. A tab that wants
//!     /// wheel-as-viewport-scroll (the cleaner default for tabs
//!     /// with multiple cursor regions) declines `onWheelMove` and
//!     /// the framework scrolls instead.
//!     pub fn onCursorMove(state: *State, app: *App, delta: isize) bool { ... }
//!
//!     /// Fired when the user wheels the mouse. `delta` is signed:
//!     /// positive = down, negative = up. Magnitude is whatever the
//!     /// terminal reports per wheel detent (typically 3-5 lines on
//!     /// most platforms; the framework already debounces).
//!     ///
//!     /// Return `true` to consume; return `false` to fall through
//!     /// to viewport scroll. If a tab omits this hook entirely,
//!     /// the framework's default behavior is to delegate to
//!     /// `onCursorMove` — which preserves the legacy
//!     /// "wheel moves cursor" behavior for single-cursor tabs.
//!     ///
//!     /// New multi-region tabs (e.g. review tab with separate
//!     /// holdings + findings tables) should declare this hook and
//!     /// return `false` so wheel always scrolls the viewport,
//!     /// reserving cursor movement for keyboard and click.
//!     pub fn onWheelMove(state: *State, app: *App, delta: isize) bool { ... }
//!
//!     // ── Misc (required) ─────────────────────────────────────
//!     pub fn isDisabled(app: *App) bool { ... }
//! };
//! ```
//!
//! `meta` carries the four declarative fields; `tab` carries the
//! function hooks. Functions can't be bundled into a struct value,
//! so the data/behavior split is a Zig-language concession rather
//! than a stylistic preference. The split mirrors `commands/framework.zig`'s
//! `Meta` for command modules.
//!
//! Tabs without keybind actions ship with `Action = enum {}`,
//! `default_bindings = &.{}`, `action_labels =
//! std.enums.EnumArray(Action, []const u8).initFill("")`, and
//! `status_hints = &.{}`. No implicit defaults — the contract is
//! fully explicit for action-related fields and lifecycle hooks.
//! The event hooks (`handleKey`, `handleMouse`, `handlePaste`) and
//! context-change hooks (`onSymbolChange`) are the exception:
//! their absence means "this tab doesn't process that event class."
//!
//! Lifecycle hooks that aren't meaningful for a given tab can use
//! the `noop*` factory helpers below to inherit no-op
//! implementations:
//!
//! ```zig
//! pub const tab = struct {
//!     pub const deactivate = framework.noopDeactivate(State);
//!     pub const tick = framework.noopTick(State);
//!     // ... other hooks defined explicitly ...
//! };
//! ```

const std = @import("std");
const vaxis = @import("vaxis");
const StyledLine = @import("../tui.zig").StyledLine;
const validator = @import("../comptime_validator.zig");

/// Re-exported KeyCombo so tab modules don't need to import
/// keybinds.zig directly for binding declarations. This is the
/// SAME type as `keybinds.KeyCombo` (re-export, not a copy) — the
/// two names are interchangeable at type-level so values flow
/// freely between framework and keybind code without repacking.
pub const KeyCombo = @import("keybinds.zig").KeyCombo;

/// A single (action, key) pair for a tab's `default_bindings`.
/// Generic over the tab's Action enum so `default_bindings` can
/// be type-checked at comptime against the tab's own enum.
pub fn TabBinding(comptime ActionT: type) type {
    return struct {
        action: ActionT,
        key: KeyCombo,
    };
}

/// Per-tab declarative metadata. Mirrors `commands/framework.zig`'s
/// `Meta`: every tab module declares
/// `pub const meta: framework.TabMeta(Action) = .{ ... };` with all
/// fields populated. The compiler enforces the contract via the
/// field types; `validateTabModule` reduces to a single
/// `expectDeclWithType` check on the declaration's type.
///
/// Function hooks (init, deinit, activate, ..., handleAction, the
/// optional event hooks) live on the separate `pub const tab =
/// struct { ... };` namespace because Zig structs can't carry
/// `pub fn` declarations as values. The split is deliberate: data
/// in `meta`, behavior in `tab`.
///
/// Generic over `ActionT` so `default_bindings`, `action_labels`,
/// and `status_hints` can be type-checked at comptime against the
/// tab's own Action enum.
pub fn TabMeta(comptime ActionT: type) type {
    return struct {
        /// Display name for the tab bar. The framework composes
        /// this into `" {N}:{label} "` where N is the tab's
        /// 1-indexed registry position.
        label: []const u8,
        /// Default keybindings for this tab. Tabs without
        /// keybind actions ship with `&.{}`.
        default_bindings: []const TabBinding(ActionT),
        /// One label per Action variant for the help overlay.
        /// Tabs without keybind actions use
        /// `std.enums.EnumArray(Action, []const u8).init(.{})`.
        action_labels: std.enums.EnumArray(ActionT, []const u8),
        /// Subset of actions that show in the status-line hint.
        /// Tabs without status hints use `&.{}`.
        status_hints: []const ActionT,
    };
}

/// Returned by a tab's optional `statusOverride` hook to take
/// over the status-bar row while a tab-internal modal is active
/// (account picker, date input, etc).
///
/// Tabs declare their intent here and the App renders it; tabs
/// don't poke vaxis cells directly. Adding a new modal style is
/// "add a variant + render arm in App.drawStatusBar."
pub const StatusOverride = union(enum) {
    /// Render `text` as a full-width hint line in the input
    /// style. Used for navigation-only modals (e.g. account
    /// picker, where the modal accepts j/k/Enter/Esc but has no
    /// text input).
    hint: []const u8,

    /// Render an interactive input prompt. The App draws
    /// `prompt`, the live input buffer with cursor, and a
    /// right-aligned `hint`. Used for modals that accept text
    /// input (e.g. date input on projections).
    ///
    /// Note: the input buffer itself is App-owned shared state
    /// (`app.input_buf` / `app.input_len`), so only one input
    /// prompt can be active at a time. Modals that need their
    /// own buffer will require a framework extension; today,
    /// shared input is sufficient.
    input_prompt: struct {
        prompt: []const u8,
        hint: []const u8,
    },
};

/// Argument passed to `onScroll` indicating which extreme the
/// user scrolled to. `top` corresponds to the global `scroll_top`
/// action (default: `g`); `bottom` corresponds to `scroll_bottom`
/// (default: `G`).
pub const ScrollEdge = enum { top, bottom };

// ── Lifecycle hook factories (no-op defaults) ─────────────────
//
// Tabs that don't need a particular lifecycle hook can declare
// `pub const deactivate = framework.noopDeactivate(State);` etc.
// This keeps the contract explicit (every required field is named
// in the tab struct) while letting tabs avoid writing dummy bodies.
//
// Event hooks (handleKey, handleMouse, handlePaste) and context-
// change hooks (onSymbolChange) are NOT in this list — they're
// optional via `@hasDecl` checking, so a tab that doesn't care
// simply omits the method.

const App = @import("../tui.zig").App;

/// Returns a `deactivate(state, app) void` no-op for the given
/// State type.
pub fn noopDeactivate(comptime StateT: type) fn (*StateT, *App) void {
    return struct {
        fn f(_: *StateT, _: *App) void {}
    }.f;
}

/// Returns a `tick(state, app, frame) void` no-op for the given
/// State type.
pub fn noopTick(comptime StateT: type) fn (*StateT, *App, u64) void {
    return struct {
        fn f(_: *StateT, _: *App, _: u64) void {}
    }.f;
}

/// Returns an `isDisabled(app) bool` that always returns false.
/// The most common case — most tabs are always enabled.
pub fn alwaysEnabled() fn (*App) bool {
    return struct {
        fn f(_: *App) bool {
            return false;
        }
    }.f;
}

// ── Comptime contract validation ──────────────────────────────
//
// `validateTabModule(comptime Module: type)` walks a tab module
// at comptime and asserts it conforms to the framework contract.
// Every error message includes the full expected signature so the
// developer knows exactly what to add, copy-paste ready.
//
// Call site policy: registry-walk only. The registry in
// `src/tui.zig` calls this once per entry. Do NOT add in-file
// `comptime { framework.validateTabModule(@This()); }` blocks to
// individual tab files — they're redundant with the registry walk
// under both `zig build` and ZLS build-on-save (the only ZLS mode
// that runs comptime), and the registry walk produces a better
// diagnostic. Mirrors the policy in `src/commands/framework.zig`.

/// Validate a tab module against the framework contract. Emits a
/// `@compileError` with the full expected signature for any
/// missing or wrong-shape decl.
///
/// The contract is documented at the top of this file. This
/// function is the source of truth — if you change the contract,
/// update both.
pub fn validateTabModule(comptime Module: type) void {
    comptime {
        const mod_name = @typeName(Module);

        // ── Top-level decls ────────────────────────────────────
        if (!@hasDecl(Module, "Action")) {
            @compileError("Tab module `" ++ mod_name ++ "` is missing `pub const Action = enum { ... };` " ++
                "(use `enum {}` if the tab has no keybind actions)");
        }
        if (!@hasDecl(Module, "State")) {
            @compileError("Tab module `" ++ mod_name ++ "` is missing `pub const State = struct { ... };`");
        }
        if (!@hasDecl(Module, "tab")) {
            @compileError("Tab module `" ++ mod_name ++ "` is missing `pub const tab = struct { ... };` " ++
                "(see src/tui/tab_framework.zig for the full contract)");
        }

        const Action = Module.Action;
        const State = Module.State;
        const tab_decl = Module.tab;

        // Sanity: Action must be an enum, State must be a struct.
        if (@typeInfo(Action) != .@"enum") {
            @compileError("Tab module `" ++ mod_name ++ "`: `Action` must be an enum type, got " ++ @typeName(Action));
        }
        if (@typeInfo(State) != .@"struct") {
            @compileError("Tab module `" ++ mod_name ++ "`: `State` must be a struct type, got " ++ @typeName(State));
        }

        // ── Type aliases inside `tab` ──────────────────────────
        if (!@hasDecl(tab_decl, "ActionT")) {
            @compileError("Tab module `" ++ mod_name ++ "`: `tab` is missing `pub const ActionT = Action;`");
        }
        if (!@hasDecl(tab_decl, "StateT")) {
            @compileError("Tab module `" ++ mod_name ++ "`: `tab` is missing `pub const StateT = State;`");
        }

        // ── meta (top-level decl, struct value) ────────────────
        // One check covers all four declarative fields. Field
        // types are enforced by `TabMeta(Action)`'s declaration;
        // missing fields surface as Zig's native "missing field"
        // comptime errors pointing at the call site.
        validator.expectDeclWithType(
            "Tab module",
            mod_name,
            Module,
            "meta",
            TabMeta(Action),
            "pub const meta: framework.TabMeta(Action) = .{ .label = \"...\", .default_bindings = &.{ ... }, .action_labels = std.enums.EnumArray(Action, []const u8).init(.{ ... }), .status_hints = &.{ ... } };",
        );

        // ── Lifecycle hooks (required) ─────────────────────────
        validator.expectFnInferredError(
            "Tab module",
            mod_name,
            tab_decl,
            "init",
            &.{ *State, *App },
            void,
            "pub fn init(state: *State, app: *App) !void { ... }",
        );
        validator.expectFn(
            "Tab module",
            mod_name,
            tab_decl,
            "deinit",
            fn (*State, *App) void,
            "pub fn deinit(state: *State, app: *App) void { ... }",
        );
        validator.expectFnInferredError(
            "Tab module",
            mod_name,
            tab_decl,
            "activate",
            &.{ *State, *App },
            void,
            "pub fn activate(state: *State, app: *App) !void { ... }",
        );
        validator.expectFn(
            "Tab module",
            mod_name,
            tab_decl,
            "deactivate",
            fn (*State, *App) void,
            "pub fn deactivate(state: *State, app: *App) void { ... }",
        );
        validator.expectFnInferredError(
            "Tab module",
            mod_name,
            tab_decl,
            "reload",
            &.{ *State, *App },
            void,
            "pub fn reload(state: *State, app: *App) !void { ... }",
        );
        validator.expectFn(
            "Tab module",
            mod_name,
            tab_decl,
            "tick",
            fn (*State, *App, u64) void,
            "pub fn tick(state: *State, app: *App, frame: u64) void { ... }",
        );

        // ── Action dispatch (required) ─────────────────────────
        validator.expectFn(
            "Tab module",
            mod_name,
            tab_decl,
            "handleAction",
            fn (*State, *App, Action) void,
            "pub fn handleAction(state: *State, app: *App, action: Action) void { ... }",
        );

        // ── Misc (required) ────────────────────────────────────
        validator.expectFn(
            "Tab module",
            mod_name,
            tab_decl,
            "isDisabled",
            fn (*App) bool,
            "pub fn isDisabled(app: *App) bool { ... }",
        );

        // ── Event hooks (optional, but typed when present) ─────
        // `@hasDecl` returns true; if the signature is wrong we
        // surface that as a hard error so a typo'd signature
        // doesn't silently get skipped at dispatch time.
        if (@hasDecl(tab_decl, "handleKey")) {
            validator.expectFn(
                "Tab module",
                mod_name,
                tab_decl,
                "handleKey",
                fn (*State, *App, vaxis.Key) bool,
                "pub fn handleKey(state: *State, app: *App, key: vaxis.Key) bool { ... }",
            );
        }
        if (@hasDecl(tab_decl, "handleMouse")) {
            validator.expectFn(
                "Tab module",
                mod_name,
                tab_decl,
                "handleMouse",
                fn (*State, *App, vaxis.Mouse) bool,
                "pub fn handleMouse(state: *State, app: *App, mouse: vaxis.Mouse) bool { ... }",
            );
        }
        if (@hasDecl(tab_decl, "handlePaste")) {
            validator.expectFn(
                "Tab module",
                mod_name,
                tab_decl,
                "handlePaste",
                fn (*State, *App, []const u8) bool,
                "pub fn handlePaste(state: *State, app: *App, text: []const u8) bool { ... }",
            );
        }
        if (@hasDecl(tab_decl, "statusOverride")) {
            validator.expectFn(
                "Tab module",
                mod_name,
                tab_decl,
                "statusOverride",
                fn (*State, *App) ?StatusOverride,
                "pub fn statusOverride(state: *State, app: *App) ?StatusOverride { ... }",
            );
        }
        if (@hasDecl(tab_decl, "wantsPollTick")) {
            validator.expectFn(
                "Tab module",
                mod_name,
                tab_decl,
                "wantsPollTick",
                fn (*State, *App) bool,
                "pub fn wantsPollTick(state: *State, app: *App) bool { ... }",
            );
        }

        // ── Draw hooks (mutually exclusive, exactly one required) ──
        //
        // Every tab declares EXACTLY ONE of:
        //
        //   pub fn buildStyledLines(state: *State, app: *App, arena: std.mem.Allocator) ![]const StyledLine
        //   pub fn drawContent(state: *State, app: *App, arena: std.mem.Allocator, buf: []vaxis.Cell, width: u16, height: u16) !void
        //
        // `buildStyledLines` is for line-list tabs (App handles
        // scroll_offset clamping + cell rendering). `drawContent`
        // is for direct-buffer tabs (cell layout / Kitty
        // graphics that don't fit the line-list shape).
        //
        // The framework's draw dispatcher tries `drawContent`
        // first, then falls back to `buildStyledLines`. Declaring
        // neither leaves the tab unrenderable; declaring both is
        // ambiguous. The validator surfaces both as compile errors.
        const has_build = @hasDecl(Module, "buildStyledLines");
        const has_draw = @hasDecl(Module, "drawContent");
        if (!has_build and !has_draw) {
            @compileError("Tab module `" ++ mod_name ++ "` must declare exactly one of " ++
                "`buildStyledLines` or `drawContent`. See the framework draw-hook docs in tab_framework.zig.");
        }
        if (has_build and has_draw) {
            @compileError("Tab module `" ++ mod_name ++ "` declares both `buildStyledLines` and " ++
                "`drawContent`. Only one is allowed — pick the right one for your tab's render shape.");
        }
        if (has_build) {
            validator.expectFnInferredError(
                "Tab module",
                mod_name,
                Module,
                "buildStyledLines",
                &.{ *State, *App, std.mem.Allocator },
                []const StyledLine,
                "pub fn buildStyledLines(state: *State, app: *App, arena: std.mem.Allocator) ![]const StyledLine { ... }",
            );
        }
        if (has_draw) {
            validator.expectFnInferredError(
                "Tab module",
                mod_name,
                Module,
                "drawContent",
                &.{ *State, *App, std.mem.Allocator, []vaxis.Cell, u16, u16 },
                void,
                "pub fn drawContent(state: *State, app: *App, arena: std.mem.Allocator, buf: []vaxis.Cell, width: u16, height: u16) !void { ... }",
            );
        }

        // ── Context-change hooks (optional, typed when present) ──
        if (@hasDecl(tab_decl, "onSymbolChange")) {
            validator.expectFn(
                "Tab module",
                mod_name,
                tab_decl,
                "onSymbolChange",
                fn (*State, *App) void,
                "pub fn onSymbolChange(state: *State, app: *App) void { ... }",
            );
        }
        if (@hasDecl(tab_decl, "onPortfolioReload")) {
            validator.expectFn(
                "Tab module",
                mod_name,
                tab_decl,
                "onPortfolioReload",
                fn (*State, *App) void,
                "pub fn onPortfolioReload(state: *State, app: *App) void { ... }",
            );
        }
        if (@hasDecl(tab_decl, "onScroll")) {
            validator.expectFn(
                "Tab module",
                mod_name,
                tab_decl,
                "onScroll",
                fn (*State, *App, ScrollEdge) void,
                "pub fn onScroll(state: *State, app: *App, where: ScrollEdge) void { ... }",
            );
        }
        if (@hasDecl(tab_decl, "onCursorMove")) {
            validator.expectFn(
                "Tab module",
                mod_name,
                tab_decl,
                "onCursorMove",
                fn (*State, *App, isize) bool,
                "pub fn onCursorMove(state: *State, app: *App, delta: isize) bool { ... }",
            );
        }
        if (@hasDecl(tab_decl, "onWheelMove")) {
            validator.expectFn(
                "Tab module",
                mod_name,
                tab_decl,
                "onWheelMove",
                fn (*State, *App, isize) bool,
                "pub fn onWheelMove(state: *State, app: *App, delta: isize) bool { ... }",
            );
        }
    }
}

// ── Tests ─────────────────────────────────────────────────────

const testing = std.testing;

test "TabBinding generic over Action" {
    const A = enum { foo, bar };
    const B = TabBinding(A);
    const b: B = .{ .action = .foo, .key = .{ .codepoint = 'a' } };
    try testing.expectEqual(A.foo, b.action);
    try testing.expectEqual(@as(u21, 'a'), b.key.codepoint);
}

test "noopDeactivate returns a callable no-op" {
    const S = struct { x: u32 = 0 };
    const fn_ptr = noopDeactivate(S);
    var s: S = .{};
    var dummy_app: App = undefined;
    fn_ptr(&s, &dummy_app);
    try testing.expectEqual(@as(u32, 0), s.x);
}

test "noopTick returns a callable no-op" {
    const S = struct { x: u32 = 0 };
    const fn_ptr = noopTick(S);
    var s: S = .{};
    var dummy_app: App = undefined;
    fn_ptr(&s, &dummy_app, 42);
    try testing.expectEqual(@as(u32, 0), s.x);
}

test "alwaysEnabled returns false" {
    const fn_ptr = alwaysEnabled();
    var dummy_app: App = undefined;
    try testing.expect(!fn_ptr(&dummy_app));
}
