const std = @import("std");
const app_icon_tool = @import("app_icon");
const app_manifest = @import("app_manifest");
const diagnostics = @import("diagnostics");
const json_to_zon = @import("json_to_zon.zig");
const raw_manifest = @import("raw_manifest.zig");
const web_engine_tool = @import("web_engine.zig");

pub const ValidationResult = struct {
    ok: bool,
    message: []const u8,
};

pub const Metadata = struct {
    id: []const u8,
    name: []const u8,
    display_name: ?[]const u8 = null,
    /// One human-facing sentence about the app: the About panel credits
    /// line on macOS. Optional — absent means no credits line.
    description: ?[]const u8 = null,
    version: []const u8,
    icons: []const []const u8 = &.{},
    platforms: []const []const u8 = &.{},
    permissions: []const []const u8 = &.{},
    capabilities: []const []const u8 = &.{},
    dock_visible: bool = true,
    persist: ?PersistMetadata = null,
    images: ImagesMetadata = .{},
    service_packages: []const ServicePackageMetadata = &.{},
    /// Which carrier runs src/services operations: "auto", "in_process",
    /// or "child". Validated so a typo teaches at check time.
    service_carrier: []const u8 = "auto",
    /// In-process service pool width; null keeps the runtime default.
    service_pool_size: ?u8 = null,
    bridge_commands: []const BridgeCommandMetadata = &.{},
    web_engine: []const u8 = "system",
    /// Whether the app ships the embedded web layer: "auto" (default,
    /// inferred from the manifest's web declarations), "include", or
    /// "exclude". See `webLayer` for the inference.
    webview_layer: []const u8 = "auto",
    /// How a TypeScript core compiles: "external" (the default and only
    /// lane — the external core compiler). The removed transpiled
    /// lane's spelling is refused with a teaching at validation.
    core_compiler: []const u8 = "external",
    /// The built-in theme pack the app selects (`theme = "geist"`).
    /// Optional — absent keeps the house register. Validated against
    /// the known pack names so a typo is a check error, never a silent
    /// default-theme fallback.
    theme: ?[]const u8 = null,
    /// The manifest's one-accent brand override (`theme_accent =
    /// "#df2670"`), layered over the resolved pack by the runtime
    /// (`canvas.accentOverrides`; high contrast skips it). Optional —
    /// absent keeps the pack's own accent. Validated as a #rrggbb hex
    /// color so a typo is a check error, never a silent stock accent.
    theme_accent: ?[]const u8 = null,
    cef: web_engine_tool.CefConfig = .{},
    frontend: ?FrontendMetadata = null,
    security: SecurityMetadata = .{},
    windows: []const WindowMetadata = &.{},
    shell: ShellMetadata = .{},
    commands: []const CommandMetadata = &.{},
    menus: []const MenuMetadata = &.{},
    shortcuts: []const ShortcutMetadata = &.{},
    file_associations: []const FileAssociationMetadata = &.{},
    url_schemes: []const UrlSchemeMetadata = &.{},
    updates: UpdateMetadata = .{},
    dmg: DmgMetadata = .{},

    pub fn displayName(self: Metadata) []const u8 {
        return self.display_name orelse self.name;
    }

    pub fn deinit(self: Metadata, allocator: std.mem.Allocator) void {
        allocator.free(self.id);
        allocator.free(self.name);
        if (self.display_name) |value| allocator.free(value);
        if (self.description) |value| allocator.free(value);
        if (self.theme) |value| allocator.free(value);
        if (self.theme_accent) |value| allocator.free(value);
        allocator.free(self.version);
        allocator.free(self.web_engine);
        allocator.free(self.webview_layer);
        allocator.free(self.core_compiler);
        allocator.free(self.service_carrier);
        allocator.free(self.cef.dir);
        for (self.icons) |value| allocator.free(value);
        if (self.icons.len > 0) allocator.free(self.icons);
        for (self.platforms) |value| allocator.free(value);
        if (self.platforms.len > 0) allocator.free(self.platforms);
        for (self.permissions) |value| allocator.free(value);
        if (self.permissions.len > 0) allocator.free(self.permissions);
        for (self.capabilities) |value| allocator.free(value);
        if (self.capabilities.len > 0) allocator.free(self.capabilities);
        if (self.persist) |persist| {
            allocator.free(persist.restore.ok);
            allocator.free(persist.restore.none);
            allocator.free(persist.restore.err);
        }
        for (self.service_packages) |package_entry| {
            allocator.free(package_entry.name);
            allocator.free(package_entry.version);
            allocator.free(package_entry.content_hash);
        }
        if (self.service_packages.len > 0) allocator.free(self.service_packages);
        for (self.bridge_commands) |command| {
            allocator.free(command.name);
            for (command.permissions) |value| allocator.free(value);
            if (command.permissions.len > 0) allocator.free(command.permissions);
            for (command.origins) |value| allocator.free(value);
            if (command.origins.len > 0) allocator.free(command.origins);
        }
        if (self.bridge_commands.len > 0) allocator.free(self.bridge_commands);
        if (self.frontend) |frontend| {
            allocator.free(frontend.dist);
            allocator.free(frontend.entry);
            if (frontend.dev) |dev| {
                allocator.free(dev.url);
                for (dev.command) |value| allocator.free(value);
                if (dev.command.len > 0) allocator.free(dev.command);
                allocator.free(dev.ready_path);
            }
        }
        for (self.security.navigation.allowed_origins) |value| allocator.free(value);
        if (self.security.navigation.allowed_origins.len > 0) allocator.free(self.security.navigation.allowed_origins);
        if (!std.mem.eql(u8, self.security.navigation.external_links.action, "deny") or self.security.navigation.external_links.allowed_urls.len > 0) {
            allocator.free(self.security.navigation.external_links.action);
        }
        for (self.security.navigation.external_links.allowed_urls) |value| allocator.free(value);
        if (self.security.navigation.external_links.allowed_urls.len > 0) allocator.free(self.security.navigation.external_links.allowed_urls);
        for (self.windows) |window| {
            allocator.free(window.label);
            if (window.title) |title| allocator.free(title);
            allocator.free(window.titlebar);
            allocator.free(window.close_policy);
        }
        if (self.windows.len > 0) allocator.free(self.windows);
        for (self.shell.windows) |window| {
            allocator.free(window.label);
            if (window.title) |title| allocator.free(title);
            allocator.free(window.restore_policy);
            allocator.free(window.titlebar);
            allocator.free(window.close_policy);
            for (window.views) |view| {
                allocator.free(view.label);
                allocator.free(view.kind);
                if (view.parent) |parent| allocator.free(parent);
                if (view.edge) |edge| allocator.free(edge);
                if (view.axis) |axis| allocator.free(axis);
                if (view.role) |role| allocator.free(role);
                if (view.accessibility_label) |accessibility_label| allocator.free(accessibility_label);
                if (view.url) |url| allocator.free(url);
                if (view.text) |text| allocator.free(text);
                if (view.command) |command| allocator.free(command);
                if (view.gpu_backend) |gpu_backend| allocator.free(gpu_backend);
                if (view.gpu_pixel_format) |gpu_pixel_format| allocator.free(gpu_pixel_format);
                if (view.gpu_present_mode) |gpu_present_mode| allocator.free(gpu_present_mode);
                if (view.gpu_alpha_mode) |gpu_alpha_mode| allocator.free(gpu_alpha_mode);
                if (view.gpu_color_space) |gpu_color_space| allocator.free(gpu_color_space);
            }
            if (window.views.len > 0) allocator.free(window.views);
        }
        if (self.shell.windows.len > 0) allocator.free(self.shell.windows);
        for (self.shell.chrome.tabs) |tab| {
            allocator.free(tab.id);
            allocator.free(tab.label);
            allocator.free(tab.icon);
        }
        if (self.shell.chrome.tabs.len > 0) allocator.free(self.shell.chrome.tabs);
        if (self.shell.chrome.primary_action) |action| {
            allocator.free(action.id);
            allocator.free(action.label);
            allocator.free(action.icon);
        }
        for (self.commands) |command| {
            allocator.free(command.id);
            allocator.free(command.title);
        }
        if (self.commands.len > 0) allocator.free(self.commands);
        for (self.menus) |menu| {
            allocator.free(menu.title);
            for (menu.items) |item| {
                allocator.free(item.label);
                allocator.free(item.command);
                allocator.free(item.key);
                for (item.modifiers) |value| allocator.free(value);
                if (item.modifiers.len > 0) allocator.free(item.modifiers);
            }
            if (menu.items.len > 0) allocator.free(menu.items);
        }
        if (self.menus.len > 0) allocator.free(self.menus);
        for (self.shortcuts) |shortcut| {
            allocator.free(shortcut.id);
            allocator.free(shortcut.key);
            for (shortcut.modifiers) |value| allocator.free(value);
            if (shortcut.modifiers.len > 0) allocator.free(shortcut.modifiers);
        }
        if (self.shortcuts.len > 0) allocator.free(self.shortcuts);
        for (self.file_associations) |association| {
            allocator.free(association.name);
            allocator.free(association.role);
            for (association.extensions) |value| allocator.free(value);
            if (association.extensions.len > 0) allocator.free(association.extensions);
            for (association.mime_types) |value| allocator.free(value);
            if (association.mime_types.len > 0) allocator.free(association.mime_types);
            if (association.icon) |icon| allocator.free(icon);
        }
        if (self.file_associations.len > 0) allocator.free(self.file_associations);
        for (self.url_schemes) |scheme| {
            allocator.free(scheme.scheme);
            allocator.free(scheme.role);
        }
        if (self.url_schemes.len > 0) allocator.free(self.url_schemes);
        if (self.updates.feed_url) |value| allocator.free(value);
        if (self.updates.public_key) |value| allocator.free(value);
        if (self.dmg.volume_name) |value| allocator.free(value);
        if (self.dmg.background) |value| allocator.free(value);
        for (self.dmg.items) |item| {
            if (item.path) |value| allocator.free(value);
            if (item.name) |value| allocator.free(value);
        }
        if (self.dmg.items.len > 0) allocator.free(self.dmg.items);
    }
};

pub const ServicePackageMetadata = struct {
    name: []const u8,
    version: []const u8,
    content_hash: []const u8,
};

pub const PersistMetadata = struct {
    version: u64,
    debounce_ms: u32 = 500,
    restore: PersistRestoreMetadata,
};

pub const ImagesMetadata = struct {
    max_image_pixel_bytes: usize = 1024 * 1024,
};

pub const PersistRestoreMetadata = struct {
    ok: []const u8,
    none: []const u8,
    err: []const u8,
};

pub const BridgeCommandMetadata = struct {
    name: []const u8,
    permissions: []const []const u8 = &.{},
    origins: []const []const u8 = &.{},
};

pub const WindowMetadata = struct {
    label: []const u8 = "main",
    title: ?[]const u8 = null,
    width: f32 = 720,
    height: f32 = 480,
    x: ?f32 = null,
    y: ?f32 = null,
    resizable: bool = true,
    restore_state: bool = true,
    titlebar: []const u8 = "standard",
    transparent: bool = false,
    always_on_top: bool = false,
    click_through: bool = false,
    activate_on_show: bool = true,
    initially_hidden: bool = false,
    allows_fullscreen: bool = true,
    min_width: f32 = 0,
    min_height: f32 = 0,
    close_policy: []const u8 = "quit",
};

pub const ShellMetadata = struct {
    windows: []const ShellWindowMetadata = &.{},
    chrome: ShellChromeMetadata = .{},
};

pub const ShellChromeMetadata = struct {
    tabs: []const ShellTabMetadata = &.{},
    primary_action: ?ShellPrimaryActionMetadata = null,
};

pub const ShellTabMetadata = struct {
    id: []const u8,
    label: []const u8,
    icon: []const u8 = "",
};

pub const ShellPrimaryActionMetadata = struct {
    id: []const u8,
    label: []const u8,
    icon: []const u8 = "",
};

pub const ShellWindowMetadata = struct {
    label: []const u8 = "main",
    title: ?[]const u8 = null,
    width: f32 = 720,
    height: f32 = 480,
    x: ?f32 = null,
    y: ?f32 = null,
    resizable: bool = true,
    restore_state: bool = true,
    restore_policy: []const u8 = "clamp_to_visible_screen",
    titlebar: []const u8 = "standard",
    transparent: bool = false,
    always_on_top: bool = false,
    click_through: bool = false,
    activate_on_show: bool = true,
    initially_hidden: bool = false,
    allows_fullscreen: bool = true,
    min_width: f32 = 0,
    min_height: f32 = 0,
    close_policy: []const u8 = "quit",
    views: []const ShellViewMetadata = &.{},
};

pub const ShellViewMetadata = struct {
    label: []const u8,
    kind: []const u8,
    parent: ?[]const u8 = null,
    edge: ?[]const u8 = null,
    axis: ?[]const u8 = null,
    x: ?f32 = null,
    y: ?f32 = null,
    width: ?f32 = null,
    height: ?f32 = null,
    min_width: ?f32 = null,
    min_height: ?f32 = null,
    max_width: ?f32 = null,
    max_height: ?f32 = null,
    fill: bool = false,
    layer: i32 = 0,
    visible: bool = true,
    enabled: bool = true,
    role: ?[]const u8 = null,
    accessibility_label: ?[]const u8 = null,
    url: ?[]const u8 = null,
    text: ?[]const u8 = null,
    command: ?[]const u8 = null,
    gpu_backend: ?[]const u8 = null,
    gpu_pixel_format: ?[]const u8 = null,
    gpu_present_mode: ?[]const u8 = null,
    gpu_alpha_mode: ?[]const u8 = null,
    gpu_color_space: ?[]const u8 = null,
    gpu_vsync: ?bool = null,
};

pub const ShortcutMetadata = struct {
    id: []const u8,
    key: []const u8,
    modifiers: []const []const u8 = &.{},
};

pub const CommandMetadata = struct {
    id: []const u8,
    title: []const u8 = "",
    enabled: bool = true,
    checked: bool = false,
};

pub const MenuMetadata = struct {
    title: []const u8,
    items: []const MenuItemMetadata = &.{},
};

pub const MenuItemMetadata = struct {
    label: []const u8 = "",
    command: []const u8 = "",
    key: []const u8 = "",
    modifiers: []const []const u8 = &.{},
    separator: bool = false,
    enabled: bool = true,
    checked: bool = false,
};

pub const FileAssociationMetadata = struct {
    name: []const u8,
    role: []const u8 = "viewer",
    extensions: []const []const u8 = &.{},
    mime_types: []const []const u8 = &.{},
    icon: ?[]const u8 = null,
};

pub const UrlSchemeMetadata = struct {
    scheme: []const u8,
    role: []const u8 = "viewer",
};

pub const DmgPosition = struct {
    x: u16,
    y: u16,
};

pub const DmgItemKind = enum {
    app,
    applications,
    file,
    link,
};

pub const DmgItemMetadata = struct {
    kind: DmgItemKind,
    path: ?[]const u8 = null,
    name: ?[]const u8 = null,
    position: DmgPosition,
};

pub const DmgMetadata = struct {
    volume_name: ?[]const u8 = null,
    background: ?[]const u8 = null,
    window_width: u16 = 660,
    window_height: u16 = 400,
    icon_size: u16 = 128,
    app_position: DmgPosition = .{ .x = 166, .y = 182 },
    applications_position: DmgPosition = .{ .x = 486, .y = 182 },
    applications_link: bool = true,
    items: []const DmgItemMetadata = &.{},
};

pub const UpdateMetadata = struct {
    feed_url: ?[]const u8 = null,
    public_key: ?[]const u8 = null,
    check_on_start: bool = false,

    pub fn enabled(self: UpdateMetadata) bool {
        return self.feed_url != null;
    }
};

pub const FrontendDevMetadata = struct {
    url: []const u8,
    command: []const []const u8 = &.{},
    ready_path: []const u8 = "/",
    timeout_ms: u32 = 30_000,
};

pub const FrontendMetadata = struct {
    dist: []const u8 = "dist",
    entry: []const u8 = "index.html",
    spa_fallback: bool = true,
    dev: ?FrontendDevMetadata = null,
};

pub const ExternalLinkMetadata = struct {
    action: []const u8 = "deny",
    allowed_urls: []const []const u8 = &.{},
};

pub const NavigationMetadata = struct {
    allowed_origins: []const []const u8 = &.{},
    external_links: ExternalLinkMetadata = .{},
};

pub const SecurityMetadata = struct {
    navigation: NavigationMetadata = .{},
};

const RawManifest = raw_manifest.RawManifest;
const RawBridge = raw_manifest.RawBridge;
const RawBridgeCommand = raw_manifest.RawBridgeCommand;
const RawFrontend = raw_manifest.RawFrontend;
const RawFrontendDev = raw_manifest.RawFrontendDev;
const RawSecurity = raw_manifest.RawSecurity;
const RawNavigation = raw_manifest.RawNavigation;
const RawExternalLinks = raw_manifest.RawExternalLinks;
const RawWindow = raw_manifest.RawWindow;
const RawShell = raw_manifest.RawShell;
const RawShellWindow = raw_manifest.RawShellWindow;
const RawShellView = raw_manifest.RawShellView;
const RawCommand = raw_manifest.RawCommand;
const RawMenu = raw_manifest.RawMenu;
const RawMenuItem = raw_manifest.RawMenuItem;
const RawShortcut = raw_manifest.RawShortcut;
const RawFileAssociation = raw_manifest.RawFileAssociation;
const RawUrlScheme = raw_manifest.RawUrlScheme;
const RawUpdates = raw_manifest.RawUpdates;
const RawDmg = raw_manifest.RawDmg;
const RawDmgItem = raw_manifest.RawDmgItem;

pub const json_name = "app.json";
pub const zon_name = "app.zon";

/// Resolve the conventional manifest in the current app directory. JSON is
/// the authoring default; app.zon remains a fully supported fallback. When a
/// project temporarily contains both (for example while migrating), app.json
/// is authoritative so every CLI boundary makes the same choice.
pub fn defaultPath(io: std.Io) ?[]const u8 {
    if (pathExists(io, json_name)) return json_name;
    if (pathExists(io, zon_name)) return zon_name;
    return null;
}

fn pathExists(io: std.Io, path: []const u8) bool {
    const stat = std.Io.Dir.cwd().statFile(io, path, .{}) catch return false;
    return stat.kind == .file;
}

pub fn isJsonPath(path: []const u8) bool {
    return json_to_zon.isJsonPath(path);
}

pub fn validateFile(allocator: std.mem.Allocator, io: std.Io, path: []const u8) !ValidationResult {
    const source = try readFile(allocator, io, path);
    defer allocator.free(source);

    const metadata = parseTextForPath(allocator, path, source) catch |err| return .{
        .ok = false,
        .message = parseFailureMessage(allocator, path, source, err),
    };
    defer metadata.deinit(allocator);

    if (metadata.description) |description| {
        app_manifest.validateDescription(description) catch return .{
            .ok = false,
            .message = "app manifest description is invalid - it must be one non-empty line of at most 256 bytes with no control characters (it becomes the About panel credits line)",
        };
    }
    if (metadata.theme) |theme_name| {
        if (!isKnownThemePack(theme_name)) return .{
            .ok = false,
            .message = "app manifest theme is invalid - expected one of: house, geist",
        };
    }
    if (metadata.theme_accent) |accent| {
        if (!isHexColor(accent)) return .{
            .ok = false,
            .message = "app manifest theme_accent is invalid - expected a #rrggbb hex color (e.g. \"#df2670\")",
        };
    }
    validateIconPaths(metadata.icons) catch return .{ .ok = false, .message = "app manifest icons are invalid" };
    if (try checkIconSources(allocator, io, std.fs.path.dirname(path) orelse ".", metadata.icons)) |icon_message| {
        return .{ .ok = false, .message = icon_message };
    }
    const permissions = parsePermissions(allocator, metadata.permissions) catch return .{ .ok = false, .message = "app manifest permissions are invalid" };
    defer allocator.free(permissions);
    const capabilities = parseCapabilities(allocator, metadata.capabilities) catch return .{ .ok = false, .message = "app manifest capabilities are invalid" };
    defer allocator.free(capabilities);
    const persist = convertPersist(metadata.persist);
    app_manifest.validateImages(.{ .max_image_pixel_bytes = metadata.images.max_image_pixel_bytes }) catch return .{
        .ok = false,
        .message = "app manifest images.max_image_pixel_bytes must be between 1048576 (the default) and 8388608 bytes; allocation is lazy once per used slot, up to 16 slots",
    };
    if (!validServicePackages(metadata.service_packages)) return .{
        .ok = false,
        .message = "app manifest service_packages must use safe npm names, exact X.Y.Z versions, unique names, and lowercase SHA-256 content hashes",
    };
    if (!validServiceCarrier(metadata.service_carrier)) return .{
        .ok = false,
        .message = "app manifest service_carrier must be \"auto\", \"in_process\", or \"child\"",
    };
    if (metadata.service_pool_size) |pool_size| {
        if (pool_size < 1 or pool_size > 16) return .{
            .ok = false,
            .message = "app manifest service_pool_size must be between 1 and 16",
        };
    }
    const bridge_commands = parseBridgeCommands(allocator, metadata.bridge_commands) catch return .{ .ok = false, .message = "app manifest bridge commands are invalid" };
    defer {
        for (bridge_commands) |command| allocator.free(command.permissions);
        allocator.free(bridge_commands);
    }
    const frontend = if (metadata.frontend) |frontend_value| convertFrontend(frontend_value) else null;
    const security = convertSecurity(metadata.security) catch return .{ .ok = false, .message = "app manifest security policy is invalid" };
    const windows = convertWindows(allocator, metadata.windows) catch return .{ .ok = false, .message = "app manifest windows are invalid" };
    defer allocator.free(windows);
    const shell = parseShell(allocator, metadata.shell) catch return .{ .ok = false, .message = "app manifest shell is invalid" };
    defer deinitParsedShell(allocator, shell);
    const commands = parseCommands(allocator, metadata.commands) catch return .{ .ok = false, .message = "app manifest commands are invalid" };
    defer allocator.free(commands);
    const menus = parseMenus(allocator, metadata.menus) catch return .{ .ok = false, .message = "app manifest menus are invalid" };
    defer deinitParsedMenus(allocator, menus);
    const shortcuts = parseShortcuts(allocator, metadata.shortcuts) catch return .{ .ok = false, .message = "app manifest shortcuts are invalid" };
    defer allocator.free(shortcuts);
    const file_associations = parseFileAssociations(allocator, metadata.file_associations) catch return .{ .ok = false, .message = "app manifest file associations are invalid" };
    defer allocator.free(file_associations);
    const url_schemes = parseUrlSchemes(allocator, metadata.url_schemes) catch return .{ .ok = false, .message = "app manifest URL schemes are invalid" };
    defer allocator.free(url_schemes);
    const updates = convertUpdates(metadata.updates) catch return .{ .ok = false, .message = "app manifest updates are invalid - updates require an HTTPS feed_url and a base64 Ed25519 public_key; check_on_start requires both" };
    // General manifest validation owns only values the app explicitly
    // declared. Archive-time fallbacks (notably display_name as the volume
    // name) are validated when a macOS archive is actually requested, so an
    // otherwise valid display name does not make every `native check` fail.
    validateDmgSettings(metadata.dmg) catch return .{ .ok = false, .message = "app manifest dmg settings are invalid - check the volume/bundle names, window/icon geometry, and positions; an explicit items list needs exactly one app and unique safe destination names" };
    if (try checkDmgSources(allocator, io, std.fs.path.dirname(path) orelse ".", metadata.dmg)) |dmg_message| {
        return .{ .ok = false, .message = dmg_message };
    }
    const manifest_web_engine = parseWebEngine(metadata.web_engine) catch return .{ .ok = false, .message = "app manifest web engine is invalid" };
    if (metadata.updates.enabled() and manifest_web_engine == .chromium) return .{
        .ok = false,
        .message = "native updates currently require web_engine = \"system\" on macOS; the Chromium host does not yet provide the updater UI/install lifecycle",
    };
    const manifest_webview_layer = parseWebViewLayer(metadata.webview_layer) catch return .{ .ok = false, .message = "app manifest webview_layer is invalid - expected \"auto\", \"include\", or \"exclude\"" };
    if (!std.mem.eql(u8, metadata.core_compiler, "external")) {
        if (std.mem.eql(u8, metadata.core_compiler, "transpiler")) {
            return .{ .ok = false, .message = "app manifest core_compiler = \"transpiler\" names the removed TS-to-Zig transpiled lane (v0.7.0 removed it) - TypeScript cores compile through the external core compiler now; delete the setting (or spell it \"external\")" };
        }
        return .{ .ok = false, .message = "app manifest core_compiler is invalid - expected \"external\" (the default and only lane)" };
    }
    const platform_settings = parsePlatformSettings(allocator, metadata.platforms) catch return .{ .ok = false, .message = "app manifest platforms are invalid" };
    defer allocator.free(platform_settings);

    const manifest: app_manifest.Manifest = .{
        .identity = .{ .id = metadata.id, .name = metadata.name, .display_name = metadata.display_name, .description = metadata.description },
        .version = parseVersion(metadata.version) catch return .{ .ok = false, .message = "app manifest version is invalid" },
        .permissions = permissions,
        .capabilities = capabilities,
        .dock_visible = metadata.dock_visible,
        .persist = persist,
        .images = .{ .max_image_pixel_bytes = metadata.images.max_image_pixel_bytes },
        .bridge = .{ .commands = bridge_commands },
        .frontend = frontend,
        .security = security,
        .platforms = platform_settings,
        .windows = windows,
        .shell = shell,
        .commands = commands,
        .menus = menus,
        .shortcuts = shortcuts,
        .file_associations = file_associations,
        .url_schemes = url_schemes,
        .updates = updates,
        .cef = .{ .dir = metadata.cef.dir, .auto_install = metadata.cef.auto_install },
        .webview_layer = manifest_webview_layer,
        .package = .{ .web_engine = manifest_web_engine },
    };
    app_manifest.validateManifest(manifest) catch |err| return .{
        .ok = false,
        .message = switch (err) {
            error.MissingTrayCapability => "app manifest dock_visible = false requires the \"tray\" capability: an accessory app has no Dock/app-switcher route back to hidden windows - add \"tray\" to capabilities and install a status item, or keep dock_visible = true (the default)",
            error.WebViewLayerConflict => web_layer_conflict_message,
            else => "manifest fields failed semantic validation",
        },
    };
    return .{ .ok = true, .message = if (isJsonPath(path)) "app.json is valid" else "app.zon is valid" };
}

/// Re-parse a failed manifest with std.zon diagnostics enabled so the
/// message names the line and column instead of a bare "could not be
/// parsed". Returns null when no diagnostic could be produced; the
/// (allocated) message intentionally lives until process exit.
fn zonParseFailureMessage(allocator: std.mem.Allocator, source: []const u8) ?[]const u8 {
    var arena = std.heap.ArenaAllocator.init(allocator);
    defer arena.deinit();
    const scratch = arena.allocator();
    const source_z = scratch.dupeZ(u8, source) catch return null;
    var diag: std.zon.parse.Diagnostics = .{};
    defer diag.deinit(scratch);
    @setEvalBranchQuota(4000);
    if (std.zon.parse.fromSliceAlloc(RawManifest, scratch, source_z, &diag, .{})) |_| {
        return null;
    } else |_| {
        const rendered = std.fmt.allocPrint(scratch, "{f}", .{&diag}) catch return null;
        const first_line_end = std.mem.indexOfScalar(u8, rendered, '\n') orelse rendered.len;
        const first_line = std.mem.trim(u8, rendered[0..first_line_end], " \n");
        if (first_line.len == 0) return null;
        return std.fmt.allocPrint(allocator, "app.zon could not be parsed - {s}", .{first_line}) catch null;
    }
}

fn parseFailureMessage(allocator: std.mem.Allocator, path: []const u8, source: []const u8, err: anyerror) []const u8 {
    if (!isJsonPath(path)) return zonParseFailureMessage(allocator, source) orelse "app.zon metadata could not be parsed";
    if (err == error.NullNotAllowed) return "app.json cannot contain null values - omit optional fields instead";
    return std.fmt.allocPrint(allocator, "{s} could not be parsed as a Native SDK manifest ({s})", .{ std.fs.path.basename(path), @errorName(err) }) catch "app.json metadata could not be parsed";
}

pub fn readMetadata(allocator: std.mem.Allocator, io: std.Io, path: []const u8) !Metadata {
    const source = try readFile(allocator, io, path);
    defer allocator.free(source);
    return parseTextForPath(allocator, path, source);
}

/// Parse legacy ZON text. Kept as the focused test/helper API; file-backed
/// callers use parseTextForPath so app.json and app.zon share one conversion
/// and semantic-validation pipeline.
pub fn parseText(allocator: std.mem.Allocator, source: []const u8) !Metadata {
    var arena = std.heap.ArenaAllocator.init(allocator);
    defer arena.deinit();
    const scratch = arena.allocator();
    const source_z = try scratch.dupeZ(u8, source);
    @setEvalBranchQuota(4000);
    const raw = try std.zon.parse.fromSliceAlloc(RawManifest, scratch, source_z, null, .{});
    return metadataFromRaw(allocator, raw);
}

pub fn parseJsonText(allocator: std.mem.Allocator, source: []const u8) !Metadata {
    try json_to_zon.validateSource(allocator, source);
    var arena = std.heap.ArenaAllocator.init(allocator);
    defer arena.deinit();
    const raw = try std.json.parseFromSliceLeaky(RawManifest, arena.allocator(), source, .{ .ignore_unknown_fields = false });
    return metadataFromRaw(allocator, raw);
}

pub fn parseTextForPath(allocator: std.mem.Allocator, path: []const u8, source: []const u8) !Metadata {
    return if (isJsonPath(path)) parseJsonText(allocator, source) else parseText(allocator, source);
}

fn metadataFromRaw(allocator: std.mem.Allocator, raw: RawManifest) !Metadata {
    return .{
        .id = try allocator.dupe(u8, raw.id),
        .name = try allocator.dupe(u8, raw.name),
        .display_name = if (raw.display_name) |value| try allocator.dupe(u8, value) else null,
        .description = if (raw.description) |value| try allocator.dupe(u8, value) else null,
        .theme = if (raw.theme) |value| try allocator.dupe(u8, value) else null,
        .theme_accent = if (raw.theme_accent) |value| try allocator.dupe(u8, value) else null,
        .version = try allocator.dupe(u8, raw.version),
        .icons = try duplicateStringList(allocator, raw.icons),
        .platforms = try duplicateStringList(allocator, raw.platforms),
        .permissions = try duplicateStringList(allocator, raw.permissions),
        .capabilities = try duplicateStringList(allocator, raw.capabilities),
        .dock_visible = raw.dock_visible,
        .persist = try duplicateRawPersist(allocator, raw.persist),
        .images = .{ .max_image_pixel_bytes = raw.images.max_image_pixel_bytes },
        .service_packages = try duplicateRawServicePackages(allocator, raw.service_packages),
        .service_carrier = try allocator.dupe(u8, raw.service_carrier),
        .service_pool_size = if (raw.service_pool_size == 0) null else raw.service_pool_size,
        .bridge_commands = try convertRawBridgeCommands(allocator, raw.bridge.commands),
        .web_engine = try allocator.dupe(u8, raw.web_engine),
        .webview_layer = try allocator.dupe(u8, raw.webview_layer),
        .core_compiler = try allocator.dupe(u8, raw.core_compiler),
        .cef = .{
            .dir = try allocator.dupe(u8, raw.cef.dir),
            .auto_install = raw.cef.auto_install,
        },
        .frontend = try convertRawFrontend(allocator, raw.frontend),
        .security = try convertRawSecurity(allocator, raw.security),
        .windows = try convertRawWindows(allocator, raw.windows),
        .shell = try convertRawShell(allocator, raw.shell),
        .commands = try convertRawCommands(allocator, raw.commands),
        .menus = try convertRawMenus(allocator, raw.menus),
        .shortcuts = try convertRawShortcuts(allocator, raw.shortcuts),
        .file_associations = try convertRawFileAssociations(allocator, raw.file_associations),
        .url_schemes = try convertRawUrlSchemes(allocator, raw.url_schemes),
        .updates = try duplicateRawUpdates(allocator, raw.updates),
        .dmg = try convertRawDmg(allocator, raw.dmg),
    };
}

fn duplicateRawUpdates(allocator: std.mem.Allocator, raw: RawUpdates) !UpdateMetadata {
    return .{
        .feed_url = try duplicateOptionalString(allocator, raw.feed_url),
        .public_key = try duplicateOptionalString(allocator, raw.public_key),
        .check_on_start = raw.check_on_start,
    };
}

fn convertUpdates(updates: UpdateMetadata) !app_manifest.UpdateConfig {
    if (updates.feed_url == null and updates.public_key == null and !updates.check_on_start) return .{};
    const feed_url = updates.feed_url orelse return error.InvalidUpdates;
    const public_key = updates.public_key orelse return error.InvalidUpdates;
    if (!std.mem.startsWith(u8, feed_url, "https://")) return error.InvalidUpdates;
    if (public_key.len == 0) return error.InvalidUpdates;
    return .{ .feed_url = feed_url, .public_key = public_key, .check_on_start = updates.check_on_start };
}

fn duplicateRawServicePackages(allocator: std.mem.Allocator, packages: []const raw_manifest.RawServicePackage) ![]const ServicePackageMetadata {
    if (packages.len == 0) return &.{};
    const out = try allocator.alloc(ServicePackageMetadata, packages.len);
    for (packages, 0..) |package_entry, index| out[index] = .{
        .name = try allocator.dupe(u8, package_entry.name),
        .version = try allocator.dupe(u8, package_entry.version),
        .content_hash = try allocator.dupe(u8, package_entry.content_hash),
    };
    return out;
}

fn validServicePackages(packages: []const ServicePackageMetadata) bool {
    for (packages, 0..) |package_entry, index| {
        if (!validNpmPackageName(package_entry.name) or !exactPackageVersion(package_entry.version) or package_entry.content_hash.len != 64) return false;
        for (package_entry.content_hash) |byte| if (!(std.ascii.isDigit(byte) or (byte >= 'a' and byte <= 'f'))) return false;
        for (packages[0..index]) |earlier| if (std.mem.eql(u8, earlier.name, package_entry.name)) return false;
    }
    return true;
}

fn validServiceCarrier(value: []const u8) bool {
    return std.mem.eql(u8, value, "auto") or std.mem.eql(u8, value, "in_process") or std.mem.eql(u8, value, "child");
}

fn validNpmPackageName(name: []const u8) bool {
    if (name.len == 0) return false;
    const plain = struct {
        fn valid(value: []const u8) bool {
            if (value.len == 0) return false;
            for (value) |byte| if (!(std.ascii.isAlphanumeric(byte) or byte == '.' or byte == '_' or byte == '-')) return false;
            return true;
        }
    }.valid;
    if (name[0] != '@') return plain(name);
    const slash = std.mem.indexOfScalar(u8, name, '/') orelse return false;
    return slash > 1 and slash + 1 < name.len and
        std.mem.indexOfScalar(u8, name[slash + 1 ..], '/') == null and
        plain(name[1..slash]) and plain(name[slash + 1 ..]);
}

fn exactPackageVersion(version: []const u8) bool {
    var parts = std.mem.splitScalar(u8, version, '.');
    var count: usize = 0;
    while (parts.next()) |part| {
        if (part.len == 0) return false;
        for (part) |byte| if (!std.ascii.isDigit(byte)) return false;
        count += 1;
    }
    return count == 3;
}

fn convertRawDmg(allocator: std.mem.Allocator, dmg: RawDmg) !DmgMetadata {
    return .{
        .volume_name = try duplicateOptionalString(allocator, dmg.volume_name),
        .background = try duplicateOptionalString(allocator, dmg.background),
        .window_width = dmg.window_width,
        .window_height = dmg.window_height,
        .icon_size = dmg.icon_size,
        .app_position = .{ .x = dmg.app_position.x, .y = dmg.app_position.y },
        .applications_position = .{ .x = dmg.applications_position.x, .y = dmg.applications_position.y },
        .applications_link = dmg.applications_link,
        .items = try convertRawDmgItems(allocator, dmg.items),
    };
}

fn convertRawDmgItems(allocator: std.mem.Allocator, items: []const RawDmgItem) ![]const DmgItemMetadata {
    if (items.len == 0) return &.{};
    const converted = try allocator.alloc(DmgItemMetadata, items.len);
    errdefer allocator.free(converted);
    var initialized: usize = 0;
    errdefer for (converted[0..initialized]) |item| {
        if (item.path) |value| allocator.free(value);
        if (item.name) |value| allocator.free(value);
    };
    for (items, 0..) |item, index| {
        converted[index] = try convertRawDmgItem(allocator, item);
        initialized += 1;
    }
    return converted;
}

fn convertRawDmgItem(allocator: std.mem.Allocator, item: RawDmgItem) !DmgItemMetadata {
    const path = try duplicateOptionalString(allocator, item.path);
    errdefer if (path) |value| allocator.free(value);
    const name = try duplicateOptionalString(allocator, item.name);
    errdefer if (name) |value| allocator.free(value);
    return .{
        .kind = try parseDmgItemKind(item.kind),
        .path = path,
        .name = name,
        .position = .{ .x = item.position.x, .y = item.position.y },
    };
}

fn parseDmgItemKind(value: []const u8) !DmgItemKind {
    if (std.mem.eql(u8, value, "app")) return .app;
    if (std.mem.eql(u8, value, "applications")) return .applications;
    if (std.mem.eql(u8, value, "file")) return .file;
    if (std.mem.eql(u8, value, "link")) return .link;
    return error.InvalidDmgItemKind;
}

fn duplicateOptionalString(allocator: std.mem.Allocator, value: ?[]const u8) !?[]const u8 {
    return if (value) |payload| try allocator.dupe(u8, payload) else null;
}

fn duplicateStringList(allocator: std.mem.Allocator, values: []const []const u8) ![]const []const u8 {
    if (values.len == 0) return &.{};
    const out = try allocator.alloc([]const u8, values.len);
    for (values, 0..) |value, index| {
        out[index] = try allocator.dupe(u8, value);
    }
    return out;
}

fn convertRawBridgeCommands(allocator: std.mem.Allocator, commands: []const RawBridgeCommand) ![]const BridgeCommandMetadata {
    if (commands.len == 0) return &.{};
    const converted = try allocator.alloc(BridgeCommandMetadata, commands.len);
    for (commands, 0..) |command, index| {
        converted[index] = .{
            .name = try allocator.dupe(u8, command.name),
            .permissions = try duplicateStringList(allocator, command.permissions),
            .origins = try duplicateStringList(allocator, command.origins),
        };
    }
    return converted;
}

fn convertRawFrontend(allocator: std.mem.Allocator, frontend: ?RawFrontend) !?FrontendMetadata {
    const value = frontend orelse return null;
    return .{
        .dist = try allocator.dupe(u8, value.dist),
        .entry = try allocator.dupe(u8, value.entry),
        .spa_fallback = value.spa_fallback,
        .dev = if (value.dev) |dev| .{
            .url = try allocator.dupe(u8, dev.url),
            .command = try duplicateStringList(allocator, dev.command),
            .ready_path = try allocator.dupe(u8, dev.ready_path),
            .timeout_ms = dev.timeout_ms,
        } else null,
    };
}

fn duplicateRawPersist(allocator: std.mem.Allocator, raw: ?raw_manifest.RawPersist) !?PersistMetadata {
    const persist = raw orelse return null;
    const ok = try allocator.dupe(u8, persist.restore.ok);
    errdefer allocator.free(ok);
    const none = try allocator.dupe(u8, persist.restore.none);
    errdefer allocator.free(none);
    const err = try allocator.dupe(u8, persist.restore.err);
    return .{
        .version = persist.version,
        .debounce_ms = persist.debounce_ms,
        .restore = .{ .ok = ok, .none = none, .err = err },
    };
}

fn convertPersist(persist: ?PersistMetadata) ?app_manifest.PersistConfig {
    const value = persist orelse return null;
    return .{
        .version = value.version,
        .debounce_ms = value.debounce_ms,
        .restore = .{
            .ok = value.restore.ok,
            .none = value.restore.none,
            .err = value.restore.err,
        },
    };
}

fn convertRawSecurity(allocator: std.mem.Allocator, security: RawSecurity) !SecurityMetadata {
    const external_action = if (security.navigation.external_links.allowed_urls.len == 0 and
        std.mem.eql(u8, security.navigation.external_links.action, "deny"))
        "deny"
    else
        try allocator.dupe(u8, security.navigation.external_links.action);
    return .{
        .navigation = .{
            .allowed_origins = try duplicateStringList(allocator, security.navigation.allowed_origins),
            .external_links = .{
                .action = external_action,
                .allowed_urls = try duplicateStringList(allocator, security.navigation.external_links.allowed_urls),
            },
        },
    };
}

fn convertRawWindows(allocator: std.mem.Allocator, windows: []const RawWindow) ![]const WindowMetadata {
    if (windows.len == 0) return &.{};
    const converted = try allocator.alloc(WindowMetadata, windows.len);
    for (windows, 0..) |window, index| {
        converted[index] = .{
            .label = try allocator.dupe(u8, window.label),
            .title = if (window.title) |title| try allocator.dupe(u8, title) else null,
            .width = window.width,
            .height = window.height,
            .x = window.x,
            .y = window.y,
            .resizable = window.resizable,
            .restore_state = window.restore_state,
            .titlebar = try allocator.dupe(u8, window.titlebar),
            .transparent = window.transparent,
            .always_on_top = window.always_on_top,
            .click_through = window.click_through,
            .activate_on_show = window.activate_on_show,
            .initially_hidden = window.initially_hidden,
            .allows_fullscreen = window.allows_fullscreen,
            .min_width = window.min_width,
            .min_height = window.min_height,
            .close_policy = try allocator.dupe(u8, window.close_policy),
        };
    }
    return converted;
}

fn convertRawShell(allocator: std.mem.Allocator, shell: RawShell) !ShellMetadata {
    return .{
        .windows = try convertRawShellWindows(allocator, shell.windows),
        .chrome = try convertRawShellChrome(allocator, shell.chrome),
    };
}

fn convertRawShellChrome(allocator: std.mem.Allocator, chrome: raw_manifest.RawShellChrome) !ShellChromeMetadata {
    var converted: ShellChromeMetadata = .{};
    if (chrome.tabs.len > 0) {
        const tabs = try allocator.alloc(ShellTabMetadata, chrome.tabs.len);
        for (chrome.tabs, 0..) |tab, index| {
            tabs[index] = .{
                .id = try allocator.dupe(u8, tab.id),
                .label = try allocator.dupe(u8, tab.label),
                .icon = try allocator.dupe(u8, tab.icon),
            };
        }
        converted.tabs = tabs;
    }
    if (chrome.primary_action) |action| {
        converted.primary_action = .{
            .id = try allocator.dupe(u8, action.id),
            .label = try allocator.dupe(u8, action.label),
            .icon = try allocator.dupe(u8, action.icon),
        };
    }
    return converted;
}

fn convertRawShellWindows(allocator: std.mem.Allocator, windows: []const RawShellWindow) ![]const ShellWindowMetadata {
    if (windows.len == 0) return &.{};
    const converted = try allocator.alloc(ShellWindowMetadata, windows.len);
    for (windows, 0..) |window, index| {
        converted[index] = .{
            .label = try allocator.dupe(u8, window.label),
            .title = try duplicateOptionalString(allocator, window.title),
            .width = window.width,
            .height = window.height,
            .x = window.x,
            .y = window.y,
            .resizable = window.resizable,
            .restore_state = window.restore_state,
            .restore_policy = try allocator.dupe(u8, window.restore_policy),
            .titlebar = try allocator.dupe(u8, window.titlebar),
            .transparent = window.transparent,
            .always_on_top = window.always_on_top,
            .click_through = window.click_through,
            .activate_on_show = window.activate_on_show,
            .initially_hidden = window.initially_hidden,
            .allows_fullscreen = window.allows_fullscreen,
            .min_width = window.min_width,
            .min_height = window.min_height,
            .close_policy = try allocator.dupe(u8, window.close_policy),
            .views = try convertRawShellViews(allocator, window.views),
        };
    }
    return converted;
}

fn convertRawShellViews(allocator: std.mem.Allocator, views: []const RawShellView) ![]const ShellViewMetadata {
    if (views.len == 0) return &.{};
    const converted = try allocator.alloc(ShellViewMetadata, views.len);
    for (views, 0..) |view, index| {
        converted[index] = .{
            .label = try allocator.dupe(u8, view.label),
            .kind = try allocator.dupe(u8, view.kind),
            .parent = try duplicateOptionalString(allocator, view.parent),
            .edge = try duplicateOptionalString(allocator, view.edge),
            .axis = try duplicateOptionalString(allocator, view.axis),
            .x = view.x,
            .y = view.y,
            .width = view.width,
            .height = view.height,
            .min_width = view.min_width,
            .min_height = view.min_height,
            .max_width = view.max_width,
            .max_height = view.max_height,
            .fill = view.fill,
            .layer = view.layer,
            .visible = view.visible,
            .enabled = view.enabled,
            .role = try duplicateOptionalString(allocator, view.role),
            .accessibility_label = try duplicateOptionalString(allocator, view.accessibility_label),
            .url = try duplicateOptionalString(allocator, view.url),
            .text = try duplicateOptionalString(allocator, view.text),
            .command = try duplicateOptionalString(allocator, view.command),
            .gpu_backend = try duplicateOptionalString(allocator, view.gpu_backend),
            .gpu_pixel_format = try duplicateOptionalString(allocator, view.gpu_pixel_format),
            .gpu_present_mode = try duplicateOptionalString(allocator, view.gpu_present_mode),
            .gpu_alpha_mode = try duplicateOptionalString(allocator, view.gpu_alpha_mode),
            .gpu_color_space = try duplicateOptionalString(allocator, view.gpu_color_space),
            .gpu_vsync = view.gpu_vsync,
        };
    }
    return converted;
}

fn convertRawShortcuts(allocator: std.mem.Allocator, shortcuts: []const RawShortcut) ![]const ShortcutMetadata {
    if (shortcuts.len == 0) return &.{};
    const converted = try allocator.alloc(ShortcutMetadata, shortcuts.len);
    for (shortcuts, 0..) |shortcut, index| {
        converted[index] = .{
            .id = try allocator.dupe(u8, shortcut.id),
            .key = try allocator.dupe(u8, shortcut.key),
            .modifiers = try duplicateStringList(allocator, shortcut.modifiers),
        };
    }
    return converted;
}

fn convertRawCommands(allocator: std.mem.Allocator, commands: []const RawCommand) ![]const CommandMetadata {
    if (commands.len == 0) return &.{};
    const converted = try allocator.alloc(CommandMetadata, commands.len);
    for (commands, 0..) |command, index| {
        converted[index] = .{
            .id = try allocator.dupe(u8, command.id),
            .title = try allocator.dupe(u8, command.title),
            .enabled = command.enabled,
            .checked = command.checked,
        };
    }
    return converted;
}

fn convertRawMenus(allocator: std.mem.Allocator, menus: []const RawMenu) ![]const MenuMetadata {
    if (menus.len == 0) return &.{};
    const converted = try allocator.alloc(MenuMetadata, menus.len);
    for (menus, 0..) |menu, index| {
        converted[index] = .{
            .title = try allocator.dupe(u8, menu.title),
            .items = try convertRawMenuItems(allocator, menu.items),
        };
    }
    return converted;
}

fn convertRawMenuItems(allocator: std.mem.Allocator, items: []const RawMenuItem) ![]const MenuItemMetadata {
    if (items.len == 0) return &.{};
    const converted = try allocator.alloc(MenuItemMetadata, items.len);
    for (items, 0..) |item, index| {
        converted[index] = .{
            .label = try allocator.dupe(u8, item.label),
            .command = try allocator.dupe(u8, item.command),
            .key = try allocator.dupe(u8, item.key),
            .modifiers = try duplicateStringList(allocator, item.modifiers),
            .separator = item.separator,
            .enabled = item.enabled,
            .checked = item.checked,
        };
    }
    return converted;
}

fn convertRawFileAssociations(allocator: std.mem.Allocator, associations: []const RawFileAssociation) ![]const FileAssociationMetadata {
    if (associations.len == 0) return &.{};
    const converted = try allocator.alloc(FileAssociationMetadata, associations.len);
    for (associations, 0..) |association, index| {
        converted[index] = .{
            .name = try allocator.dupe(u8, association.name),
            .role = try allocator.dupe(u8, association.role),
            .extensions = try duplicateStringList(allocator, association.extensions),
            .mime_types = try duplicateStringList(allocator, association.mime_types),
            .icon = try duplicateOptionalString(allocator, association.icon),
        };
    }
    return converted;
}

fn convertRawUrlSchemes(allocator: std.mem.Allocator, schemes: []const RawUrlScheme) ![]const UrlSchemeMetadata {
    if (schemes.len == 0) return &.{};
    const converted = try allocator.alloc(UrlSchemeMetadata, schemes.len);
    for (schemes, 0..) |scheme, index| {
        converted[index] = .{
            .scheme = try allocator.dupe(u8, scheme.scheme),
            .role = try allocator.dupe(u8, scheme.role),
        };
    }
    return converted;
}

pub fn parseVersion(value: []const u8) !app_manifest.Version {
    var parts = std.mem.splitScalar(u8, value, '.');
    const major = try parseVersionNumber(parts.next() orelse return error.InvalidVersion);
    const minor = try parseVersionNumber(parts.next() orelse return error.InvalidVersion);
    const patch_text = parts.next() orelse return error.InvalidVersion;
    if (parts.next() != null) return error.InvalidVersion;
    return .{
        .major = major,
        .minor = minor,
        .patch = try parseVersionNumber(patch_text),
    };
}

pub fn printDiagnostic(result: ValidationResult) void {
    const severity: diagnostics.Severity = if (result.ok) .info else .@"error";
    var buffer: [1024]u8 = undefined;
    var writer = std.Io.Writer.fixed(&buffer);
    diagnostics.formatShort(.{ .severity = severity, .code = diagnostics.code("manifest", if (result.ok) "valid" else "invalid"), .message = result.message }, &writer) catch return;
    std.debug.print("{s}\n", .{writer.buffered()});
}

fn readFile(allocator: std.mem.Allocator, io: std.Io, path: []const u8) ![]u8 {
    var file = try std.Io.Dir.cwd().openFile(io, path, .{});
    defer file.close(io);
    var read_buffer: [4096]u8 = undefined;
    var reader = file.reader(io, &read_buffer);
    return reader.interface.allocRemaining(allocator, .limited(1024 * 1024));
}

fn convertFrontend(frontend: FrontendMetadata) app_manifest.FrontendConfig {
    return .{
        .dist = frontend.dist,
        .entry = frontend.entry,
        .spa_fallback = frontend.spa_fallback,
        .dev = if (frontend.dev) |dev| .{
            .url = dev.url,
            .command = dev.command,
            .ready_path = dev.ready_path,
            .timeout_ms = dev.timeout_ms,
        } else null,
    };
}

fn convertSecurity(security: SecurityMetadata) !app_manifest.SecurityConfig {
    return .{
        .navigation = .{
            .allowed_origins = if (security.navigation.allowed_origins.len > 0) security.navigation.allowed_origins else &.{ "zero://app", "zero://inline" },
            .external_links = .{
                .action = parseExternalLinkAction(security.navigation.external_links.action) catch return error.InvalidSecurity,
                .allowed_urls = security.navigation.external_links.allowed_urls,
            },
        },
    };
}

fn convertWindows(allocator: std.mem.Allocator, windows: []const WindowMetadata) ![]const app_manifest.Window {
    if (windows.len == 0) return &.{};
    const converted = try allocator.alloc(app_manifest.Window, windows.len);
    errdefer allocator.free(converted);
    for (windows, 0..) |window, index| {
        converted[index] = .{
            .label = window.label,
            .title = window.title,
            .width = window.width,
            .height = window.height,
            .x = window.x,
            .y = window.y,
            .resizable = window.resizable,
            .restore_state = window.restore_state,
            .titlebar = try parseTitlebarStyle(window.titlebar),
            .transparent = window.transparent,
            .always_on_top = window.always_on_top,
            .click_through = window.click_through,
            .activate_on_show = window.activate_on_show,
            .initially_hidden = window.initially_hidden,
            .allows_fullscreen = window.allows_fullscreen,
            .min_width = try parseWindowMinSize(window.min_width),
            .min_height = try parseWindowMinSize(window.min_height),
            .close_policy = try parseClosePolicy(window.close_policy),
        };
    }
    return converted;
}

fn parseShell(allocator: std.mem.Allocator, shell: ShellMetadata) !app_manifest.ShellConfig {
    const chrome = try parseShellChrome(allocator, shell.chrome);
    errdefer if (chrome.tabs.len > 0) allocator.free(chrome.tabs);
    if (shell.windows.len == 0) return .{ .chrome = chrome };
    const windows = try allocator.alloc(app_manifest.ShellWindow, shell.windows.len);
    errdefer allocator.free(windows);
    var initialized: usize = 0;
    errdefer {
        for (windows[0..initialized]) |window| {
            if (window.views.len > 0) allocator.free(window.views);
        }
    }
    for (shell.windows, 0..) |window, index| {
        // Parse the fallible scalar fields BEFORE allocating the views
        // slice, so a bad policy/titlebar string cannot leak it.
        const restore_policy = try parseRestorePolicy(window.restore_policy);
        const titlebar = try parseTitlebarStyle(window.titlebar);
        const min_width = try parseWindowMinSize(window.min_width);
        const min_height = try parseWindowMinSize(window.min_height);
        const close_policy = try parseClosePolicy(window.close_policy);
        const views = try parseShellViews(allocator, window.views);
        windows[index] = .{
            .label = window.label,
            .title = window.title,
            .width = window.width,
            .height = window.height,
            .x = window.x,
            .y = window.y,
            .resizable = window.resizable,
            .restore_state = window.restore_state,
            .restore_policy = restore_policy,
            .titlebar = titlebar,
            .transparent = window.transparent,
            .always_on_top = window.always_on_top,
            .click_through = window.click_through,
            .activate_on_show = window.activate_on_show,
            .initially_hidden = window.initially_hidden,
            .allows_fullscreen = window.allows_fullscreen,
            .min_width = min_width,
            .min_height = min_height,
            .close_policy = close_policy,
            .views = views,
        };
        initialized += 1;
    }
    return .{ .windows = windows, .chrome = chrome };
}

/// Declared platform chrome from app.zon metadata: the strings pass
/// through (Metadata owns them, exactly like window/view labels); only
/// the tabs slice is parse-owned. Structural rules live in
/// `app_manifest.validateShellChrome`, which `parseShell`'s caller runs
/// over the whole shell.
fn parseShellChrome(allocator: std.mem.Allocator, chrome: ShellChromeMetadata) !app_manifest.ShellChrome {
    var parsed: app_manifest.ShellChrome = .{};
    if (chrome.tabs.len > 0) {
        const tabs = try allocator.alloc(app_manifest.ShellTab, chrome.tabs.len);
        for (chrome.tabs, 0..) |tab, index| {
            tabs[index] = .{ .id = tab.id, .label = tab.label, .icon = tab.icon };
        }
        parsed.tabs = tabs;
    }
    if (chrome.primary_action) |action| {
        parsed.primary_action = .{ .id = action.id, .label = action.label, .icon = action.icon };
    }
    return parsed;
}

fn parseShellViews(allocator: std.mem.Allocator, values: []const ShellViewMetadata) ![]const app_manifest.ShellView {
    if (values.len == 0) return &.{};
    const views = try allocator.alloc(app_manifest.ShellView, values.len);
    errdefer allocator.free(views);
    for (values, 0..) |view, index| {
        views[index] = .{
            .label = view.label,
            .kind = try parseViewKind(view.kind),
            .parent = view.parent,
            .edge = if (view.edge) |edge| try parseShellEdge(edge) else null,
            .axis = if (view.axis) |axis| try parseShellAxis(axis) else null,
            .x = view.x,
            .y = view.y,
            .width = view.width,
            .height = view.height,
            .min_width = view.min_width,
            .min_height = view.min_height,
            .max_width = view.max_width,
            .max_height = view.max_height,
            .fill = view.fill,
            .layer = view.layer,
            .visible = view.visible,
            .enabled = view.enabled,
            .role = view.role,
            .accessibility_label = view.accessibility_label,
            .url = view.url,
            .text = view.text,
            .command = view.command,
            .gpu_backend = if (view.gpu_backend) |value| try parseGpuSurfaceBackend(value) else null,
            .gpu_pixel_format = if (view.gpu_pixel_format) |value| try parseGpuSurfacePixelFormat(value) else null,
            .gpu_present_mode = if (view.gpu_present_mode) |value| try parseGpuSurfacePresentMode(value) else null,
            .gpu_alpha_mode = if (view.gpu_alpha_mode) |value| try parseGpuSurfaceAlphaMode(value) else null,
            .gpu_color_space = if (view.gpu_color_space) |value| try parseGpuSurfaceColorSpace(value) else null,
            .gpu_vsync = view.gpu_vsync,
        };
    }
    return views;
}

fn deinitParsedShell(allocator: std.mem.Allocator, shell: app_manifest.ShellConfig) void {
    for (shell.windows) |window| {
        if (window.views.len > 0) allocator.free(window.views);
    }
    if (shell.windows.len > 0) allocator.free(shell.windows);
    if (shell.chrome.tabs.len > 0) allocator.free(shell.chrome.tabs);
}

fn deinitParsedMenus(allocator: std.mem.Allocator, menus: []const app_manifest.Menu) void {
    for (menus) |menu| {
        if (menu.items.len > 0) allocator.free(menu.items);
    }
    if (menus.len > 0) allocator.free(menus);
}

/// The built-in theme pack names, kept in step with the canvas
/// `ThemePack` enum (tooling deliberately does not link the canvas
/// module; the runner re-validates at comptime, so a drift here shows
/// up as a build error in the app, never a silently shipped typo).
fn isKnownThemePack(name: []const u8) bool {
    const known = [_][]const u8{ "house", "geist" };
    for (known) |candidate| {
        if (std.mem.eql(u8, name, candidate)) return true;
    }
    return false;
}

/// One #rrggbb hex color — the `theme_accent` shape the runner's
/// comptime parse accepts (kept in step the same way as the pack names:
/// a drift is a build error in the app, never a shipped typo).
fn isHexColor(value: []const u8) bool {
    if (value.len != 7 or value[0] != '#') return false;
    for (value[1..]) |byte| {
        _ = std.fmt.charToDigit(byte, 16) catch return false;
    }
    return true;
}

fn validateIconPaths(icons: []const []const u8) !void {
    for (icons, 0..) |icon, index| {
        try validateRelativePath(icon);
        for (icons[0..index]) |previous| {
            if (std.mem.eql(u8, previous, icon)) return error.DuplicateIcon;
        }
    }
}

/// The app-icon teaching checks `native validate` and `native check`
/// share with packaging (same messages, no packaging): every `.icons`
/// entry must be a generatable source (.png/.svg) or a prebuilt
/// container (.icns/.ico), and a source file that exists must decode to
/// a square image. A missing file is packaging's problem (it warns and
/// falls back); an undersized source prints the upscaling warning
/// without failing validation. Returns the error message or null when
/// the icons pass. The returned message is allocated and intentionally
/// lives until process exit (same policy as `zonParseFailureMessage`).
fn checkIconSources(allocator: std.mem.Allocator, io: std.Io, manifest_dir: []const u8, icons: []const []const u8) !?[]const u8 {
    for (icons) |icon_path| {
        const is_prebuilt = app_icon_tool.pathHasExtension(icon_path, ".icns") or
            app_icon_tool.pathHasExtension(icon_path, ".ico");
        const kind = app_icon_tool.sourceKindForPath(icon_path) orelse {
            if (is_prebuilt) continue;
            var buffer: [512]u8 = undefined;
            return try allocator.dupe(u8, app_icon_tool.formatBadExtensionMessage(&buffer, icon_path));
        };

        const resolved = try std.fs.path.join(allocator, &.{ manifest_dir, icon_path });
        defer allocator.free(resolved);
        const bytes = readFile(allocator, io, resolved) catch continue;
        defer allocator.free(bytes);
        switch (try app_icon_tool.loadSource(allocator, bytes, kind)) {
            .ok => |loaded| {
                var source = loaded;
                defer source.deinit(allocator);
                if (kind == .png and source.width < app_icon_tool.min_recommended_source_size) {
                    var buffer: [512]u8 = undefined;
                    std.debug.print("{s}\n", .{app_icon_tool.formatSmallSourceMessage(&buffer, icon_path, source.width, source.height)});
                }
            },
            .issue => |issue| {
                var buffer: [512]u8 = undefined;
                const message = switch (issue) {
                    .not_square => |dims| app_icon_tool.formatNotSquareMessage(&buffer, icon_path, dims.width, dims.height),
                    .unsupported => app_icon_tool.formatUnsupportedMessage(&buffer, icon_path),
                };
                return try allocator.dupe(u8, message);
            },
        }
    }
    return null;
}

fn parseCapabilities(allocator: std.mem.Allocator, values: []const []const u8) ![]const app_manifest.Capability {
    var capabilities: std.ArrayList(app_manifest.Capability) = .empty;
    errdefer capabilities.deinit(allocator);
    for (values) |value| {
        try capabilities.append(allocator, parseCapability(value) catch return error.InvalidCapability);
    }
    return capabilities.toOwnedSlice(allocator);
}

fn parsePermissions(allocator: std.mem.Allocator, values: []const []const u8) ![]const app_manifest.Permission {
    var permissions: std.ArrayList(app_manifest.Permission) = .empty;
    errdefer permissions.deinit(allocator);
    for (values) |value| {
        try permissions.append(allocator, parsePermission(value));
    }
    return permissions.toOwnedSlice(allocator);
}

fn parsePermission(value: []const u8) app_manifest.Permission {
    if (std.mem.eql(u8, value, "network")) return .network;
    if (std.mem.eql(u8, value, "filesystem")) return .filesystem;
    if (std.mem.eql(u8, value, "camera")) return .camera;
    if (std.mem.eql(u8, value, "microphone")) return .microphone;
    if (std.mem.eql(u8, value, "system_audio")) return .system_audio;
    if (std.mem.eql(u8, value, "location")) return .location;
    if (std.mem.eql(u8, value, "notifications")) return .notifications;
    if (std.mem.eql(u8, value, "clipboard")) return .clipboard;
    if (std.mem.eql(u8, value, "window")) return .window;
    if (std.mem.eql(u8, value, "command")) return .command;
    if (std.mem.eql(u8, value, "view")) return .view;
    if (std.mem.eql(u8, value, "dialog")) return .dialog;
    if (std.mem.eql(u8, value, "credentials")) return .credentials;
    return .{ .custom = value };
}

fn parseCapability(value: []const u8) !app_manifest.Capability {
    if (std.mem.eql(u8, value, "native_module")) return .native_module;
    if (std.mem.eql(u8, value, "webview")) return .webview;
    if (std.mem.eql(u8, value, "js_bridge")) return .js_bridge;
    if (std.mem.eql(u8, value, "native_views")) return .native_views;
    if (std.mem.eql(u8, value, "gpu_surfaces")) return .gpu_surfaces;
    if (std.mem.eql(u8, value, "menus")) return .menus;
    if (std.mem.eql(u8, value, "shortcuts")) return .shortcuts;
    if (std.mem.eql(u8, value, "tray")) return .tray;
    if (std.mem.eql(u8, value, "filesystem")) return .filesystem;
    if (std.mem.eql(u8, value, "network")) return .network;
    if (std.mem.eql(u8, value, "notifications")) return .notifications;
    if (std.mem.eql(u8, value, "dialog")) return .dialog;
    if (std.mem.eql(u8, value, "clipboard")) return .clipboard;
    if (std.mem.eql(u8, value, "credentials")) return .credentials;
    if (std.mem.eql(u8, value, "persist")) return .persist;
    if (std.mem.eql(u8, value, "store")) return .store;
    if (std.mem.eql(u8, value, "sqlite")) return .sqlite;
    if (std.mem.eql(u8, value, "open_url")) return .open_url;
    if (std.mem.eql(u8, value, "reveal_path")) return .reveal_path;
    if (std.mem.eql(u8, value, "recent_documents")) return .recent_documents;
    if (std.mem.eql(u8, value, "file_drops")) return .file_drops;
    if (std.mem.eql(u8, value, "app_activation_events")) return .app_activation_events;
    if (std.mem.eql(u8, value, "file_associations")) return .file_associations;
    if (std.mem.eql(u8, value, "url_schemes")) return .url_schemes;
    return error.InvalidCapability;
}

fn parseBridgeCommands(allocator: std.mem.Allocator, values: []const BridgeCommandMetadata) ![]const app_manifest.BridgeCommand {
    var commands: std.ArrayList(app_manifest.BridgeCommand) = .empty;
    errdefer commands.deinit(allocator);
    for (values) |value| {
        try commands.append(allocator, .{
            .name = value.name,
            .permissions = try parsePermissions(allocator, value.permissions),
            .origins = value.origins,
        });
    }
    return commands.toOwnedSlice(allocator);
}

fn parseShortcuts(allocator: std.mem.Allocator, values: []const ShortcutMetadata) ![]const app_manifest.Shortcut {
    if (values.len == 0) return &.{};
    var shortcuts: std.ArrayList(app_manifest.Shortcut) = .empty;
    errdefer shortcuts.deinit(allocator);
    for (values) |value| {
        try shortcuts.append(allocator, .{
            .id = value.id,
            .key = value.key,
            .modifiers = try parseShortcutModifiers(value.modifiers),
        });
    }
    return shortcuts.toOwnedSlice(allocator);
}

fn parseCommands(allocator: std.mem.Allocator, values: []const CommandMetadata) ![]const app_manifest.Command {
    if (values.len == 0) return &.{};
    var commands: std.ArrayList(app_manifest.Command) = .empty;
    errdefer commands.deinit(allocator);
    for (values) |value| {
        try commands.append(allocator, .{
            .id = value.id,
            .title = value.title,
            .enabled = value.enabled,
            .checked = value.checked,
        });
    }
    return commands.toOwnedSlice(allocator);
}

fn parseMenus(allocator: std.mem.Allocator, values: []const MenuMetadata) ![]const app_manifest.Menu {
    if (values.len == 0) return &.{};
    var menus: std.ArrayList(app_manifest.Menu) = .empty;
    errdefer {
        for (menus.items) |menu| {
            if (menu.items.len > 0) allocator.free(menu.items);
        }
        menus.deinit(allocator);
    }
    for (values) |value| {
        try menus.append(allocator, .{
            .title = value.title,
            .items = try parseMenuItems(allocator, value.items),
        });
    }
    return menus.toOwnedSlice(allocator);
}

fn parseMenuItems(allocator: std.mem.Allocator, values: []const MenuItemMetadata) ![]const app_manifest.MenuItem {
    if (values.len == 0) return &.{};
    var items: std.ArrayList(app_manifest.MenuItem) = .empty;
    errdefer items.deinit(allocator);
    for (values) |value| {
        try items.append(allocator, .{
            .label = value.label,
            .command = value.command,
            .key = value.key,
            .modifiers = try parseShortcutModifiers(value.modifiers),
            .separator = value.separator,
            .enabled = value.enabled,
            .checked = value.checked,
        });
    }
    return items.toOwnedSlice(allocator);
}

fn parseFileAssociations(allocator: std.mem.Allocator, values: []const FileAssociationMetadata) ![]const app_manifest.FileAssociation {
    if (values.len == 0) return &.{};
    var associations: std.ArrayList(app_manifest.FileAssociation) = .empty;
    errdefer associations.deinit(allocator);
    for (values) |value| {
        try associations.append(allocator, .{
            .name = value.name,
            .role = try parseAssociationRole(value.role),
            .extensions = value.extensions,
            .mime_types = value.mime_types,
            .icon = value.icon,
        });
    }
    return associations.toOwnedSlice(allocator);
}

fn parseUrlSchemes(allocator: std.mem.Allocator, values: []const UrlSchemeMetadata) ![]const app_manifest.UrlScheme {
    if (values.len == 0) return &.{};
    var schemes: std.ArrayList(app_manifest.UrlScheme) = .empty;
    errdefer schemes.deinit(allocator);
    for (values) |value| {
        try schemes.append(allocator, .{
            .scheme = value.scheme,
            .role = try parseAssociationRole(value.role),
        });
    }
    return schemes.toOwnedSlice(allocator);
}

fn parseAssociationRole(value: []const u8) !app_manifest.AssociationRole {
    if (std.mem.eql(u8, value, "viewer")) return .viewer;
    if (std.mem.eql(u8, value, "editor")) return .editor;
    if (std.mem.eql(u8, value, "shell")) return .shell;
    if (std.mem.eql(u8, value, "none")) return .none;
    return error.InvalidAssociationRole;
}

fn parseShortcutModifiers(values: []const []const u8) !app_manifest.ShortcutModifiers {
    var modifiers: app_manifest.ShortcutModifiers = .{};
    for (values) |value| {
        if (std.mem.eql(u8, value, "primary")) {
            modifiers.primary = true;
        } else if (std.mem.eql(u8, value, "command")) {
            modifiers.command = true;
        } else if (std.mem.eql(u8, value, "control")) {
            modifiers.control = true;
        } else if (std.mem.eql(u8, value, "option") or std.mem.eql(u8, value, "alt")) {
            modifiers.option = true;
        } else if (std.mem.eql(u8, value, "shift")) {
            modifiers.shift = true;
        } else {
            return error.InvalidShortcut;
        }
    }
    return modifiers;
}

fn parsePlatformSettings(allocator: std.mem.Allocator, values: []const []const u8) ![]const app_manifest.PlatformSettings {
    if (values.len == 0) return &.{};
    var platforms: std.ArrayList(app_manifest.PlatformSettings) = .empty;
    errdefer platforms.deinit(allocator);
    for (values) |value| {
        try platforms.append(allocator, .{ .platform = parsePlatform(value) });
    }
    return platforms.toOwnedSlice(allocator);
}

fn parsePlatform(value: []const u8) app_manifest.Platform {
    if (std.mem.eql(u8, value, "macos")) return .macos;
    if (std.mem.eql(u8, value, "windows")) return .windows;
    if (std.mem.eql(u8, value, "linux")) return .linux;
    if (std.mem.eql(u8, value, "ios")) return .ios;
    if (std.mem.eql(u8, value, "android")) return .android;
    if (std.mem.eql(u8, value, "web")) return .web;
    return .unknown;
}

fn parseExternalLinkAction(value: []const u8) !app_manifest.ExternalLinkAction {
    if (std.mem.eql(u8, value, "deny")) return .deny;
    if (std.mem.eql(u8, value, "open_system_browser")) return .open_system_browser;
    return error.InvalidAction;
}

fn parseRestorePolicy(value: []const u8) !app_manifest.WindowRestorePolicy {
    if (std.mem.eql(u8, value, "clamp_to_visible_screen")) return .clamp_to_visible_screen;
    if (std.mem.eql(u8, value, "center_on_primary")) return .center_on_primary;
    return error.InvalidWindowRestorePolicy;
}

fn parseTitlebarStyle(value: []const u8) !app_manifest.WindowTitlebarStyle {
    if (std.mem.eql(u8, value, "standard")) return .standard;
    if (std.mem.eql(u8, value, "hidden_inset")) return .hidden_inset;
    if (std.mem.eql(u8, value, "hidden_inset_tall")) return .hidden_inset_tall;
    if (std.mem.eql(u8, value, "chromeless")) return .chromeless;
    return error.InvalidWindowTitlebarStyle;
}

fn parseClosePolicy(value: []const u8) !app_manifest.WindowClosePolicy {
    if (std.mem.eql(u8, value, "quit")) return .quit;
    if (std.mem.eql(u8, value, "hide")) return .hide;
    return error.InvalidWindowClosePolicy;
}

/// Same validation posture as the titlebar style: a min-size floor the
/// host cannot honor (negative or non-finite) is a manifest error, not
/// a silent clamp. 0 is the "no floor" sentinel.
fn parseWindowMinSize(value: f32) !f32 {
    if (!std.math.isFinite(value) or value < 0) return error.InvalidWindowMinSize;
    return value;
}

fn parseViewKind(value: []const u8) !app_manifest.ViewKind {
    if (std.mem.eql(u8, value, "webview")) return .webview;
    if (std.mem.eql(u8, value, "toolbar")) return .toolbar;
    if (std.mem.eql(u8, value, "titlebar_accessory")) return .titlebar_accessory;
    if (std.mem.eql(u8, value, "sidebar")) return .sidebar;
    if (std.mem.eql(u8, value, "statusbar")) return .statusbar;
    if (std.mem.eql(u8, value, "split")) return .split;
    if (std.mem.eql(u8, value, "stack")) return .stack;
    if (std.mem.eql(u8, value, "button")) return .button;
    if (std.mem.eql(u8, value, "icon_button")) return .icon_button;
    if (std.mem.eql(u8, value, "list_item")) return .list_item;
    if (std.mem.eql(u8, value, "checkbox")) return .checkbox;
    if (std.mem.eql(u8, value, "toggle")) return .toggle;
    if (std.mem.eql(u8, value, "segmented_control")) return .segmented_control;
    if (std.mem.eql(u8, value, "text_field")) return .text_field;
    if (std.mem.eql(u8, value, "search_field")) return .search_field;
    if (std.mem.eql(u8, value, "label")) return .label;
    if (std.mem.eql(u8, value, "spacer")) return .spacer;
    if (std.mem.eql(u8, value, "gpu_surface")) return .gpu_surface;
    if (std.mem.eql(u8, value, "progress_indicator")) return .progress_indicator;
    return error.InvalidViewKind;
}

fn parseGpuSurfaceBackend(value: []const u8) !app_manifest.GpuSurfaceBackend {
    if (std.mem.eql(u8, value, "none")) return .none;
    if (std.mem.eql(u8, value, "metal")) return .metal;
    if (std.mem.eql(u8, value, "software")) return .software;
    return error.InvalidViewKind;
}

fn parseGpuSurfacePixelFormat(value: []const u8) !app_manifest.GpuSurfacePixelFormat {
    if (std.mem.eql(u8, value, "none")) return .none;
    if (std.mem.eql(u8, value, "bgra8_unorm")) return .bgra8_unorm;
    return error.InvalidViewKind;
}

fn parseGpuSurfacePresentMode(value: []const u8) !app_manifest.GpuSurfacePresentMode {
    if (std.mem.eql(u8, value, "none")) return .none;
    if (std.mem.eql(u8, value, "timer")) return .timer;
    return error.InvalidViewKind;
}

fn parseGpuSurfaceAlphaMode(value: []const u8) !app_manifest.GpuSurfaceAlphaMode {
    if (std.mem.eql(u8, value, "none")) return .none;
    if (std.mem.eql(u8, value, "opaque")) return .@"opaque";
    if (std.mem.eql(u8, value, "premultiplied")) return .premultiplied;
    return error.InvalidViewKind;
}

fn parseGpuSurfaceColorSpace(value: []const u8) !app_manifest.GpuSurfaceColorSpace {
    if (std.mem.eql(u8, value, "none")) return .none;
    if (std.mem.eql(u8, value, "srgb")) return .srgb;
    if (std.mem.eql(u8, value, "display_p3")) return .display_p3;
    return error.InvalidViewKind;
}

fn parseShellEdge(value: []const u8) !app_manifest.ShellEdge {
    if (std.mem.eql(u8, value, "top")) return .top;
    if (std.mem.eql(u8, value, "right")) return .right;
    if (std.mem.eql(u8, value, "bottom")) return .bottom;
    if (std.mem.eql(u8, value, "left")) return .left;
    return error.InvalidLayout;
}

fn parseShellAxis(value: []const u8) !app_manifest.ShellAxis {
    if (std.mem.eql(u8, value, "row") or std.mem.eql(u8, value, "horizontal")) return .row;
    if (std.mem.eql(u8, value, "column") or std.mem.eql(u8, value, "vertical")) return .column;
    return error.InvalidLayout;
}

fn parseWebEngine(value: []const u8) !app_manifest.WebEngine {
    if (std.mem.eql(u8, value, "system")) return .system;
    if (std.mem.eql(u8, value, "chromium")) return .chromium;
    return error.InvalidWebEngine;
}

fn parseWebViewLayer(value: []const u8) !app_manifest.WebViewLayer {
    if (std.mem.eql(u8, value, "auto")) return .auto;
    if (std.mem.eql(u8, value, "include")) return .include;
    if (std.mem.eql(u8, value, "exclude")) return .exclude;
    return error.InvalidWebViewLayer;
}

/// The teaching message every boundary prints for the same
/// contradiction: a manifest that excludes the web layer while declaring
/// web content.
pub const web_layer_conflict_message = "the app manifest sets webview_layer = \"exclude\" but the app declares web content (a frontend block, the \"webview\" capability, a shell webview view, or the Chromium web engine - from web_engine or --web-engine) - remove the web declarations or drop the exclude";

/// The same contradiction arriving through the CLI flag instead of the
/// manifest field: `--web-layer exclude` against an app that declares
/// web content.
pub const web_layer_flag_conflict_message = "--web-layer exclude contradicts the app's web declarations (a .frontend block, the \"webview\" capability, a .shell webview view, or the Chromium web engine - from .web_engine or --web-engine) - remove the web declarations or drop the flag";

/// Why the web layer is (or is not) in the build, for verdict lines —
/// the shared contract's reason set.
pub const WebLayerReason = app_manifest.web_layer.Reason;

/// The layer setting a boundary resolves before deciding: "auto",
/// "include", or "exclude" — the shared contract's input enum, exported
/// for the CLI's `--web-layer` flag.
pub const WebViewLayerSetting = app_manifest.WebViewLayer;

/// Parse a `--web-layer` flag value (auto|include|exclude), via the
/// shared contract so the flag and the app.zon field accept exactly the
/// same vocabulary.
pub const parseWebViewLayerSetting = app_manifest.web_layer.parseWebViewLayer;

pub const WebLayer = struct {
    enabled: bool,
    reason: WebLayerReason,
    /// Whether the deciding include/exclude came from the CLI's
    /// `--web-layer` flag rather than app.zon's `.webview_layer`; only
    /// meaningful for the declared_include/declared_exclude reasons.
    from_flag: bool = false,

    /// The parenthesized half of a verdict line: `web layer: none
    /// (inferred)` / `web layer: webview2 (declared: capabilities)`.
    pub fn sourceText(self: WebLayer) []const u8 {
        return switch (self.reason) {
            .inferred_native_only => "inferred: nothing in app.zon declares web use",
            .declared_exclude => if (self.from_flag) "declared: --web-layer exclude" else "declared: .webview_layer = \"exclude\"",
            .capability => "declared: capabilities",
            .frontend => "declared: .frontend",
            .shell_webview => "declared: .shell webview view",
            .chromium_engine => "declared: the Chromium web engine (.web_engine or --web-engine)",
            .declared_include => if (self.from_flag) "declared: --web-layer include" else "declared: .webview_layer = \"include\"",
            // Only the build graph's lenient parse can produce this
            // reason; parsed metadata always reaches this fn readable.
            .unreadable_manifest => "kept: app.zon could not be parsed",
        };
    }
};

pub const WebLayerError = error{ InvalidWebViewLayer, WebViewLayerConflict };

/// The CLI-side adapter over the shared web-layer contract
/// (app_manifest.web_layer): the same declare-to-use rule the build
/// graph and the runner apply, fed the engine this boundary RESOLVED —
/// `--web-engine` orelse app.zon, already resolved by the CLI's verb
/// handlers. `.webview_layer = "include"|"exclude"` overrides, and an
/// exclude that contradicts a web declaration (including a resolved
/// Chromium engine) is refused.
pub fn webLayer(metadata: Metadata, resolved_engine: web_engine_tool.Engine) WebLayerError!WebLayer {
    return webLayerResolved(metadata, resolved_engine, null);
}

/// `webLayer` with the CLI's `--web-layer` flag in play: the flag beats
/// app.zon's `.webview_layer` exactly as `-Dweb-layer` beats it in the
/// build graph (effective setting = flag orelse manifest), so the build
/// graphs can forward their resolved decision and hand-run packages can
/// override the field without editing app.zon. An exclude flag against
/// a web declaration is the same refused conflict as a manifest exclude.
pub fn webLayerResolved(metadata: Metadata, resolved_engine: web_engine_tool.Engine, layer_flag: ?WebViewLayerSetting) WebLayerError!WebLayer {
    const manifest_setting = parseWebViewLayer(metadata.webview_layer) catch return error.InvalidWebViewLayer;
    const engine: app_manifest.WebEngine = switch (resolved_engine) {
        .system => .system,
        .chromium => .chromium,
    };
    const decision = app_manifest.web_layer.infer(metadata, engine, layer_flag orelse manifest_setting) catch return error.WebViewLayerConflict;
    if (layer_flag != null) {
        // The flag decides, but the verdict line keeps app.zon's own
        // richer reason whenever the flag merely confirms what the
        // manifest already decides: the build graphs forward their
        // resolved decision on every `zig build package`, and the common
        // case must keep reporting "declared: capabilities", not the
        // forwarded flag. Only a flag that CHANGES the outcome names
        // itself as the cause.
        if (app_manifest.web_layer.infer(metadata, engine, manifest_setting)) |manifest_decision| {
            if (manifest_decision.enabled == decision.enabled) {
                return .{ .enabled = manifest_decision.enabled, .reason = manifest_decision.reason };
            }
        } else |_| {}
        return .{ .enabled = decision.enabled, .reason = decision.reason, .from_flag = true };
    }
    return .{ .enabled = decision.enabled, .reason = decision.reason };
}

/// The web-layer verdict for callers with no engine flag in play
/// (`native check`): the manifest's own engine is the resolved engine.
pub fn webLayerFromManifest(metadata: Metadata) WebLayerError!WebLayer {
    return webLayer(metadata, web_engine_tool.Engine.parse(metadata.web_engine) orelse .system);
}

fn validateRelativePath(path: []const u8) !void {
    if (path.len == 0) return error.InvalidPath;
    if (path[0] == '/' or path[0] == '\\') return error.InvalidPath;
    if (path.len >= 3 and std.ascii.isAlphabetic(path[0]) and path[1] == ':' and (path[2] == '/' or path[2] == '\\')) return error.InvalidPath;
    var segment_start: usize = 0;
    for (path, 0..) |ch, index| {
        if (ch == 0 or ch == '\\') return error.InvalidPath;
        if (ch == '/') {
            try validatePathSegment(path[segment_start..index]);
            segment_start = index + 1;
        }
    }
    try validatePathSegment(path[segment_start..]);
}

pub fn validateDmgSettings(dmg: DmgMetadata) !void {
    if (dmg.window_width < 320 or dmg.window_width > 2000) return error.InvalidDmgWindow;
    if (dmg.window_height < 240 or dmg.window_height > 1400) return error.InvalidDmgWindow;
    if (dmg.icon_size < 32 or dmg.icon_size > 256) return error.InvalidDmgIconSize;
    if (dmg.items.len == 0) {
        if (!dmgPositionInsideWindow(dmg, dmg.app_position)) return error.InvalidDmgPosition;
        if (dmg.applications_link and !dmgPositionInsideWindow(dmg, dmg.applications_position)) return error.InvalidDmgPosition;
    } else {
        if (dmg.items.len > 64) return error.TooManyDmgItems;
        var app_count: usize = 0;
        var applications_count: usize = 0;
        for (dmg.items, 0..) |item, index| {
            if (!dmgPositionInsideWindow(dmg, item.position)) return error.InvalidDmgPosition;
            switch (item.kind) {
                .app => {
                    app_count += 1;
                    if (item.path != null) return error.InvalidDmgItem;
                    if (item.name) |name| try validateDmgItemName(name);
                },
                .applications => {
                    applications_count += 1;
                    if (item.path != null or item.name != null) return error.InvalidDmgItem;
                },
                .file => {
                    const path = item.path orelse return error.InvalidDmgItem;
                    try validateRelativePath(path);
                    if (item.name) |name| try validateDmgItemName(name);
                },
                .link => {
                    const target = item.path orelse return error.InvalidDmgItem;
                    if (!std.fs.path.isAbsolute(target)) return error.InvalidDmgItem;
                    for (target) |byte| if (byte < 0x20 or byte == 0x7f) return error.InvalidDmgItem;
                    if (item.name) |name| try validateDmgItemName(name);
                },
            }
            const name = dmgItemDestinationName(item) orelse continue;
            try validateDmgItemName(name);
            for (dmg.items[0..index]) |previous| {
                const previous_name = dmgItemDestinationName(previous) orelse continue;
                if (std.ascii.eqlIgnoreCase(previous_name, name)) return error.DuplicateDmgItem;
            }
        }
        if (app_count != 1 or applications_count > 1) return error.InvalidDmgItem;
    }
    if (dmg.volume_name) |name| try validateDmgVolumeName(name);
    if (dmg.background) |background| {
        try validateRelativePath(background);
        if (!dmgBackgroundExtensionSupported(background)) return error.InvalidDmgBackground;
    }
}

/// Validate the derived values packaging actually passes to `hdiutil` and
/// `ditto`, not only the optional raw overrides. In particular, an absent
/// volume name falls back to the app display name, and an app item without an
/// `.app` suffix gains one before it becomes a filesystem component.
pub fn validateDmgPackageSettings(metadata: Metadata) !void {
    try validateDmgSettings(metadata.dmg);
    try validateDmgVolumeName(metadata.dmg.volume_name orelse metadata.displayName());

    var app_name_buffer: [255]u8 = undefined;
    const app_name = try dmgAppBundleName(&app_name_buffer, metadata);
    for (metadata.dmg.items) |item| {
        const destination_name = dmgItemDestinationName(item) orelse continue;
        if (std.ascii.eqlIgnoreCase(destination_name, app_name)) return error.DuplicateDmgItem;
    }
}

fn dmgConfiguredAppName(metadata: Metadata) []const u8 {
    if (metadata.dmg.items.len > 0) {
        for (metadata.dmg.items) |item| {
            if (item.kind == .app) return item.name orelse metadata.displayName();
        }
    }
    return metadata.displayName();
}

/// Resolve the exact app-bundle filesystem name used inside the DMG. Keeping
/// this beside validation ensures collision checks and staging cannot drift.
pub fn dmgAppBundleName(buffer: []u8, metadata: Metadata) ![]const u8 {
    const configured_name = dmgConfiguredAppName(metadata);
    const has_app_suffix = configured_name.len >= 4 and std.ascii.eqlIgnoreCase(configured_name[configured_name.len - 4 ..], ".app");
    const configured_stem = if (has_app_suffix) configured_name[0 .. configured_name.len - 4] else configured_name;
    const stem = if (configured_stem.len == 0) metadata.name else configured_stem;
    if (stem.len > 255 - ".app".len or buffer.len < stem.len + ".app".len) return error.InvalidDmgItem;

    for (stem, 0..) |byte, index| {
        buffer[index] = switch (byte) {
            '/', ':', '\\', 0...0x1f, 0x7f => '-',
            else => byte,
        };
    }
    @memcpy(buffer[stem.len..][0..".app".len], ".app");
    return buffer[0 .. stem.len + ".app".len];
}

fn validateDmgVolumeName(name: []const u8) !void {
    if (name.len == 0 or name.len > 127) return error.InvalidDmgVolumeName;
    for (name) |byte| {
        if (byte < 0x20 or byte == 0x7f or byte == '/' or byte == ':') return error.InvalidDmgVolumeName;
    }
}

fn dmgPositionInsideWindow(dmg: DmgMetadata, position: DmgPosition) bool {
    return position.x < dmg.window_width and position.y < dmg.window_height;
}

pub fn dmgItemDestinationName(item: DmgItemMetadata) ?[]const u8 {
    return switch (item.kind) {
        .app => null,
        .applications => "Applications",
        .file, .link => item.name orelse if (item.path) |path| std.fs.path.basename(path) else null,
    };
}

fn validateDmgItemName(name: []const u8) !void {
    if (name.len == 0 or name.len > 255) return error.InvalidDmgItem;
    if (std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, "..")) return error.InvalidDmgItem;
    if (std.ascii.eqlIgnoreCase(name, ".background") or std.ascii.eqlIgnoreCase(name, ".DS_Store")) return error.InvalidDmgItem;
    for (name) |byte| {
        if (byte < 0x20 or byte == 0x7f or byte == '/' or byte == ':' or byte == '\\') return error.InvalidDmgItem;
    }
}

fn dmgBackgroundExtensionSupported(path: []const u8) bool {
    return app_icon_tool.pathHasExtension(path, ".png") or
        app_icon_tool.pathHasExtension(path, ".jpg") or
        app_icon_tool.pathHasExtension(path, ".jpeg") or
        app_icon_tool.pathHasExtension(path, ".tif") or
        app_icon_tool.pathHasExtension(path, ".tiff");
}

const max_dmg_background_bytes = 64 * 1024 * 1024;

const DmgImageDimensions = struct {
    width: usize,
    height: usize,
};

pub fn dmgRetinaRelativePathAlloc(allocator: std.mem.Allocator, path: []const u8) !?[]u8 {
    if (app_icon_tool.pathHasExtension(path, ".tif") or app_icon_tool.pathHasExtension(path, ".tiff")) return null;
    const extension = std.fs.path.extension(path);
    const retina_path = try std.fmt.allocPrint(allocator, "{s}@2x{s}", .{ path[0 .. path.len - extension.len], extension });
    return retina_path;
}

fn dmgImageDimensions(path: []const u8, bytes: []const u8) ?DmgImageDimensions {
    if (app_icon_tool.pathHasExtension(path, ".png")) return pngDimensions(bytes);
    if (app_icon_tool.pathHasExtension(path, ".jpg") or app_icon_tool.pathHasExtension(path, ".jpeg")) return jpegDimensions(bytes);
    if (app_icon_tool.pathHasExtension(path, ".tif") or app_icon_tool.pathHasExtension(path, ".tiff")) return tiffDimensions(bytes);
    return null;
}

/// Validate the complete PNG chunk envelope (including CRCs and IEND), while
/// leaving pixel decoding to Finder. This accepts interlaced/background PNGs
/// beyond the deliberately narrower app-icon rasterizer dialect.
fn pngDimensions(bytes: []const u8) ?DmgImageDimensions {
    const signature = "\x89PNG\r\n\x1a\n";
    if (bytes.len < signature.len or !std.mem.eql(u8, bytes[0..signature.len], signature)) return null;
    var offset: usize = signature.len;
    var dimensions: ?DmgImageDimensions = null;
    var saw_idat = false;
    while (offset < bytes.len) {
        if (bytes.len - offset < 12) return null;
        const data_len: usize = std.mem.readInt(u32, bytes[offset..][0..4], .big);
        if (data_len > bytes.len - offset - 12) return null;
        const chunk_type = bytes[offset + 4 .. offset + 8];
        const data = bytes[offset + 8 .. offset + 8 + data_len];
        const expected_crc = std.mem.readInt(u32, bytes[offset + 8 + data_len ..][0..4], .big);
        var crc = std.hash.Crc32.init();
        crc.update(chunk_type);
        crc.update(data);
        if (crc.final() != expected_crc) return null;
        offset += 12 + data_len;

        if (std.mem.eql(u8, chunk_type, "IHDR")) {
            if (dimensions != null or data.len != 13 or saw_idat) return null;
            const width = std.mem.readInt(u32, data[0..4], .big);
            const height = std.mem.readInt(u32, data[4..8], .big);
            if (width == 0 or height == 0 or data[10] != 0 or data[11] != 0 or data[12] > 1) return null;
            const valid_depth = switch (data[9]) {
                0 => data[8] == 1 or data[8] == 2 or data[8] == 4 or data[8] == 8 or data[8] == 16,
                2, 4, 6 => data[8] == 8 or data[8] == 16,
                3 => data[8] == 1 or data[8] == 2 or data[8] == 4 or data[8] == 8,
                else => false,
            };
            if (!valid_depth) return null;
            dimensions = .{ .width = width, .height = height };
        } else if (std.mem.eql(u8, chunk_type, "IDAT")) {
            if (dimensions == null) return null;
            saw_idat = true;
        } else if (std.mem.eql(u8, chunk_type, "IEND")) {
            if (data.len != 0 or !saw_idat or offset != bytes.len) return null;
            return dimensions;
        }
    }
    return null;
}

fn jpegDimensions(bytes: []const u8) ?DmgImageDimensions {
    if (bytes.len < 4 or bytes[0] != 0xff or bytes[1] != 0xd8) return null;
    var offset: usize = 2;
    var dimensions: ?DmgImageDimensions = null;
    var in_scan = false;
    var saw_scan = false;
    while (offset < bytes.len) {
        if (in_scan) {
            while (offset < bytes.len and bytes[offset] != 0xff) : (offset += 1) {}
            if (offset == bytes.len) return null;
        } else if (bytes[offset] != 0xff) {
            return null;
        }
        while (offset < bytes.len and bytes[offset] == 0xff) : (offset += 1) {}
        if (offset == bytes.len) return null;
        const marker = bytes[offset];
        offset += 1;
        if (marker == 0x00) {
            if (!in_scan) return null;
            continue;
        }
        if (marker == 0xd9) return if (saw_scan) dimensions else null;
        if (marker >= 0xd0 and marker <= 0xd7) {
            if (!in_scan) return null;
            continue;
        }
        if (marker == 0x01) continue;
        if (marker == 0xd8) return null;
        if (bytes.len - offset < 2) return null;
        const segment_len: usize = std.mem.readInt(u16, bytes[offset..][0..2], .big);
        if (segment_len < 2 or segment_len > bytes.len - offset) return null;
        const segment = bytes[offset + 2 .. offset + segment_len];
        if (jpegMarkerCarriesDimensions(marker)) {
            if (segment.len < 6) return null;
            const height = std.mem.readInt(u16, segment[1..3], .big);
            const width = std.mem.readInt(u16, segment[3..5], .big);
            if (width == 0 or height == 0) return null;
            dimensions = .{ .width = width, .height = height };
        }
        in_scan = marker == 0xda;
        saw_scan = saw_scan or in_scan;
        offset += segment_len;
    }
    return null;
}

fn jpegMarkerCarriesDimensions(marker: u8) bool {
    return switch (marker) {
        0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf => true,
        else => false,
    };
}

fn tiffDimensions(bytes: []const u8) ?DmgImageDimensions {
    if (bytes.len < 8) return null;
    const endian: std.builtin.Endian = if (std.mem.eql(u8, bytes[0..2], "II"))
        .little
    else if (std.mem.eql(u8, bytes[0..2], "MM"))
        .big
    else
        return null;
    return switch (readEndianU16(bytes, 2, endian) orelse return null) {
        42 => classicTiffDimensions(bytes, endian),
        43 => bigTiffDimensions(bytes, endian),
        else => null,
    };
}

const max_tiff_directories = 64;

const TiffIntegerKind = enum {
    short,
    long,
    long8,
};

const TiffIntegerField = struct {
    kind: TiffIntegerKind,
    count: usize,
    data_offset: usize,
};

const TiffDirectory = struct {
    dimensions: DmgImageDimensions,
    next_offset: usize,
};

fn classicTiffDimensions(bytes: []const u8, endian: std.builtin.Endian) ?DmgImageDimensions {
    var ifd_offset = std.math.cast(usize, readEndianU32(bytes, 4, endian) orelse return null) orelse return null;
    var dimensions: ?DmgImageDimensions = null;
    var directory_count: usize = 0;
    while (ifd_offset != 0) {
        if (directory_count == max_tiff_directories) return null;
        const directory = classicTiffDirectory(bytes, ifd_offset, endian) orelse return null;
        if (dimensions == null) dimensions = directory.dimensions;
        ifd_offset = directory.next_offset;
        directory_count += 1;
    }
    return dimensions;
}

fn classicTiffDirectory(bytes: []const u8, ifd_offset: usize, endian: std.builtin.Endian) ?TiffDirectory {
    const entry_count = readEndianU16(bytes, ifd_offset, endian) orelse return null;
    const entries_start = std.math.add(usize, ifd_offset, 2) catch return null;
    const entries_bytes = std.math.mul(usize, entry_count, 12) catch return null;
    const entries_end = std.math.add(usize, entries_start, entries_bytes) catch return null;
    if (entries_end > bytes.len or bytes.len - entries_end < 4) return null;
    var width: ?usize = null;
    var height: ?usize = null;
    var strip_offsets: ?TiffIntegerField = null;
    var strip_byte_counts: ?TiffIntegerField = null;
    var tile_offsets: ?TiffIntegerField = null;
    var tile_byte_counts: ?TiffIntegerField = null;
    for (0..entry_count) |index| {
        const entry_offset = entries_start + index * 12;
        const tag = readEndianU16(bytes, entry_offset, endian) orelse return null;
        switch (tag) {
            256, 257 => {
                const field = classicTiffIntegerField(bytes, entry_offset, endian) orelse return null;
                if (field.count != 1) return null;
                const value = tiffIntegerValue(bytes, field, 0, endian) orelse return null;
                if (tag == 256) {
                    if (width != null) return null;
                    width = value;
                } else {
                    if (height != null) return null;
                    height = value;
                }
            },
            273 => {
                if (strip_offsets != null) return null;
                strip_offsets = classicTiffIntegerField(bytes, entry_offset, endian) orelse return null;
            },
            279 => {
                if (strip_byte_counts != null) return null;
                strip_byte_counts = classicTiffIntegerField(bytes, entry_offset, endian) orelse return null;
            },
            324 => {
                if (tile_offsets != null) return null;
                tile_offsets = classicTiffIntegerField(bytes, entry_offset, endian) orelse return null;
            },
            325 => {
                if (tile_byte_counts != null) return null;
                tile_byte_counts = classicTiffIntegerField(bytes, entry_offset, endian) orelse return null;
            },
            else => {},
        }
    }
    if ((width orelse 0) == 0 or (height orelse 0) == 0) return null;
    if (!tiffImagePayloadsValid(bytes, endian, strip_offsets, strip_byte_counts, tile_offsets, tile_byte_counts)) return null;
    return .{
        .dimensions = .{ .width = width.?, .height = height.? },
        .next_offset = std.math.cast(usize, readEndianU32(bytes, entries_end, endian) orelse return null) orelse return null,
    };
}

fn classicTiffIntegerField(bytes: []const u8, entry_offset: usize, endian: std.builtin.Endian) ?TiffIntegerField {
    const kind: TiffIntegerKind = switch (readEndianU16(bytes, entry_offset + 2, endian) orelse return null) {
        3 => .short,
        4 => .long,
        else => return null,
    };
    const count = std.math.cast(usize, readEndianU32(bytes, entry_offset + 4, endian) orelse return null) orelse return null;
    const payload_bytes = std.math.mul(usize, count, tiffIntegerSize(kind)) catch return null;
    const data_offset = if (payload_bytes <= 4)
        entry_offset + 8
    else
        std.math.cast(usize, readEndianU32(bytes, entry_offset + 8, endian) orelse return null) orelse return null;
    if (data_offset > bytes.len or payload_bytes > bytes.len - data_offset) return null;
    return .{ .kind = kind, .count = count, .data_offset = data_offset };
}

fn bigTiffDimensions(bytes: []const u8, endian: std.builtin.Endian) ?DmgImageDimensions {
    if (bytes.len < 16 or (readEndianU16(bytes, 4, endian) orelse return null) != 8 or (readEndianU16(bytes, 6, endian) orelse return null) != 0) return null;
    var ifd_offset = std.math.cast(usize, readEndianU64(bytes, 8, endian) orelse return null) orelse return null;
    var dimensions: ?DmgImageDimensions = null;
    var directory_count: usize = 0;
    while (ifd_offset != 0) {
        if (directory_count == max_tiff_directories) return null;
        const directory = bigTiffDirectory(bytes, ifd_offset, endian) orelse return null;
        if (dimensions == null) dimensions = directory.dimensions;
        ifd_offset = directory.next_offset;
        directory_count += 1;
    }
    return dimensions;
}

fn bigTiffDirectory(bytes: []const u8, ifd_offset: usize, endian: std.builtin.Endian) ?TiffDirectory {
    const entry_count = std.math.cast(usize, readEndianU64(bytes, ifd_offset, endian) orelse return null) orelse return null;
    const entries_start = std.math.add(usize, ifd_offset, 8) catch return null;
    const entries_bytes = std.math.mul(usize, entry_count, 20) catch return null;
    const entries_end = std.math.add(usize, entries_start, entries_bytes) catch return null;
    if (entries_end > bytes.len or bytes.len - entries_end < 8) return null;
    var width: ?usize = null;
    var height: ?usize = null;
    var strip_offsets: ?TiffIntegerField = null;
    var strip_byte_counts: ?TiffIntegerField = null;
    var tile_offsets: ?TiffIntegerField = null;
    var tile_byte_counts: ?TiffIntegerField = null;
    for (0..entry_count) |index| {
        const entry_offset = entries_start + index * 20;
        const tag = readEndianU16(bytes, entry_offset, endian) orelse return null;
        switch (tag) {
            256, 257 => {
                const field = bigTiffIntegerField(bytes, entry_offset, endian) orelse return null;
                if (field.count != 1) return null;
                const value = tiffIntegerValue(bytes, field, 0, endian) orelse return null;
                if (tag == 256) {
                    if (width != null) return null;
                    width = value;
                } else {
                    if (height != null) return null;
                    height = value;
                }
            },
            273 => {
                if (strip_offsets != null) return null;
                strip_offsets = bigTiffIntegerField(bytes, entry_offset, endian) orelse return null;
            },
            279 => {
                if (strip_byte_counts != null) return null;
                strip_byte_counts = bigTiffIntegerField(bytes, entry_offset, endian) orelse return null;
            },
            324 => {
                if (tile_offsets != null) return null;
                tile_offsets = bigTiffIntegerField(bytes, entry_offset, endian) orelse return null;
            },
            325 => {
                if (tile_byte_counts != null) return null;
                tile_byte_counts = bigTiffIntegerField(bytes, entry_offset, endian) orelse return null;
            },
            else => {},
        }
    }
    if ((width orelse 0) == 0 or (height orelse 0) == 0) return null;
    if (!tiffImagePayloadsValid(bytes, endian, strip_offsets, strip_byte_counts, tile_offsets, tile_byte_counts)) return null;
    return .{
        .dimensions = .{ .width = width.?, .height = height.? },
        .next_offset = std.math.cast(usize, readEndianU64(bytes, entries_end, endian) orelse return null) orelse return null,
    };
}

fn bigTiffIntegerField(bytes: []const u8, entry_offset: usize, endian: std.builtin.Endian) ?TiffIntegerField {
    const kind: TiffIntegerKind = switch (readEndianU16(bytes, entry_offset + 2, endian) orelse return null) {
        3 => .short,
        4 => .long,
        16 => .long8,
        else => return null,
    };
    const count = std.math.cast(usize, readEndianU64(bytes, entry_offset + 4, endian) orelse return null) orelse return null;
    const payload_bytes = std.math.mul(usize, count, tiffIntegerSize(kind)) catch return null;
    const data_offset = if (payload_bytes <= 8)
        entry_offset + 12
    else
        std.math.cast(usize, readEndianU64(bytes, entry_offset + 12, endian) orelse return null) orelse return null;
    if (data_offset > bytes.len or payload_bytes > bytes.len - data_offset) return null;
    return .{ .kind = kind, .count = count, .data_offset = data_offset };
}

fn tiffIntegerSize(kind: TiffIntegerKind) usize {
    return switch (kind) {
        .short => 2,
        .long => 4,
        .long8 => 8,
    };
}

fn tiffIntegerValue(bytes: []const u8, field: TiffIntegerField, index: usize, endian: std.builtin.Endian) ?usize {
    if (index >= field.count) return null;
    const value_offset = std.math.add(usize, field.data_offset, std.math.mul(usize, index, tiffIntegerSize(field.kind)) catch return null) catch return null;
    return switch (field.kind) {
        .short => readEndianU16(bytes, value_offset, endian) orelse return null,
        .long => std.math.cast(usize, readEndianU32(bytes, value_offset, endian) orelse return null) orelse return null,
        .long8 => std.math.cast(usize, readEndianU64(bytes, value_offset, endian) orelse return null) orelse return null,
    };
}

fn tiffImagePayloadsValid(
    bytes: []const u8,
    endian: std.builtin.Endian,
    strip_offsets: ?TiffIntegerField,
    strip_byte_counts: ?TiffIntegerField,
    tile_offsets: ?TiffIntegerField,
    tile_byte_counts: ?TiffIntegerField,
) bool {
    const has_strips = strip_offsets != null or strip_byte_counts != null;
    const has_tiles = tile_offsets != null or tile_byte_counts != null;
    if (!has_strips and !has_tiles) return false;
    if (has_strips) {
        if (strip_offsets == null or strip_byte_counts == null) return false;
        if (!tiffPayloadRangesValid(bytes, endian, strip_offsets.?, strip_byte_counts.?)) return false;
    }
    if (has_tiles) {
        if (tile_offsets == null or tile_byte_counts == null) return false;
        if (!tiffPayloadRangesValid(bytes, endian, tile_offsets.?, tile_byte_counts.?)) return false;
    }
    return true;
}

fn tiffPayloadRangesValid(bytes: []const u8, endian: std.builtin.Endian, offsets: TiffIntegerField, byte_counts: TiffIntegerField) bool {
    if (offsets.count == 0 or offsets.count != byte_counts.count) return false;
    for (0..offsets.count) |index| {
        const payload_offset = tiffIntegerValue(bytes, offsets, index, endian) orelse return false;
        const payload_bytes = tiffIntegerValue(bytes, byte_counts, index, endian) orelse return false;
        if (payload_bytes == 0 or payload_offset > bytes.len or payload_bytes > bytes.len - payload_offset) return false;
    }
    return true;
}

fn readEndianU16(bytes: []const u8, offset: usize, endian: std.builtin.Endian) ?u16 {
    if (offset > bytes.len or bytes.len - offset < 2) return null;
    return switch (endian) {
        .little => std.mem.readInt(u16, bytes[offset..][0..2], .little),
        .big => std.mem.readInt(u16, bytes[offset..][0..2], .big),
    };
}

fn readEndianU32(bytes: []const u8, offset: usize, endian: std.builtin.Endian) ?u32 {
    if (offset > bytes.len or bytes.len - offset < 4) return null;
    return switch (endian) {
        .little => std.mem.readInt(u32, bytes[offset..][0..4], .little),
        .big => std.mem.readInt(u32, bytes[offset..][0..4], .big),
    };
}

fn readEndianU64(bytes: []const u8, offset: usize, endian: std.builtin.Endian) ?u64 {
    if (offset > bytes.len or bytes.len - offset < 8) return null;
    return switch (endian) {
        .little => std.mem.readInt(u64, bytes[offset..][0..8], .little),
        .big => std.mem.readInt(u64, bytes[offset..][0..8], .big),
    };
}

pub fn checkDmgSources(allocator: std.mem.Allocator, io: std.Io, manifest_dir: []const u8, dmg: DmgMetadata) !?[]const u8 {
    if (try checkDmgBackground(allocator, io, manifest_dir, dmg)) |message| return message;
    return checkDmgItemSources(allocator, io, manifest_dir, dmg.items);
}

fn checkDmgBackground(allocator: std.mem.Allocator, io: std.Io, manifest_dir: []const u8, dmg: DmgMetadata) !?[]const u8 {
    const path = dmg.background orelse return null;
    validateRelativePath(path) catch return "app.zon dmg background is invalid - use a project-relative PNG, JPEG, or TIFF path";
    if (!dmgBackgroundExtensionSupported(path)) return "app.zon dmg background is invalid - Finder backgrounds must be PNG, JPEG, or TIFF";
    const resolved = try std.fs.path.join(allocator, &.{ manifest_dir, path });
    defer allocator.free(resolved);
    const bytes = std.Io.Dir.cwd().readFileAlloc(io, resolved, allocator, .limited(max_dmg_background_bytes)) catch return "app.zon dmg background could not be read";
    defer allocator.free(bytes);
    const dimensions = dmgImageDimensions(path, bytes) orelse return "app.zon dmg background is not a structurally valid PNG, JPEG, or TIFF image";
    if (dimensions.width != dmg.window_width or dimensions.height != dmg.window_height) {
        const message = try std.fmt.allocPrint(allocator, "app.zon dmg background is {d}x{d} - expected {d}x{d} to match window_width/window_height", .{ dimensions.width, dimensions.height, dmg.window_width, dmg.window_height });
        return message;
    }

    const retina_relative = (try dmgRetinaRelativePathAlloc(allocator, path)) orelse return null;
    defer allocator.free(retina_relative);
    const retina_resolved = try std.fs.path.join(allocator, &.{ manifest_dir, retina_relative });
    defer allocator.free(retina_resolved);
    _ = std.Io.Dir.cwd().statFile(io, retina_resolved, .{}) catch return null;
    const retina_bytes = std.Io.Dir.cwd().readFileAlloc(io, retina_resolved, allocator, .limited(max_dmg_background_bytes)) catch return "app.zon dmg @2x background could not be read";
    defer allocator.free(retina_bytes);
    const retina_dimensions = dmgImageDimensions(retina_relative, retina_bytes) orelse return "app.zon dmg @2x background is not a structurally valid PNG or JPEG image";
    const expected_width: usize = @as(usize, dmg.window_width) * 2;
    const expected_height: usize = @as(usize, dmg.window_height) * 2;
    if (retina_dimensions.width != expected_width or retina_dimensions.height != expected_height) {
        const message = try std.fmt.allocPrint(allocator, "app.zon dmg @2x background is {d}x{d} - expected {d}x{d}", .{ retina_dimensions.width, retina_dimensions.height, expected_width, expected_height });
        return message;
    }
    return null;
}

fn checkDmgItemSources(allocator: std.mem.Allocator, io: std.Io, manifest_dir: []const u8, items: []const DmgItemMetadata) !?[]const u8 {
    for (items) |item| {
        if (item.kind != .file) continue;
        const path = item.path orelse continue;
        const resolved = try std.fs.path.join(allocator, &.{ manifest_dir, path });
        defer allocator.free(resolved);
        var file = std.Io.Dir.cwd().openFile(io, resolved, .{}) catch {
            var dir = std.Io.Dir.cwd().openDir(io, resolved, .{}) catch return "app.zon dmg item source could not be read";
            dir.close(io);
            continue;
        };
        file.close(io);
    }
    return null;
}

fn validatePathSegment(segment: []const u8) !void {
    if (segment.len == 0) return error.InvalidPath;
    if (std.mem.eql(u8, segment, ".") or std.mem.eql(u8, segment, "..")) return error.InvalidPath;
}

fn parseVersionNumber(value: []const u8) !u32 {
    if (value.len == 0 or (value.len > 1 and value[0] == '0')) return error.InvalidVersion;
    return std.fmt.parseUnsigned(u32, value, 10) catch error.InvalidVersion;
}

test "manifest versions use canonical numeric components" {
    try std.testing.expectEqual(@as(u32, 10), (try parseVersion("1.10.0")).minor);
    try std.testing.expectError(error.InvalidVersion, parseVersion("1.010.0"));
    try std.testing.expectError(error.InvalidVersion, parseVersion("01.0.0"));
    try std.testing.expectError(error.InvalidVersion, parseVersion("1.0.00"));
}

test "manifest validation rejects leading-zero versions" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "app.json", .data =
        \\{ "id": "com.example.version", "name": "version", "version": "1.010.0" }
    });
    const path = try std.fmt.allocPrint(std.testing.allocator, ".zig-cache/tmp/{s}/app.json", .{tmp.sub_path});
    defer std.testing.allocator.free(path);
    const result = try validateFile(std.testing.allocator, std.testing.io, path);
    try std.testing.expect(!result.ok);
    try std.testing.expectEqualStrings("app manifest version is invalid", result.message);
}

test "JSON manifest parser accepts schema metadata and rejects unknown fields" {
    const metadata = try parseJsonText(std.testing.allocator,
        \\{
        \\  "$schema": "https://schema.native-sdk.dev/app/v1.json",
        \\  "id": "com.example.json",
        \\  "name": "json-app",
        \\  "version": "1.2.3",
        \\  "capabilities": ["native_views"],
        \\  "windows": [{ "label": "main", "width": 640, "height": 480 }]
        \\}
    );
    defer metadata.deinit(std.testing.allocator);
    try std.testing.expectEqualStrings("com.example.json", metadata.id);
    try std.testing.expectEqualStrings("native_views", metadata.capabilities[0]);
    try std.testing.expectEqual(@as(f32, 640), metadata.windows[0].width);

    try std.testing.expectError(error.UnknownField, parseJsonText(std.testing.allocator,
        \\{ "id": "com.example.json", "name": "json-app", "version": "1.2.3", "typo": true }
    ));
    try std.testing.expectError(error.NullNotAllowed, parseJsonText(std.testing.allocator,
        \\{ "id": "com.example.json", "name": "json-app", "version": "1.2.3", "theme": null }
    ));
}

test "JSON manifest parser carries native update configuration" {
    const metadata = try parseJsonText(std.testing.allocator,
        \\{
        \\  "id": "com.example.updates",
        \\  "name": "updates",
        \\  "version": "1.2.3",
        \\  "updates": {
        \\    "feed_url": "https://example.com/native-update.json",
        \\    "public_key": "11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=",
        \\    "check_on_start": true
        \\  }
        \\}
    );
    defer metadata.deinit(std.testing.allocator);
    try std.testing.expect(metadata.updates.enabled());
    try std.testing.expect(metadata.updates.check_on_start);
    try std.testing.expectEqualStrings("https://example.com/native-update.json", metadata.updates.feed_url.?);
}

test "JSON file validation rejects null with a teaching diagnostic regardless of extension case" {
    var cwd = std.Io.Dir.cwd();
    const root = ".zig-cache/test-validate-json-null";
    try cwd.deleteTree(std.testing.io, root);
    defer cwd.deleteTree(std.testing.io, root) catch {};
    try cwd.createDirPath(std.testing.io, root);
    try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/APP.JSON", .data =
        \\{ "id": "com.example.json", "name": "json-app", "version": "1.2.3", "theme": null }
    });

    const result = try validateFile(std.testing.allocator, std.testing.io, root ++ "/APP.JSON");
    try std.testing.expect(!result.ok);
    try std.testing.expectEqualStrings("app.json cannot contain null values - omit optional fields instead", result.message);
}

test "manifest metadata parser reads identity version and lists" {
    const metadata = try parseText(std.testing.allocator,
        \\.{
        \\  .id = "com.example.app",
        \\  .name = "example",
        \\  .display_name = "Example App",
        \\  .description = "An example app for the manifest parser.",
        \\  .version = "1.2.3",
        \\  .icons = .{ "assets/icon.png" },
        \\  .platforms = .{ "macos", "linux" },
        \\  .capabilities = .{
        \\    "native_module", "webview", "js_bridge", "native_views", "gpu_surfaces", "menus", "shortcuts", "tray",
        \\    "dialog", "credentials", "file_drops", "file_associations", "url_schemes",
        \\    "open_url", "reveal_path", "recent_documents", "app_activation_events",
        \\  },
        \\  .bridge = .{ .commands = .{ .{ .name = "native.ping" } } },
        \\  .web_engine = "chromium",
        \\  .cef = .{ .dir = "third_party/cef/macos", .auto_install = true },
        \\  .commands = .{
        \\    .{ .id = "app.refresh", .title = "Refresh" },
        \\    .{ .id = "app.sidebar.toggle", .title = "Sidebar", .checked = true },
        \\  },
        \\  .menus = .{
        \\    .{
        \\      .title = "View",
        \\      .items = .{
        \\        .{ .label = "Refresh", .command = "app.refresh", .key = "r", .modifiers = .{ "primary" } },
        \\        .{ .separator = true },
        \\        .{ .label = "Sidebar", .command = "app.sidebar.toggle", .checked = true },
        \\      },
        \\    },
        \\  },
        \\  .shortcuts = .{
        \\    .{ .id = "command.palette", .key = "p", .modifiers = .{ "primary", "shift" } },
        \\  },
        \\  .file_associations = .{
        \\    .{ .name = "Markdown Document", .extensions = .{ "md", ".markdown" }, .mime_types = .{ "text/markdown" }, .icon = "assets/markdown.icns" },
        \\  },
        \\  .url_schemes = .{
        \\    .{ .scheme = "example-app" },
        \\  },
        \\  .dmg = .{
        \\    .volume_name = "Example Installer",
        \\    .background = "assets/dmg-background.png",
        \\    .window_width = 720,
        \\    .window_height = 440,
        \\    .icon_size = 144,
        \\    .app_position = .{ .x = 180, .y = 210 },
        \\    .applications_position = .{ .x = 540, .y = 210 },
        \\    .applications_link = false,
        \\  },
        \\}
    );
    defer metadata.deinit(std.testing.allocator);

    try std.testing.expectEqualStrings("com.example.app", metadata.id);
    try std.testing.expectEqualStrings("example", metadata.name);
    try std.testing.expectEqualStrings("Example App", metadata.displayName());
    try std.testing.expectEqualStrings("An example app for the manifest parser.", metadata.description.?);
    try std.testing.expectEqualStrings("1.2.3", metadata.version);
    try std.testing.expectEqualStrings("assets/icon.png", metadata.icons[0]);
    try std.testing.expectEqualStrings("linux", metadata.platforms[1]);
    try std.testing.expectEqualStrings("webview", metadata.capabilities[1]);
    try std.testing.expectEqualStrings("native_views", metadata.capabilities[3]);
    try std.testing.expectEqualStrings("gpu_surfaces", metadata.capabilities[4]);
    try std.testing.expectEqualStrings("dialog", metadata.capabilities[8]);
    try std.testing.expectEqualStrings("file_drops", metadata.capabilities[10]);
    try std.testing.expectEqualStrings("url_schemes", metadata.capabilities[12]);
    const parsed_capabilities = try parseCapabilities(std.testing.allocator, metadata.capabilities);
    defer std.testing.allocator.free(parsed_capabilities);
    try std.testing.expectEqual(app_manifest.CapabilityKind.native_views, parsed_capabilities[3].kind());
    try std.testing.expectEqual(app_manifest.CapabilityKind.gpu_surfaces, parsed_capabilities[4].kind());
    try std.testing.expectEqual(app_manifest.CapabilityKind.menus, parsed_capabilities[5].kind());
    try std.testing.expectEqual(app_manifest.CapabilityKind.shortcuts, parsed_capabilities[6].kind());
    try std.testing.expectEqual(app_manifest.CapabilityKind.tray, parsed_capabilities[7].kind());
    try std.testing.expectEqual(app_manifest.CapabilityKind.file_drops, parsed_capabilities[10].kind());
    try std.testing.expectEqual(app_manifest.CapabilityKind.file_associations, parsed_capabilities[11].kind());
    try std.testing.expectEqual(app_manifest.CapabilityKind.url_schemes, parsed_capabilities[12].kind());
    try std.testing.expectEqual(app_manifest.CapabilityKind.open_url, parsed_capabilities[13].kind());
    try std.testing.expectEqual(app_manifest.CapabilityKind.reveal_path, parsed_capabilities[14].kind());
    try std.testing.expectEqual(app_manifest.CapabilityKind.recent_documents, parsed_capabilities[15].kind());
    try std.testing.expectEqual(app_manifest.CapabilityKind.app_activation_events, parsed_capabilities[16].kind());
    try std.testing.expectEqualStrings("native.ping", metadata.bridge_commands[0].name);
    try std.testing.expectEqualStrings("app.refresh", metadata.commands[0].id);
    try std.testing.expectEqualStrings("Refresh", metadata.commands[0].title);
    try std.testing.expect(metadata.commands[0].enabled);
    try std.testing.expect(metadata.commands[1].checked);
    try std.testing.expectEqualStrings("View", metadata.menus[0].title);
    try std.testing.expectEqualStrings("Refresh", metadata.menus[0].items[0].label);
    try std.testing.expectEqualStrings("app.refresh", metadata.menus[0].items[0].command);
    try std.testing.expectEqualStrings("primary", metadata.menus[0].items[0].modifiers[0]);
    try std.testing.expect(metadata.menus[0].items[1].separator);
    try std.testing.expect(metadata.menus[0].items[2].checked);
    try std.testing.expectEqualStrings("command.palette", metadata.shortcuts[0].id);
    try std.testing.expectEqualStrings("primary", metadata.shortcuts[0].modifiers[0]);
    try std.testing.expectEqualStrings("Markdown Document", metadata.file_associations[0].name);
    try std.testing.expectEqualStrings(".markdown", metadata.file_associations[0].extensions[1]);
    try std.testing.expectEqualStrings("text/markdown", metadata.file_associations[0].mime_types[0]);
    try std.testing.expectEqualStrings("assets/markdown.icns", metadata.file_associations[0].icon.?);
    try std.testing.expectEqualStrings("example-app", metadata.url_schemes[0].scheme);
    try std.testing.expectEqualStrings("Example Installer", metadata.dmg.volume_name.?);
    try std.testing.expectEqualStrings("assets/dmg-background.png", metadata.dmg.background.?);
    try std.testing.expectEqual(@as(u16, 720), metadata.dmg.window_width);
    try std.testing.expectEqual(@as(u16, 440), metadata.dmg.window_height);
    try std.testing.expectEqual(@as(u16, 144), metadata.dmg.icon_size);
    try std.testing.expectEqual(@as(u16, 180), metadata.dmg.app_position.x);
    try std.testing.expectEqual(@as(u16, 210), metadata.dmg.app_position.y);
    try std.testing.expectEqual(@as(u16, 540), metadata.dmg.applications_position.x);
    try std.testing.expect(!metadata.dmg.applications_link);
    try std.testing.expectEqualStrings("chromium", metadata.web_engine);
    try std.testing.expectEqualStrings("third_party/cef/macos", metadata.cef.dir);
    try std.testing.expect(metadata.cef.auto_install);
    try std.testing.expectEqual(@as(u32, 2), (try parseVersion(metadata.version)).minor);

    const associations = try parseFileAssociations(std.testing.allocator, metadata.file_associations);
    defer std.testing.allocator.free(associations);
    const schemes = try parseUrlSchemes(std.testing.allocator, metadata.url_schemes);
    defer std.testing.allocator.free(schemes);
    const menus = try parseMenus(std.testing.allocator, metadata.menus);
    defer deinitParsedMenus(std.testing.allocator, menus);
    const commands = try parseCommands(std.testing.allocator, metadata.commands);
    defer std.testing.allocator.free(commands);
    try app_manifest.validateManifest(.{
        .identity = .{ .id = metadata.id, .name = metadata.name },
        .version = try parseVersion(metadata.version),
        .commands = commands,
        .menus = menus,
        .file_associations = associations,
        .url_schemes = schemes,
    });
}

test "manifest parser reads dock visibility and defaults it to visible" {
    const accessory = try parseText(std.testing.allocator,
        \\.{
        \\  .id = "com.example.menu",
        \\  .name = "menu",
        \\  .version = "1.0.0",
        \\  .capabilities = .{"tray"},
        \\  .dock_visible = false,
        \\}
    );
    defer accessory.deinit(std.testing.allocator);
    try std.testing.expect(!accessory.dock_visible);

    const regular = try parseText(std.testing.allocator,
        \\.{
        \\  .id = "com.example.regular",
        \\  .name = "regular",
        \\  .version = "1.0.0",
        \\}
    );
    defer regular.deinit(std.testing.allocator);
    try std.testing.expect(regular.dock_visible);
}

test "manifest parser carries and validates registered-image budgets" {
    const metadata = try parseText(std.testing.allocator,
        \\.{
        \\  .id = "com.example.photos",
        \\  .name = "photos",
        \\  .version = "1.0.0",
        \\  .images = .{ .max_image_pixel_bytes = 8388608 },
        \\}
    );
    defer metadata.deinit(std.testing.allocator);
    try std.testing.expectEqual(@as(usize, 8 * 1024 * 1024), metadata.images.max_image_pixel_bytes);
    try app_manifest.validateImages(.{ .max_image_pixel_bytes = metadata.images.max_image_pixel_bytes });

    try std.testing.expectError(error.InvalidDimension, app_manifest.validateImages(.{ .max_image_pixel_bytes = 1048575 }));
    try std.testing.expectError(error.InvalidDimension, app_manifest.validateImages(.{ .max_image_pixel_bytes = 8388609 }));
}

test "manifest metadata parser carries model persistence configuration" {
    const metadata = try parseText(std.testing.allocator,
        \\.{
        \\  .id = "com.example.persisted",
        \\  .name = "persisted",
        \\  .version = "1.0.0",
        \\  .capabilities = .{ "persist" },
        \\  .persist = .{
        \\    .version = 3,
        \\    .debounce_ms = 250,
        \\    .restore = .{ .ok = "restored", .none = "fresh_boot", .err = "restore_failed" },
        \\  },
        \\}
    );
    defer metadata.deinit(std.testing.allocator);
    const persist = metadata.persist.?;
    try std.testing.expectEqual(@as(u64, 3), persist.version);
    try std.testing.expectEqual(@as(u32, 250), persist.debounce_ms);
    try std.testing.expectEqualStrings("restored", persist.restore.ok);
    try std.testing.expectEqualStrings("fresh_boot", persist.restore.none);
    try std.testing.expectEqualStrings("restore_failed", persist.restore.err);

    const capabilities = try parseCapabilities(std.testing.allocator, metadata.capabilities);
    defer std.testing.allocator.free(capabilities);
    try app_manifest.validatePersist(convertPersist(metadata.persist), capabilities);
    try std.testing.expectError(error.MissingRequiredField, app_manifest.validatePersist(convertPersist(metadata.persist), &.{}));
}

test "manifest metadata carries exact hash-pinned service packages" {
    const metadata = try parseText(std.testing.allocator,
        \\.{
        \\  .id = "com.example.services",
        \\  .name = "services",
        \\  .version = "1.0.0",
        \\  .service_packages = .{
        \\    .{ .name = "escape-string-regexp", .version = "5.0.0", .content_hash = "705f4bb4b92fd3469e264a93f2a2e4b24cf7e663d73a5318abaf29ee72674f6d" },
        \\    .{ .name = "@scope/parser", .version = "1.2.3", .content_hash = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" },
        \\  },
        \\}
    );
    defer metadata.deinit(std.testing.allocator);
    try std.testing.expect(validServicePackages(metadata.service_packages));
    try std.testing.expectEqual(@as(usize, 2), metadata.service_packages.len);
    try std.testing.expectEqualStrings("@scope/parser", metadata.service_packages[1].name);
    try std.testing.expect(!exactPackageVersion("1..2"));
    try std.testing.expect(!validNpmPackageName("package@range"));
}

test "manifest capability parser recognizes both shared SQLite tiers" {
    const values = [_][]const u8{ "store", "sqlite" };
    const capabilities = try parseCapabilities(std.testing.allocator, &values);
    defer std.testing.allocator.free(capabilities);
    try std.testing.expectEqual(app_manifest.CapabilityKind.store, capabilities[0].kind());
    try std.testing.expectEqual(app_manifest.CapabilityKind.sqlite, capabilities[1].kind());
}

test "manifest metadata parser reads structured security policy" {
    const metadata = try parseText(std.testing.allocator,
        \\.{
        \\  .id = "com.example.app",
        \\  .name = "example",
        \\  .version = "1.2.3",
        \\  .permissions = .{ "window", "filesystem", "credentials" },
        \\  .bridge = .{
        \\    .commands = .{
        \\      .{ .name = "native.ping", .permissions = .{ "filesystem" }, .origins = .{ "zero://app" } },
        \\    },
        \\  },
        \\  .security = .{
        \\    .navigation = .{
        \\      .allowed_origins = .{ "zero://app", "http://127.0.0.1:5173" },
        \\      .external_links = .{
        \\        .action = "open_system_browser",
        \\        .allowed_urls = .{ "https://example.com/*" },
        \\      },
        \\    },
        \\  },
        \\}
    );
    defer metadata.deinit(std.testing.allocator);

    try std.testing.expectEqualStrings("window", metadata.permissions[0]);
    try std.testing.expectEqualStrings("credentials", metadata.permissions[2]);
    try std.testing.expectEqualStrings("native.ping", metadata.bridge_commands[0].name);
    try std.testing.expectEqualStrings("filesystem", metadata.bridge_commands[0].permissions[0]);
    try std.testing.expectEqualStrings("zero://app", metadata.bridge_commands[0].origins[0]);
    try std.testing.expectEqualStrings("http://127.0.0.1:5173", metadata.security.navigation.allowed_origins[1]);
    try std.testing.expectEqualStrings("open_system_browser", metadata.security.navigation.external_links.action);
    try std.testing.expectEqualStrings("https://example.com/*", metadata.security.navigation.external_links.allowed_urls[0]);
}

test "manifest metadata parser reads declared platform chrome" {
    const metadata = try parseText(std.testing.allocator,
        \\.{
        \\  .id = "com.example.app",
        \\  .name = "example",
        \\  .version = "1.2.3",
        \\  .shell = .{
        \\    .chrome = .{
        \\      .tabs = .{
        \\        .{ .id = "tabs.home", .label = "Home", .icon = "menu" },
        \\        .{ .id = "tabs.settings", .label = "Settings", .icon = "settings" },
        \\      },
        \\      .primary_action = .{ .id = "action.new", .label = "New", .icon = "plus" },
        \\    },
        \\    .windows = .{
        \\      .{
        \\        .label = "main",
        \\        .views = .{
        \\          .{ .label = "content", .kind = "webview", .url = "zero://app/index.html", .fill = true },
        \\        },
        \\      },
        \\    },
        \\  },
        \\}
    );
    defer metadata.deinit(std.testing.allocator);

    try std.testing.expectEqual(@as(usize, 2), metadata.shell.chrome.tabs.len);
    try std.testing.expectEqualStrings("tabs.home", metadata.shell.chrome.tabs[0].id);
    try std.testing.expectEqualStrings("Home", metadata.shell.chrome.tabs[0].label);
    try std.testing.expectEqualStrings("menu", metadata.shell.chrome.tabs[0].icon);
    try std.testing.expectEqualStrings("action.new", metadata.shell.chrome.primary_action.?.id);

    // The parsed shell carries the declaration through to the shared
    // manifest validation, which accepts it whole.
    const shell = try parseShell(std.testing.allocator, metadata.shell);
    defer deinitParsedShell(std.testing.allocator, shell);
    try std.testing.expectEqual(@as(usize, 2), shell.chrome.tabs.len);
    try std.testing.expectEqualStrings("tabs.settings", shell.chrome.tabs[1].id);
    try std.testing.expectEqualStrings("plus", shell.chrome.primary_action.?.icon);
    try app_manifest.validateShellChrome(shell.chrome);
}

test "manifest metadata parser reads shell windows and views" {
    const metadata = try parseText(std.testing.allocator,
        \\.{
        \\  .id = "com.example.app",
        \\  .name = "example",
        \\  .version = "1.2.3",
        \\  .shell = .{
        \\    .windows = .{
        \\      .{
        \\        .label = "main",
        \\        .title = "Example",
        \\        .width = 1100,
        \\        .height = 760,
        \\        .restore_policy = "center_on_primary",
        \\        .views = .{
        \\          .{ .label = "toolbar", .kind = "toolbar", .edge = "top", .height = 44, .role = "toolbar" },
        \\          .{ .label = "content", .kind = "webview", .url = "zero://app/index.html", .fill = true, .min_width = 640, .min_height = 400, .max_width = 1440, .max_height = 900 },
        \\          .{ .label = "status", .kind = "statusbar", .edge = "bottom", .height = 24, .text = "Ready" },
        \\          .{ .label = "toolbar-stack", .kind = "stack", .parent = "toolbar", .axis = "column" },
        \\          .{ .label = "refresh-icon", .kind = "icon_button", .parent = "toolbar", .text = "R", .command = "app.refresh.icon" },
        \\          .{ .label = "save", .kind = "button", .parent = "toolbar", .accessibility_label = "Save document", .text = "Save", .command = "app.save" },
        \\          .{ .label = "mode", .kind = "segmented_control", .parent = "toolbar", .text = "List|Grid", .command = "app.view.mode" },
        \\          .{ .label = "syncing", .kind = "progress_indicator", .parent = "toolbar", .role = "Syncing" },
        \\          .{ .label = "nav-row", .kind = "list_item", .parent = "toolbar-stack", .text = "Inbox", .command = "app.open.inbox" },
        \\          .{ .label = "canvas", .kind = "gpu_surface", .gpu_backend = "metal", .gpu_pixel_format = "bgra8_unorm", .gpu_present_mode = "timer", .gpu_alpha_mode = "opaque", .gpu_color_space = "srgb", .gpu_vsync = true },
        \\        },
        \\      },
        \\    },
        \\  },
        \\}
    );
    defer metadata.deinit(std.testing.allocator);

    try std.testing.expectEqualStrings("main", metadata.shell.windows[0].label);
    try std.testing.expectEqualStrings("center_on_primary", metadata.shell.windows[0].restore_policy);
    try std.testing.expectEqualStrings("toolbar", metadata.shell.windows[0].views[0].kind);
    try std.testing.expectEqualStrings("zero://app/index.html", metadata.shell.windows[0].views[1].url.?);
    try std.testing.expect(metadata.shell.windows[0].views[1].fill);
    try std.testing.expectEqual(@as(?f32, 640), metadata.shell.windows[0].views[1].min_width);
    try std.testing.expectEqual(@as(?f32, 400), metadata.shell.windows[0].views[1].min_height);
    try std.testing.expectEqual(@as(?f32, 1440), metadata.shell.windows[0].views[1].max_width);
    try std.testing.expectEqual(@as(?f32, 900), metadata.shell.windows[0].views[1].max_height);
    try std.testing.expectEqualStrings("stack", metadata.shell.windows[0].views[3].kind);
    try std.testing.expectEqualStrings("column", metadata.shell.windows[0].views[3].axis.?);
    try std.testing.expectEqualStrings("icon_button", metadata.shell.windows[0].views[4].kind);
    try std.testing.expectEqualStrings("app.save", metadata.shell.windows[0].views[5].command.?);
    try std.testing.expectEqualStrings("Save document", metadata.shell.windows[0].views[5].accessibility_label.?);
    try std.testing.expectEqualStrings("segmented_control", metadata.shell.windows[0].views[6].kind);
    try std.testing.expectEqualStrings("progress_indicator", metadata.shell.windows[0].views[7].kind);
    try std.testing.expectEqualStrings("list_item", metadata.shell.windows[0].views[8].kind);
    try std.testing.expectEqualStrings("gpu_surface", metadata.shell.windows[0].views[9].kind);
    try std.testing.expectEqualStrings("metal", metadata.shell.windows[0].views[9].gpu_backend.?);
    try std.testing.expectEqualStrings("bgra8_unorm", metadata.shell.windows[0].views[9].gpu_pixel_format.?);
    try std.testing.expectEqualStrings("timer", metadata.shell.windows[0].views[9].gpu_present_mode.?);
    try std.testing.expectEqualStrings("opaque", metadata.shell.windows[0].views[9].gpu_alpha_mode.?);
    try std.testing.expectEqualStrings("srgb", metadata.shell.windows[0].views[9].gpu_color_space.?);
    try std.testing.expect(metadata.shell.windows[0].views[9].gpu_vsync.?);

    const shell = try parseShell(std.testing.allocator, metadata.shell);
    defer deinitParsedShell(std.testing.allocator, shell);
    try std.testing.expectEqual(app_manifest.ViewKind.webview, shell.windows[0].views[1].kind);
    try std.testing.expectEqual(@as(?f32, 640), shell.windows[0].views[1].min_width);
    try std.testing.expectEqual(@as(?f32, 400), shell.windows[0].views[1].min_height);
    try std.testing.expectEqual(@as(?f32, 1440), shell.windows[0].views[1].max_width);
    try std.testing.expectEqual(@as(?f32, 900), shell.windows[0].views[1].max_height);
    try std.testing.expectEqual(app_manifest.ViewKind.stack, shell.windows[0].views[3].kind);
    try std.testing.expectEqual(app_manifest.ShellAxis.column, shell.windows[0].views[3].axis.?);
    try std.testing.expectEqual(app_manifest.ViewKind.icon_button, shell.windows[0].views[4].kind);
    try std.testing.expectEqualStrings("Save document", shell.windows[0].views[5].accessibility_label.?);
    try std.testing.expectEqual(app_manifest.ViewKind.segmented_control, shell.windows[0].views[6].kind);
    try std.testing.expectEqual(app_manifest.ViewKind.progress_indicator, shell.windows[0].views[7].kind);
    try std.testing.expectEqual(app_manifest.ViewKind.list_item, shell.windows[0].views[8].kind);
    try std.testing.expectEqual(app_manifest.ViewKind.gpu_surface, shell.windows[0].views[9].kind);
    try std.testing.expectEqual(app_manifest.GpuSurfaceBackend.metal, shell.windows[0].views[9].gpu_backend.?);
    try std.testing.expectEqual(app_manifest.GpuSurfacePixelFormat.bgra8_unorm, shell.windows[0].views[9].gpu_pixel_format.?);
    try std.testing.expectEqual(app_manifest.GpuSurfacePresentMode.timer, shell.windows[0].views[9].gpu_present_mode.?);
    try std.testing.expectEqual(app_manifest.GpuSurfaceAlphaMode.@"opaque", shell.windows[0].views[9].gpu_alpha_mode.?);
    try std.testing.expectEqual(app_manifest.GpuSurfaceColorSpace.srgb, shell.windows[0].views[9].gpu_color_space.?);
    try std.testing.expect(shell.windows[0].views[9].gpu_vsync.?);
    try std.testing.expectEqual(app_manifest.ShellEdge.top, shell.windows[0].views[0].edge.?);
    try app_manifest.validateManifest(.{
        .identity = .{ .id = metadata.id, .name = metadata.name },
        .version = try parseVersion(metadata.version),
        .shell = shell,
    });
}

test "manifest parser reads window titlebar styles" {
    const metadata = try parseText(std.testing.allocator,
        \\.{
        \\  .id = "com.example.app",
        \\  .name = "example",
        \\  .version = "1.2.3",
        \\  .windows = .{
        \\    .{ .label = "main", .resizable = false, .titlebar = "hidden_inset", .transparent = true, .always_on_top = true, .click_through = true, .activate_on_show = false, .initially_hidden = true, .allows_fullscreen = false },
        \\    .{ .label = "tall", .titlebar = "hidden_inset_tall" },
        \\    .{ .label = "skinned", .titlebar = "chromeless" },
        \\  },
        \\  .shell = .{
        \\    .windows = .{
        \\      .{ .label = "scene", .titlebar = "hidden_inset_tall", .transparent = true, .always_on_top = true, .click_through = true, .activate_on_show = false, .initially_hidden = true, .allows_fullscreen = false, .views = .{ .{ .label = "content", .kind = "webview", .url = "zero://app/index.html" } } },
        \\    },
        \\  },
        \\}
    );
    defer metadata.deinit(std.testing.allocator);

    try std.testing.expectEqualStrings("hidden_inset", metadata.windows[0].titlebar);
    try std.testing.expect(!metadata.windows[0].resizable);
    try std.testing.expect(metadata.windows[0].transparent);
    try std.testing.expect(metadata.windows[0].always_on_top);
    try std.testing.expect(metadata.windows[0].click_through);
    try std.testing.expect(!metadata.windows[0].activate_on_show);
    try std.testing.expect(metadata.windows[0].initially_hidden);
    try std.testing.expect(!metadata.windows[0].allows_fullscreen);
    try std.testing.expectEqualStrings("hidden_inset_tall", metadata.windows[1].titlebar);
    try std.testing.expectEqualStrings("chromeless", metadata.windows[2].titlebar);
    try std.testing.expectEqualStrings("hidden_inset_tall", metadata.shell.windows[0].titlebar);

    const windows = try convertWindows(std.testing.allocator, metadata.windows);
    defer std.testing.allocator.free(windows);
    try std.testing.expectEqual(app_manifest.WindowTitlebarStyle.hidden_inset, windows[0].titlebar);
    try std.testing.expect(!windows[0].resizable);
    try std.testing.expect(windows[0].transparent);
    try std.testing.expect(windows[0].always_on_top);
    try std.testing.expect(windows[0].click_through);
    try std.testing.expect(!windows[0].activate_on_show);
    try std.testing.expect(windows[0].initially_hidden);
    try std.testing.expect(!windows[0].allows_fullscreen);
    try std.testing.expectEqual(app_manifest.WindowTitlebarStyle.hidden_inset_tall, windows[1].titlebar);
    try std.testing.expectEqual(app_manifest.WindowTitlebarStyle.chromeless, windows[2].titlebar);

    const shell = try parseShell(std.testing.allocator, metadata.shell);
    defer deinitParsedShell(std.testing.allocator, shell);
    try std.testing.expectEqual(app_manifest.WindowTitlebarStyle.hidden_inset_tall, shell.windows[0].titlebar);
    try std.testing.expect(shell.windows[0].transparent);
    try std.testing.expect(shell.windows[0].always_on_top);
    try std.testing.expect(shell.windows[0].click_through);
    try std.testing.expect(!shell.windows[0].activate_on_show);
    try std.testing.expect(shell.windows[0].initially_hidden);
    try std.testing.expect(!shell.windows[0].allows_fullscreen);
}

test "manifest parser reads window min sizes" {
    const metadata = try parseText(std.testing.allocator,
        \\.{
        \\  .id = "com.example.app",
        \\  .name = "example",
        \\  .version = "1.2.3",
        \\  .windows = .{
        \\    .{ .label = "main", .min_width = 596, .min_height = 420 },
        \\  },
        \\  .shell = .{
        \\    .windows = .{
        \\      .{ .label = "scene", .min_width = 596, .min_height = 420, .views = .{ .{ .label = "content", .kind = "webview", .url = "zero://app/index.html" } } },
        \\    },
        \\  },
        \\}
    );
    defer metadata.deinit(std.testing.allocator);

    try std.testing.expectEqual(@as(f32, 596), metadata.windows[0].min_width);
    try std.testing.expectEqual(@as(f32, 420), metadata.windows[0].min_height);
    try std.testing.expectEqual(@as(f32, 596), metadata.shell.windows[0].min_width);

    const windows = try convertWindows(std.testing.allocator, metadata.windows);
    defer std.testing.allocator.free(windows);
    try std.testing.expectEqual(@as(f32, 596), windows[0].min_width);
    try std.testing.expectEqual(@as(f32, 420), windows[0].min_height);

    const shell = try parseShell(std.testing.allocator, metadata.shell);
    defer deinitParsedShell(std.testing.allocator, shell);
    try std.testing.expectEqual(@as(f32, 596), shell.windows[0].min_width);
    try std.testing.expectEqual(@as(f32, 420), shell.windows[0].min_height);
}

test "manifest parser rejects negative window min sizes" {
    const metadata = try parseText(std.testing.allocator,
        \\.{
        \\  .id = "com.example.app",
        \\  .name = "example",
        \\  .version = "1.2.3",
        \\  .windows = .{
        \\    .{ .label = "main", .min_width = -1 },
        \\  },
        \\  .shell = .{
        \\    .windows = .{
        \\      .{ .label = "scene", .min_height = -20, .views = .{ .{ .label = "content", .kind = "webview", .url = "zero://app/index.html" } } },
        \\    },
        \\  },
        \\}
    );
    defer metadata.deinit(std.testing.allocator);

    try std.testing.expectError(error.InvalidWindowMinSize, convertWindows(std.testing.allocator, metadata.windows));
    try std.testing.expectError(error.InvalidWindowMinSize, parseShell(std.testing.allocator, metadata.shell));
}

test "manifest parser reads window close policies" {
    const metadata = try parseText(std.testing.allocator,
        \\.{
        \\  .id = "com.example.app",
        \\  .name = "example",
        \\  .version = "1.2.3",
        \\  .windows = .{
        \\    .{ .label = "main", .close_policy = "hide" },
        \\    .{ .label = "doc" },
        \\  },
        \\  .shell = .{
        \\    .windows = .{
        \\      .{ .label = "scene", .close_policy = "hide", .views = .{ .{ .label = "content", .kind = "webview", .url = "zero://app/index.html" } } },
        \\    },
        \\  },
        \\}
    );
    defer metadata.deinit(std.testing.allocator);

    try std.testing.expectEqualStrings("hide", metadata.windows[0].close_policy);
    try std.testing.expectEqualStrings("quit", metadata.windows[1].close_policy);
    try std.testing.expectEqualStrings("hide", metadata.shell.windows[0].close_policy);

    const windows = try convertWindows(std.testing.allocator, metadata.windows);
    defer std.testing.allocator.free(windows);
    try std.testing.expectEqual(app_manifest.WindowClosePolicy.hide, windows[0].close_policy);
    // Undeclared stays the .quit default — behavior unchanged for
    // every existing app.
    try std.testing.expectEqual(app_manifest.WindowClosePolicy.quit, windows[1].close_policy);

    const shell = try parseShell(std.testing.allocator, metadata.shell);
    defer deinitParsedShell(std.testing.allocator, shell);
    try std.testing.expectEqual(app_manifest.WindowClosePolicy.hide, shell.windows[0].close_policy);
}

test "manifest parser reads the core-compiler setting and defaults it to external" {
    const metadata = try parseText(std.testing.allocator,
        \\.{
        \\  .id = "com.example.app",
        \\  .name = "example",
        \\  .version = "1.2.3",
        \\  .core_compiler = "external",
        \\}
    );
    defer metadata.deinit(std.testing.allocator);
    try std.testing.expectEqualStrings("external", metadata.core_compiler);

    // Undeclared is the external lane — the one lane there is.
    const defaulted = try parseText(std.testing.allocator,
        \\.{ .id = "com.example.app", .name = "example", .version = "1.2.3" }
    );
    defer defaulted.deinit(std.testing.allocator);
    try std.testing.expectEqualStrings("external", defaulted.core_compiler);
}

test "manifest parser rejects unknown window close policy" {
    const metadata = try parseText(std.testing.allocator,
        \\.{
        \\  .id = "com.example.app",
        \\  .name = "example",
        \\  .version = "1.2.3",
        \\  .windows = .{
        \\    .{ .label = "main", .close_policy = "minimize" },
        \\  },
        \\  .shell = .{
        \\    .windows = .{
        \\      .{ .label = "scene", .close_policy = "event", .views = .{ .{ .label = "content", .kind = "webview", .url = "zero://app/index.html" } } },
        \\    },
        \\  },
        \\}
    );
    defer metadata.deinit(std.testing.allocator);

    // "event" is the deliberately reserved future tier (model-decides
    // close); it parses as unknown until it ships — staged honestly,
    // never accepted early.
    try std.testing.expectError(error.InvalidWindowClosePolicy, convertWindows(std.testing.allocator, metadata.windows));
    try std.testing.expectError(error.InvalidWindowClosePolicy, parseShell(std.testing.allocator, metadata.shell));
}

test "manifest parser rejects unknown window titlebar style" {
    const metadata = try parseText(std.testing.allocator,
        \\.{
        \\  .id = "com.example.app",
        \\  .name = "example",
        \\  .version = "1.2.3",
        \\  .windows = .{
        \\    .{ .label = "main", .titlebar = "transparent" },
        \\  },
        \\  .shell = .{
        \\    .windows = .{
        \\      .{ .label = "scene", .titlebar = "frameless", .views = .{ .{ .label = "content", .kind = "webview", .url = "zero://app/index.html" } } },
        \\    },
        \\  },
        \\}
    );
    defer metadata.deinit(std.testing.allocator);

    try std.testing.expectError(error.InvalidWindowTitlebarStyle, convertWindows(std.testing.allocator, metadata.windows));
    try std.testing.expectError(error.InvalidWindowTitlebarStyle, parseShell(std.testing.allocator, metadata.shell));
}

test "manifest parser rejects invalid shell view kind" {
    const metadata = try parseText(std.testing.allocator,
        \\.{
        \\  .id = "com.example.app",
        \\  .name = "example",
        \\  .version = "1.2.3",
        \\  .shell = .{
        \\    .windows = .{
        \\      .{ .views = .{ .{ .label = "content", .kind = "unknown", .url = "zero://app/index.html" } } },
        \\    },
        \\  },
        \\}
    );
    defer metadata.deinit(std.testing.allocator);

    try std.testing.expectError(error.InvalidViewKind, parseShell(std.testing.allocator, metadata.shell));
}

test "manifest parser rejects duplicate compatibility and shell window labels" {
    const metadata = try parseText(std.testing.allocator,
        \\.{
        \\  .id = "com.example.app",
        \\  .name = "example",
        \\  .version = "1.2.3",
        \\  .windows = .{
        \\    .{ .label = "main" },
        \\  },
        \\  .shell = .{
        \\    .windows = .{
        \\      .{ .label = "main", .views = .{ .{ .label = "content", .kind = "webview", .url = "zero://app/index.html" } } },
        \\    },
        \\  },
        \\}
    );
    defer metadata.deinit(std.testing.allocator);

    const windows = try convertWindows(std.testing.allocator, metadata.windows);
    defer std.testing.allocator.free(windows);
    const shell = try parseShell(std.testing.allocator, metadata.shell);
    defer deinitParsedShell(std.testing.allocator, shell);

    try std.testing.expectError(error.DuplicateWindow, app_manifest.validateManifest(.{
        .identity = .{ .id = metadata.id, .name = metadata.name },
        .version = try parseVersion(metadata.version),
        .windows = windows,
        .shell = shell,
    }));
}

test "manifest metadata parser reads frontend config" {
    const metadata = try parseText(std.testing.allocator,
        \\.{
        \\  .id = "com.example.app",
        \\  .name = "example",
        \\  .version = "1.2.3",
        \\  .frontend = .{
        \\    .dist = "frontend/dist",
        \\    .entry = "index.html",
        \\    .spa_fallback = false,
        \\    .dev = .{
        \\      .url = "http://127.0.0.1:5173/",
        \\      .command = .{ "npm", "run", "dev" },
        \\      .ready_path = "/health",
        \\      .timeout_ms = 12000,
        \\    },
        \\  },
        \\}
    );
    defer metadata.deinit(std.testing.allocator);

    try std.testing.expectEqualStrings("frontend/dist", metadata.frontend.?.dist);
    try std.testing.expectEqual(false, metadata.frontend.?.spa_fallback);
    try std.testing.expectEqualStrings("http://127.0.0.1:5173/", metadata.frontend.?.dev.?.url);
    try std.testing.expectEqualStrings("npm", metadata.frontend.?.dev.?.command[0]);
    try std.testing.expectEqual(@as(u32, 12000), metadata.frontend.?.dev.?.timeout_ms);
}

test "validate surfaces the non-square icon teaching error with dimensions" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const gpa = arena.allocator();
    var cwd = std.Io.Dir.cwd();
    const root = ".zig-cache/test-validate-icon-nonsquare";
    try cwd.deleteTree(std.testing.io, root);
    defer cwd.deleteTree(std.testing.io, root) catch {};
    try cwd.createDirPath(std.testing.io, root ++ "/assets");

    // A 6x4 white PNG source.
    const pixels = try gpa.alloc(u8, 6 * 4 * 4);
    @memset(pixels, 255);
    const encoded = try app_icon_tool.encodePng(gpa, pixels, 6, 4);
    try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/assets/icon.png", .data = encoded });
    try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/app.zon", .data =
        \\.{
        \\  .id = "dev.example.app",
        \\  .name = "demo",
        \\  .version = "1.0.0",
        \\  .icons = .{"assets/icon.png"},
        \\}
    });

    const result = try validateFile(gpa, std.testing.io, root ++ "/app.zon");
    try std.testing.expect(!result.ok);
    try std.testing.expect(std.mem.indexOf(u8, result.message, "6x4") != null);
    try std.testing.expect(std.mem.indexOf(u8, result.message, "square") != null);
}

test "validate rejects unsupported icon extensions naming the accepted forms" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const gpa = arena.allocator();
    var cwd = std.Io.Dir.cwd();
    const root = ".zig-cache/test-validate-icon-ext";
    try cwd.deleteTree(std.testing.io, root);
    defer cwd.deleteTree(std.testing.io, root) catch {};
    try cwd.createDirPath(std.testing.io, root);
    try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/app.zon", .data =
        \\.{
        \\  .id = "dev.example.app",
        \\  .name = "demo",
        \\  .version = "1.0.0",
        \\  .icons = .{"assets/icon.jpg"},
        \\}
    });

    const result = try validateFile(gpa, std.testing.io, root ++ "/app.zon");
    try std.testing.expect(!result.ok);
    try std.testing.expect(std.mem.indexOf(u8, result.message, ".png") != null);
    try std.testing.expect(std.mem.indexOf(u8, result.message, ".svg") != null);
}

test "validate reports an unreadable icon source naming the accepted forms" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const gpa = arena.allocator();
    var cwd = std.Io.Dir.cwd();
    const root = ".zig-cache/test-validate-icon-bad";
    try cwd.deleteTree(std.testing.io, root);
    defer cwd.deleteTree(std.testing.io, root) catch {};
    try cwd.createDirPath(std.testing.io, root ++ "/assets");
    try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/assets/icon.png", .data = "this is not a png" });
    try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/app.zon", .data =
        \\.{
        \\  .id = "dev.example.app",
        \\  .name = "demo",
        \\  .version = "1.0.0",
        \\  .icons = .{"assets/icon.png"},
        \\}
    });

    const result = try validateFile(gpa, std.testing.io, root ++ "/app.zon");
    try std.testing.expect(!result.ok);
    try std.testing.expect(std.mem.indexOf(u8, result.message, "could not be read") != null);
    try std.testing.expect(std.mem.indexOf(u8, result.message, ".png") != null);
    try std.testing.expect(std.mem.indexOf(u8, result.message, ".svg") != null);
}

test "validate accepts a square icon source and prebuilt containers" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const gpa = arena.allocator();
    var cwd = std.Io.Dir.cwd();
    const root = ".zig-cache/test-validate-icon-ok";
    try cwd.deleteTree(std.testing.io, root);
    defer cwd.deleteTree(std.testing.io, root) catch {};
    try cwd.createDirPath(std.testing.io, root ++ "/assets");

    const pixels = try gpa.alloc(u8, 600 * 600 * 4);
    @memset(pixels, 128);
    const encoded = try app_icon_tool.encodePng(gpa, pixels, 600, 600);
    try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/assets/icon.png", .data = encoded });
    try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/app.zon", .data =
        \\.{
        \\  .id = "dev.example.app",
        \\  .name = "demo",
        \\  .version = "1.0.0",
        \\  .icons = .{ "assets/icon.png", "assets/prebuilt.icns", "assets/prebuilt.ico" },
        \\}
    });

    const result = try validateFile(gpa, std.testing.io, root ++ "/app.zon");
    try std.testing.expect(result.ok);
}

test "web layer inference is declare-to-use over parsed metadata" {
    // Nothing declared: native-only.
    const canvas_capabilities = [_][]const u8{ "native_views", "gpu_surfaces" };
    const canvas: Metadata = .{ .id = "dev.example.canvas", .name = "canvas", .version = "1.0.0", .capabilities = &canvas_capabilities };
    const canvas_layer = try webLayerFromManifest(canvas);
    try std.testing.expect(!canvas_layer.enabled);
    try std.testing.expectEqual(WebLayerReason.inferred_native_only, canvas_layer.reason);

    // Each web declaration flips the inference.
    const webview_capabilities = [_][]const u8{"webview"};
    const by_capability = try webLayerFromManifest(.{ .id = "dev.example.a", .name = "a", .version = "1.0.0", .capabilities = &webview_capabilities });
    try std.testing.expect(by_capability.enabled);
    try std.testing.expectEqual(WebLayerReason.capability, by_capability.reason);

    const by_frontend = try webLayerFromManifest(.{ .id = "dev.example.b", .name = "b", .version = "1.0.0", .frontend = .{} });
    try std.testing.expect(by_frontend.enabled);
    try std.testing.expectEqual(WebLayerReason.frontend, by_frontend.reason);

    const shell_views = [_]ShellViewMetadata{.{ .label = "content", .kind = "webview", .url = "zero://app/index.html" }};
    const shell_windows = [_]ShellWindowMetadata{.{ .label = "main", .views = &shell_views }};
    const by_shell = try webLayerFromManifest(.{ .id = "dev.example.c", .name = "c", .version = "1.0.0", .shell = .{ .windows = &shell_windows } });
    try std.testing.expect(by_shell.enabled);
    try std.testing.expectEqual(WebLayerReason.shell_webview, by_shell.reason);

    // The Chromium engine is web intent; the system default alone is not.
    const by_chromium = try webLayerFromManifest(.{ .id = "dev.example.d", .name = "d", .version = "1.0.0", .web_engine = "chromium" });
    try std.testing.expect(by_chromium.enabled);
    try std.testing.expectEqual(WebLayerReason.chromium_engine, by_chromium.reason);

    // The RESOLVED engine decides, not the raw manifest value: a system
    // manifest packaged with `--web-engine chromium` ships the layer.
    const by_resolved_chromium = try webLayer(.{ .id = "dev.example.i", .name = "i", .version = "1.0.0" }, .chromium);
    try std.testing.expect(by_resolved_chromium.enabled);
    try std.testing.expectEqual(WebLayerReason.chromium_engine, by_resolved_chromium.reason);

    // Explicit overrides.
    const included = try webLayerFromManifest(.{ .id = "dev.example.e", .name = "e", .version = "1.0.0", .webview_layer = "include" });
    try std.testing.expect(included.enabled);
    const excluded = try webLayerFromManifest(.{ .id = "dev.example.f", .name = "f", .version = "1.0.0", .webview_layer = "exclude" });
    try std.testing.expect(!excluded.enabled);
    try std.testing.expectEqual(WebLayerReason.declared_exclude, excluded.reason);

    // Contradictions and typos are refused, never resolved silently —
    // including an exclude against a flag-resolved Chromium engine.
    try std.testing.expectError(error.WebViewLayerConflict, webLayerFromManifest(.{ .id = "dev.example.g", .name = "g", .version = "1.0.0", .capabilities = &webview_capabilities, .webview_layer = "exclude" }));
    try std.testing.expectError(error.WebViewLayerConflict, webLayer(.{ .id = "dev.example.j", .name = "j", .version = "1.0.0", .webview_layer = "exclude" }, .chromium));
    try std.testing.expectError(error.InvalidWebViewLayer, webLayerFromManifest(.{ .id = "dev.example.h", .name = "h", .version = "1.0.0", .webview_layer = "never" }));
}

test "web layer --web-layer flag beats the manifest field like -Dweb-layer does" {
    const canvas_capabilities = [_][]const u8{ "native_views", "gpu_surfaces" };
    const canvas: Metadata = .{ .id = "dev.example.canvas", .name = "canvas", .version = "1.0.0", .capabilities = &canvas_capabilities };
    const webview_capabilities = [_][]const u8{"webview"};
    const web: Metadata = .{ .id = "dev.example.web", .name = "web", .version = "1.0.0", .capabilities = &webview_capabilities };

    // An include flag that changes the outcome names itself as the cause.
    const forced_in = try webLayerResolved(canvas, .system, .include);
    try std.testing.expect(forced_in.enabled);
    try std.testing.expectEqual(WebLayerReason.declared_include, forced_in.reason);
    try std.testing.expectEqualStrings("declared: --web-layer include", forced_in.sourceText());

    // A flag that merely confirms the inference keeps the manifest's own
    // richer reason, so graph-forwarded packages report like hand-run ones.
    const confirmed = try webLayerResolved(web, .system, .include);
    try std.testing.expect(confirmed.enabled);
    try std.testing.expectEqual(WebLayerReason.capability, confirmed.reason);
    try std.testing.expectEqualStrings("declared: capabilities", confirmed.sourceText());
    const confirmed_off = try webLayerResolved(canvas, .system, .exclude);
    try std.testing.expect(!confirmed_off.enabled);
    try std.testing.expectEqual(WebLayerReason.inferred_native_only, confirmed_off.reason);

    // An exclude flag against a web declaration is the same refused
    // conflict as a manifest exclude — including a resolved Chromium engine.
    try std.testing.expectError(error.WebViewLayerConflict, webLayerResolved(web, .system, .exclude));
    try std.testing.expectError(error.WebViewLayerConflict, webLayerResolved(canvas, .chromium, .exclude));

    // `--web-layer auto` overrides a manifest exclude back to inference,
    // exactly as `-Dweb-layer=auto` does in the build graph.
    const reopened = try webLayerResolved(.{ .id = "dev.example.k", .name = "k", .version = "1.0.0", .capabilities = &webview_capabilities, .webview_layer = "exclude" }, .system, .auto);
    try std.testing.expect(reopened.enabled);
    try std.testing.expectEqual(WebLayerReason.capability, reopened.reason);

    // No flag: identical to `webLayer` (the manifest field decides).
    const plain = try webLayerResolved(canvas, .system, null);
    try std.testing.expect(!plain.enabled);
    try std.testing.expectEqual(WebLayerReason.inferred_native_only, plain.reason);

    // The flag parser shares the contract's vocabulary.
    try std.testing.expectEqual(WebViewLayerSetting.include, parseWebViewLayerSetting("include").?);
    try std.testing.expectEqual(WebViewLayerSetting.exclude, parseWebViewLayerSetting("exclude").?);
    try std.testing.expectEqual(WebViewLayerSetting.auto, parseWebViewLayerSetting("auto").?);
    try std.testing.expectEqual(null, parseWebViewLayerSetting("never"));
}

test "validate rejects a web-declaring manifest that excludes the web layer" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const gpa = arena.allocator();
    var cwd = std.Io.Dir.cwd();
    const root = ".zig-cache/test-validate-web-layer-conflict";
    try cwd.deleteTree(std.testing.io, root);
    defer cwd.deleteTree(std.testing.io, root) catch {};
    try cwd.createDirPath(std.testing.io, root);
    try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/app.zon", .data =
        \\.{
        \\  .id = "dev.example.app",
        \\  .name = "demo",
        \\  .version = "1.0.0",
        \\  .capabilities = .{"webview"},
        \\  .webview_layer = "exclude",
        \\}
    });

    const result = try validateFile(gpa, std.testing.io, root ++ "/app.zon");
    try std.testing.expect(!result.ok);
    try std.testing.expect(std.mem.indexOf(u8, result.message, "webview_layer = \"exclude\"") != null);
    try std.testing.expect(std.mem.indexOf(u8, result.message, "declares web content") != null);

    // A native-only manifest with the same exclude is valid.
    try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/app.zon", .data =
        \\.{
        \\  .id = "dev.example.app",
        \\  .name = "demo",
        \\  .version = "1.0.0",
        \\  .capabilities = .{"gpu_surfaces"},
        \\  .webview_layer = "exclude",
        \\}
    });
    const native_only = try validateFile(gpa, std.testing.io, root ++ "/app.zon");
    try std.testing.expect(native_only.ok);

    // A typo in the setting is its own teaching error.
    try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/app.zon", .data =
        \\.{
        \\  .id = "dev.example.app",
        \\  .name = "demo",
        \\  .version = "1.0.0",
        \\  .webview_layer = "never",
        \\}
    });
    const invalid = try validateFile(gpa, std.testing.io, root ++ "/app.zon");
    try std.testing.expect(!invalid.ok);
    try std.testing.expect(std.mem.indexOf(u8, invalid.message, "webview_layer is invalid") != null);
}

test "validate rejects accessory startup without a tray capability" {
    var cwd = std.Io.Dir.cwd();
    const root = ".zig-cache/test-validate-accessory-tray";
    try cwd.deleteTree(std.testing.io, root);
    defer cwd.deleteTree(std.testing.io, root) catch {};
    try cwd.createDirPath(std.testing.io, root);
    try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/app.zon", .data =
        \\.{
        \\  .id = "dev.example.accessory",
        \\  .name = "accessory",
        \\  .version = "1.0.0",
        \\  .dock_visible = false,
        \\}
    });

    const invalid = try validateFile(std.testing.allocator, std.testing.io, root ++ "/app.zon");
    try std.testing.expect(!invalid.ok);
    try std.testing.expect(std.mem.indexOf(u8, invalid.message, "dock_visible = false requires the \"tray\" capability") != null);

    try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/app.zon", .data =
        \\.{
        \\  .id = "dev.example.accessory",
        \\  .name = "accessory",
        \\  .version = "1.0.0",
        \\  .capabilities = .{"tray"},
        \\  .dock_visible = false,
        \\}
    });
    const valid = try validateFile(std.testing.allocator, std.testing.io, root ++ "/app.zon");
    try std.testing.expect(valid.ok);
}

test "manifest validates the theme pack name" {
    const metadata = try parseText(std.testing.allocator,
        \\.{
        \\  .id = "com.example.app",
        \\  .name = "example",
        \\  .version = "1.0.0",
        \\  .theme = "geist",
        \\}
    );
    defer metadata.deinit(std.testing.allocator);
    try std.testing.expectEqualStrings("geist", metadata.theme.?);
    // The known-pack check is the tooling half of the contract; the
    // runner re-validates the same names at comptime in the app build.
    try std.testing.expect(isKnownThemePack("house"));
    try std.testing.expect(isKnownThemePack("geist"));
    try std.testing.expect(!isKnownThemePack("neon"));
}

test "manifest validates the theme accent hex color" {
    const metadata = try parseText(std.testing.allocator,
        \\.{
        \\  .id = "com.example.app",
        \\  .name = "example",
        \\  .version = "1.0.0",
        \\  .theme = "geist",
        \\  .theme_accent = "#df2670",
        \\}
    );
    defer metadata.deinit(std.testing.allocator);
    try std.testing.expectEqualStrings("#df2670", metadata.theme_accent.?);
    // The hex-shape check is the tooling half of the contract; the
    // runner re-parses the same shape at comptime in the app build.
    try std.testing.expect(isHexColor("#df2670"));
    try std.testing.expect(isHexColor("#000000"));
    try std.testing.expect(!isHexColor("df2670"));
    try std.testing.expect(!isHexColor("#df267"));
    try std.testing.expect(!isHexColor("#df26700"));
    try std.testing.expect(!isHexColor("#df267g"));
}

test "DMG settings default to a complete drag-to-Applications layout" {
    const metadata = try parseText(std.testing.allocator,
        \\.{
        \\  .id = "com.example.app",
        \\  .name = "example",
        \\  .version = "1.0.0",
        \\}
    );
    defer metadata.deinit(std.testing.allocator);
    try validateDmgSettings(metadata.dmg);
    try std.testing.expectEqual(@as(u16, 660), metadata.dmg.window_width);
    try std.testing.expectEqual(@as(u16, 400), metadata.dmg.window_height);
    try std.testing.expectEqual(@as(u16, 128), metadata.dmg.icon_size);
    try std.testing.expectEqual(@as(u16, 166), metadata.dmg.app_position.x);
    try std.testing.expectEqual(@as(u16, 182), metadata.dmg.app_position.y);
    try std.testing.expectEqual(@as(u16, 486), metadata.dmg.applications_position.x);
    try std.testing.expectEqual(@as(u16, 182), metadata.dmg.applications_position.y);
    try std.testing.expect(metadata.dmg.applications_link);
    try std.testing.expect(metadata.dmg.background == null);
    try std.testing.expectEqual(@as(usize, 0), metadata.dmg.items.len);
}

test "manifest validation does not apply an implicit DMG volume name" {
    var cwd = std.Io.Dir.cwd();
    const root = ".zig-cache/test-manifest-dmg-implicit-volume";
    try cwd.deleteTree(std.testing.io, root);
    defer cwd.deleteTree(std.testing.io, root) catch {};
    try cwd.createDirPath(std.testing.io, root);
    try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/app.zon", .data =
        \\.{
        \\  .id = "com.example.app",
        \\  .name = "example",
        \\  .display_name = "Example: Studio",
        \\  .version = "1.0.0",
        \\}
    });

    const result = try validateFile(std.testing.allocator, std.testing.io, root ++ "/app.zon");
    try std.testing.expect(result.ok);
}

test "DMG settings reject unsafe paths and out-of-window positions" {
    try std.testing.expectError(error.InvalidPath, validateDmgSettings(.{ .background = "../outside.png" }));
    try std.testing.expectError(error.InvalidDmgBackground, validateDmgSettings(.{ .background = "assets/background.svg" }));
    try std.testing.expectError(error.InvalidDmgPosition, validateDmgSettings(.{ .app_position = .{ .x = 660, .y = 190 } }));
    try std.testing.expectError(error.InvalidDmgVolumeName, validateDmgSettings(.{ .volume_name = "Bad/Name" }));
    try validateDmgSettings(.{ .window_width = 400, .applications_link = false });
    try validateDmgSettings(.{ .background = "assets/background.tiff" });
}

test "DMG package settings validate effective volume and app bundle names" {
    var maximum_volume: [127]u8 = @splat('v');
    try validateDmgPackageSettings(.{
        .id = "dev.example.demo",
        .name = "demo",
        .display_name = &maximum_volume,
        .version = "1.0.0",
    });

    var oversized_volume: [128]u8 = @splat('v');
    try std.testing.expectError(error.InvalidDmgVolumeName, validateDmgPackageSettings(.{
        .id = "dev.example.demo",
        .name = "demo",
        .display_name = &oversized_volume,
        .version = "1.0.0",
    }));

    var maximum_stem: [251]u8 = @splat('a');
    const maximum_items = [_]DmgItemMetadata{
        .{ .kind = .app, .name = &maximum_stem, .position = .{ .x = 170, .y = 182 } },
    };
    try validateDmgPackageSettings(.{
        .id = "dev.example.demo",
        .name = "demo",
        .version = "1.0.0",
        .dmg = .{ .items = &maximum_items },
    });

    var oversized_stem: [252]u8 = @splat('a');
    const oversized_items = [_]DmgItemMetadata{
        .{ .kind = .app, .name = &oversized_stem, .position = .{ .x = 170, .y = 182 } },
    };
    try std.testing.expectError(error.InvalidDmgItem, validateDmgPackageSettings(.{
        .id = "dev.example.demo",
        .name = "demo",
        .version = "1.0.0",
        .dmg = .{ .items = &oversized_items },
    }));

    const conflicting_items = [_]DmgItemMetadata{
        .{ .kind = .app, .position = .{ .x = 170, .y = 182 } },
        .{ .kind = .file, .path = "docs/demo.app", .name = "Demo.app", .position = .{ .x = 490, .y = 182 } },
    };
    try std.testing.expectError(error.DuplicateDmgItem, validateDmgPackageSettings(.{
        .id = "dev.example.demo",
        .name = "demo",
        .display_name = "Demo",
        .version = "1.0.0",
        .dmg = .{ .items = &conflicting_items },
    }));
}

test "DMG background readers validate JPEG and TIFF envelopes" {
    const jpeg =
        "\xff\xd8" ++
        "\xff\xc0\x00\x11\x08\x01\x90\x02\x94\x03\x01\x11\x00\x02\x11\x00\x03\x11\x00" ++
        "\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00\x3f\x00" ++
        "\x00\xff\xd9";
    const jpeg_dimensions = jpegDimensions(jpeg) orelse return error.TestUnexpectedResult;
    try std.testing.expectEqual(@as(usize, 660), jpeg_dimensions.width);
    try std.testing.expectEqual(@as(usize, 400), jpeg_dimensions.height);
    try std.testing.expect(jpegDimensions(jpeg[0 .. jpeg.len - 2]) == null);

    // A complete 1x1 baseline TIFF: nine IFD entries followed by one strip
    // byte. This makes the positive fixture a usable image, not merely a
    // header carrying width and height tags.
    var tiff: [123]u8 = @splat(0);
    tiff[0] = 'I';
    tiff[1] = 'I';
    std.mem.writeInt(u16, tiff[2..4], 42, .little);
    std.mem.writeInt(u32, tiff[4..8], 8, .little);
    std.mem.writeInt(u16, tiff[8..10], 9, .little);
    const entries = [_][4]u32{
        .{ 256, 4, 1, 1 }, // ImageWidth
        .{ 257, 4, 1, 1 }, // ImageLength
        .{ 258, 3, 1, 8 }, // BitsPerSample
        .{ 259, 3, 1, 1 }, // Compression: none
        .{ 262, 3, 1, 1 }, // PhotometricInterpretation: black is zero
        .{ 273, 4, 1, 122 }, // StripOffsets
        .{ 277, 3, 1, 1 }, // SamplesPerPixel
        .{ 278, 4, 1, 1 }, // RowsPerStrip
        .{ 279, 4, 1, 1 }, // StripByteCounts
    };
    for (entries, 0..) |entry, index| {
        const offset = 10 + index * 12;
        std.mem.writeInt(u16, tiff[offset..][0..2], @intCast(entry[0]), .little);
        std.mem.writeInt(u16, tiff[offset + 2 ..][0..2], @intCast(entry[1]), .little);
        std.mem.writeInt(u32, tiff[offset + 4 ..][0..4], entry[2], .little);
        std.mem.writeInt(u32, tiff[offset + 8 ..][0..4], entry[3], .little);
    }
    tiff[122] = 0;
    const tiff_dimensions = tiffDimensions(&tiff) orelse return error.TestUnexpectedResult;
    try std.testing.expectEqual(@as(usize, 1), tiff_dimensions.width);
    try std.testing.expectEqual(@as(usize, 1), tiff_dimensions.height);
    try std.testing.expect(tiffDimensions(tiff[0..122]) == null);
}

test "TIFF background reader validates indirect strips and BigTIFF payloads" {
    var multi_strip_tiff: [80]u8 = @splat(0);
    multi_strip_tiff[0] = 'I';
    multi_strip_tiff[1] = 'I';
    std.mem.writeInt(u16, multi_strip_tiff[2..4], 42, .little);
    std.mem.writeInt(u32, multi_strip_tiff[4..8], 8, .little);
    std.mem.writeInt(u16, multi_strip_tiff[8..10], 4, .little);
    const classic_entries = [_][4]u32{
        .{ 256, 4, 1, 1 }, // ImageWidth
        .{ 257, 4, 1, 2 }, // ImageLength
        .{ 273, 4, 2, 62 }, // StripOffsets array
        .{ 279, 4, 2, 70 }, // StripByteCounts array
    };
    for (classic_entries, 0..) |entry, index| {
        const offset = 10 + index * 12;
        std.mem.writeInt(u16, multi_strip_tiff[offset..][0..2], @intCast(entry[0]), .little);
        std.mem.writeInt(u16, multi_strip_tiff[offset + 2 ..][0..2], @intCast(entry[1]), .little);
        std.mem.writeInt(u32, multi_strip_tiff[offset + 4 ..][0..4], entry[2], .little);
        std.mem.writeInt(u32, multi_strip_tiff[offset + 8 ..][0..4], entry[3], .little);
    }
    std.mem.writeInt(u32, multi_strip_tiff[62..66], 78, .little);
    std.mem.writeInt(u32, multi_strip_tiff[66..70], 79, .little);
    std.mem.writeInt(u32, multi_strip_tiff[70..74], 1, .little);
    std.mem.writeInt(u32, multi_strip_tiff[74..78], 1, .little);
    const multi_strip_dimensions = tiffDimensions(&multi_strip_tiff) orelse return error.TestUnexpectedResult;
    try std.testing.expectEqual(@as(usize, 1), multi_strip_dimensions.width);
    try std.testing.expectEqual(@as(usize, 2), multi_strip_dimensions.height);
    try std.testing.expect(tiffDimensions(multi_strip_tiff[0..79]) == null);

    var big_tiff: [113]u8 = @splat(0);
    big_tiff[0] = 'I';
    big_tiff[1] = 'I';
    std.mem.writeInt(u16, big_tiff[2..4], 43, .little);
    std.mem.writeInt(u16, big_tiff[4..6], 8, .little);
    std.mem.writeInt(u64, big_tiff[8..16], 16, .little);
    std.mem.writeInt(u64, big_tiff[16..24], 4, .little);
    const big_entries = [_][4]u64{
        .{ 256, 16, 1, 1 }, // ImageWidth
        .{ 257, 16, 1, 1 }, // ImageLength
        .{ 273, 16, 1, 112 }, // StripOffsets
        .{ 279, 16, 1, 1 }, // StripByteCounts
    };
    for (big_entries, 0..) |entry, index| {
        const offset = 24 + index * 20;
        std.mem.writeInt(u16, big_tiff[offset..][0..2], @intCast(entry[0]), .little);
        std.mem.writeInt(u16, big_tiff[offset + 2 ..][0..2], @intCast(entry[1]), .little);
        std.mem.writeInt(u64, big_tiff[offset + 4 ..][0..8], entry[2], .little);
        std.mem.writeInt(u64, big_tiff[offset + 12 ..][0..8], entry[3], .little);
    }
    const big_dimensions = tiffDimensions(&big_tiff) orelse return error.TestUnexpectedResult;
    try std.testing.expectEqual(@as(usize, 1), big_dimensions.width);
    try std.testing.expectEqual(@as(usize, 1), big_dimensions.height);
    try std.testing.expect(tiffDimensions(big_tiff[0..112]) == null);
}

test "DMG explicit items select and position Finder contents" {
    const metadata = try parseText(std.testing.allocator,
        \\.{
        \\  .id = "com.example.app",
        \\  .name = "example",
        \\  .version = "1.0.0",
        \\  .dmg = .{
        \\    .items = .{
        \\      .{ .kind = "app", .position = .{ .x = 150, .y = 170 } },
        \\      .{ .kind = "applications", .position = .{ .x = 510, .y = 170 } },
        \\      .{ .kind = "file", .path = "docs/README.pdf", .name = "Read Me.pdf", .position = .{ .x = 260, .y = 330 } },
        \\      .{ .kind = "link", .path = "/Library/QuickLook", .name = "QuickLook", .position = .{ .x = 400, .y = 330 } },
        \\    },
        \\  },
        \\}
    );
    defer metadata.deinit(std.testing.allocator);
    try validateDmgSettings(metadata.dmg);
    try std.testing.expectEqual(@as(usize, 4), metadata.dmg.items.len);
    try std.testing.expectEqual(DmgItemKind.app, metadata.dmg.items[0].kind);
    try std.testing.expectEqual(DmgItemKind.file, metadata.dmg.items[2].kind);
    try std.testing.expectEqualStrings("Read Me.pdf", dmgItemDestinationName(metadata.dmg.items[2]).?);
    try std.testing.expectEqualStrings("QuickLook", dmgItemDestinationName(metadata.dmg.items[3]).?);
}

test "DMG explicit items require one app and reject unsafe or duplicate destinations" {
    const applications_only = [_]DmgItemMetadata{
        .{ .kind = .applications, .position = .{ .x = 490, .y = 182 } },
    };
    try std.testing.expectError(error.InvalidDmgItem, validateDmgSettings(.{ .items = &applications_only }));

    const duplicate_applications = [_]DmgItemMetadata{
        .{ .kind = .app, .position = .{ .x = 170, .y = 182 } },
        .{ .kind = .applications, .position = .{ .x = 490, .y = 182 } },
        .{ .kind = .link, .path = "/Applications", .name = "applications", .position = .{ .x = 330, .y = 320 } },
    };
    try std.testing.expectError(error.DuplicateDmgItem, validateDmgSettings(.{ .items = &duplicate_applications }));

    const unsafe_file = [_]DmgItemMetadata{
        .{ .kind = .app, .position = .{ .x = 170, .y = 182 } },
        .{ .kind = .file, .path = "../README.pdf", .position = .{ .x = 490, .y = 182 } },
    };
    try std.testing.expectError(error.InvalidPath, validateDmgSettings(.{ .items = &unsafe_file }));

    const relative_link = [_]DmgItemMetadata{
        .{ .kind = .app, .position = .{ .x = 170, .y = 182 } },
        .{ .kind = .link, .path = "Library/QuickLook", .position = .{ .x = 490, .y = 182 } },
    };
    try std.testing.expectError(error.InvalidDmgItem, validateDmgSettings(.{ .items = &relative_link }));
}

test "manifest validation resolves a custom DMG background beside app.zon" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const gpa = arena.allocator();
    var cwd = std.Io.Dir.cwd();
    const root = ".zig-cache/test-manifest-dmg-background";
    try cwd.deleteTree(std.testing.io, root);
    defer cwd.deleteTree(std.testing.io, root) catch {};
    try cwd.createDirPath(std.testing.io, root ++ "/art");
    const pixels = try gpa.alloc(u8, 320 * 240 * 4);
    @memset(pixels, 255);
    const encoded = try app_icon_tool.encodePng(gpa, pixels, 320, 240);
    try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/art/installer.png", .data = encoded });
    try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/app.zon", .data =
        \\.{
        \\  .id = "com.example.app",
        \\  .name = "example",
        \\  .version = "1.0.0",
        \\  .dmg = .{ .background = "art/installer.png", .window_width = 320, .window_height = 240, .applications_link = false },
        \\}
    });

    const valid = try validateFile(gpa, std.testing.io, root ++ "/app.zon");
    try std.testing.expect(valid.ok);

    const retina_pixels = try gpa.alloc(u8, 640 * 480 * 4);
    @memset(retina_pixels, 255);
    const retina_encoded = try app_icon_tool.encodePng(gpa, retina_pixels, 640, 480);
    try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/art/installer@2x.png", .data = retina_encoded });
    const retina_valid = try validateFile(gpa, std.testing.io, root ++ "/app.zon");
    try std.testing.expect(retina_valid.ok);

    const wrong_retina = try app_icon_tool.encodePng(gpa, retina_pixels[0 .. 640 * 479 * 4], 640, 479);
    try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/art/installer@2x.png", .data = wrong_retina });
    const retina_mismatch = try validateFile(gpa, std.testing.io, root ++ "/app.zon");
    try std.testing.expect(!retina_mismatch.ok);
    try std.testing.expect(std.mem.indexOf(u8, retina_mismatch.message, "@2x background is 640x479") != null);
    try cwd.deleteFile(std.testing.io, root ++ "/art/installer@2x.png");

    try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/art/installer.png", .data = "not a png" });
    const malformed = try validateFile(gpa, std.testing.io, root ++ "/app.zon");
    try std.testing.expect(!malformed.ok);
    try std.testing.expect(std.mem.indexOf(u8, malformed.message, "not a structurally valid") != null);

    try cwd.deleteFile(std.testing.io, root ++ "/art/installer.png");
    const missing = try validateFile(gpa, std.testing.io, root ++ "/app.zon");
    try std.testing.expect(!missing.ok);
    try std.testing.expect(std.mem.indexOf(u8, missing.message, "dmg background could not be read") != null);
}

test "manifest validation resolves explicit DMG file items beside app.zon" {
    var cwd = std.Io.Dir.cwd();
    const root = ".zig-cache/test-manifest-dmg-items";
    try cwd.deleteTree(std.testing.io, root);
    defer cwd.deleteTree(std.testing.io, root) catch {};
    try cwd.createDirPath(std.testing.io, root ++ "/docs");
    try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/docs/README.pdf", .data = "fixture" });
    try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/app.zon", .data =
        \\.{
        \\  .id = "com.example.app",
        \\  .name = "example",
        \\  .version = "1.0.0",
        \\  .dmg = .{ .items = .{
        \\    .{ .kind = "app", .position = .{ .x = 170, .y = 182 } },
        \\    .{ .kind = "file", .path = "docs/README.pdf", .position = .{ .x = 490, .y = 182 } },
        \\  } },
        \\}
    });

    const valid = try validateFile(std.testing.allocator, std.testing.io, root ++ "/app.zon");
    try std.testing.expect(valid.ok);
    try cwd.deleteFile(std.testing.io, root ++ "/docs/README.pdf");
    const missing = try validateFile(std.testing.allocator, std.testing.io, root ++ "/app.zon");
    try std.testing.expect(!missing.ok);
    try std.testing.expect(std.mem.indexOf(u8, missing.message, "dmg item source could not be read") != null);
}
