//! iPXE script generator.
//!
//! ## Menu hierarchy (per Phase 2 spec)
//!
//! ```text
//! Top level:
//!   Default
//!     > Boot from Local HDD
//!   Installers
//!     > Linux Installers   -> submenu of Linux ISOs
//!     > Windows Installers -> submenu of Windows ISOs  (controlled by Settings::windows_enabled)
//!   Tools
//!     > Utilities          -> memtest, etc. (embedded assets only)
//!     > OpenPXE Shell     -> drop to iPXE shell with branded prompt
//!     > Network Card Info  -> ifstat / config / route dump
//!   Queued Deployment       -> join the deployment queue
//! ```
//!
//! ## iPXE is entirely backend — users do not see or write iPXE
//!
//! All user-facing knobs live in `Settings`. Script generation translates
//! those knobs into iPXE primitives (chain, menu, item, choose, etc.).
//! There is intentionally no UI path to upload a custom `.ipxe` script.

use openpxe_core::{Settings, TimeoutAction};
use openpxe_iso_store::introspect::DistroFamily;
use openpxe_iso_store::{BootEntry, BootKind, IsoMeta};
use std::fmt::Write as _;

/// Top-level OpenPXE boot menu. Serialized identically for BIOS and UEFI
/// clients because iPXE normalises the menu primitives across firmwares.
///
/// v0.4.69: rendered with an iVentoy-style graphical frame — a
/// `console --picture` directive paints a full-screen PNG background
/// (the operator's uploaded logo on a dark field, or the default
/// OpenPXE mark) with the menu text overlaid below a reserved top
/// margin, plus a footer carrying version + arch + firmware kind. On
/// iPXE binaries built with `IMAGE_PNG` + `CONSOLE_FRAMEBUFFER` (our
/// x86_64 UEFI binaries, compiled from source) the background paints;
/// on binaries without PNG support the `|| console` fallback yields a
/// clean text menu. The old ASCII wordmark has been removed.
#[must_use]
pub fn render_menu(isos: &[IsoMeta], settings: &Settings, base_url: &str) -> String {
    let mut s = String::new();
    let base = base_url.trim_end_matches('/');
    let timeout_ms = settings.boot_menu_timeout_secs.saturating_mul(1000);
    let default_item = match settings.timeout_action {
        TimeoutAction::QueuedDeployment => "queue",
        // Stay -> iPXE's `--timeout 0` is "no timeout". Pick any default
        // label; the client waits for keypress. We use the same label as
        // LocalHdd to keep the menu's pre-highlight stable.
        TimeoutAction::LocalHdd | TimeoutAction::Stay => "local",
    };

    let _ = writeln!(s, "#!ipxe");
    let _ = writeln!(s, "# OpenPXE top-level menu - auto-generated, do not edit");
    let _ = writeln!(s, "set base-url {base}");
    let _ = writeln!(s, "set esc:hex 1b");
    let _ = writeln!(s, "set cls ${{esc:string}}[2J");
    // v0.4.69: graphical background. `/branding/pxe-logo` always
    // returns a full-screen 1024×768 PNG now — the operator's logo on a
    // dark field, or a default OpenPXE mark when none is uploaded. The
    // `--top 290` reserves the top band (where the logo paints) so the
    // menu text lands below it.
    //
    // v0.5.7: gate the whole command behind `iseq ${platform} efi`.
    // `console --picture` needs IMAGE_PNG + CONSOLE_FRAMEBUFFER, which
    // only our from-source UEFI binaries carry (x86_64/arm64 UEFI — see
    // deploy/docker/Dockerfile). The fetched BIOS `undionly.kpxe` has
    // neither, and on legacy BIOS the `--picture` attempt misbehaves
    // *before* the trailing `|| console` fallback can recover (it tries
    // to set a framebuffer mode the BIOS console can't honour). Guarding
    // on platform means BIOS clients never issue the command at all —
    // they drop straight to the plain text menu — while UEFI clients
    // still get the graphical background. A PNG-less UEFI build (e.g. the
    // upstream i386-efi baseline) still falls back gracefully through the
    // same `|| console`. No operator toggle needed; mixed BIOS+UEFI
    // fleets each get the right treatment automatically.
    let _ = writeln!(
        s,
        "iseq ${{platform}} efi && console --picture {base}/branding/pxe-logo --top 290 || console"
    );
    // Map iPXE's ${{buildarch}} + ${{platform}} into the human form the
    // user asked for (e.g. "x86 BIOS", "x86_64 UEFI", "arm64 UEFI").
    // iPXE evaluates `iseq` lazily, so we only set whichever line
    // matches. Anything not on the allowlist falls through to a generic
    // `<buildarch> <platform>` display.
    let _ = writeln!(s, "set arch-label ${{buildarch}} ${{platform}}");
    let _ = writeln!(
        s,
        "iseq ${{buildarch}} i386   && iseq ${{platform}} pcbios && set arch-label x86 BIOS         || iseq ${{buildarch}} x86_64 && iseq ${{platform}} efi    && set arch-label x86_64 UEFI      || iseq ${{buildarch}} arm64  && iseq ${{platform}} efi    && set arch-label arm64 UEFI       || true"
    );
    let _ = writeln!(s, ":menu");
    let _ = writeln!(s, "menu OpenPXE - network boot menu");
    // v0.4.69: the ASCII wordmark is gone — the graphical background
    // (set via `console --picture` above) carries the branding now.
    let _ = writeln!(s, "item --gap");
    let _ = writeln!(
        s,
        "item --gap -- ------------------------- Default -------------------------"
    );
    let _ = writeln!(s, "item local            Boot from Local HDD");
    let _ = writeln!(
        s,
        "item --gap -- ----------------------- Installers -----------------------"
    );
    if has_family(isos, is_linux_family) {
        let _ = writeln!(s, "item linux            Linux Installers  >");
    } else {
        let _ = writeln!(s, "item --gap -- (no Linux ISOs uploaded)");
    }
    // v0.5.8: Windows just works — no Settings toggle. Show the Windows
    // installers submenu whenever a Windows ISO is present; entries boot
    // via HTTP sanboot of the raw ISO, so no SMB/extraction is required.
    if has_family(isos, is_windows_family) {
        let _ = writeln!(s, "item windows          Windows Installers  >");
    } else {
        let _ = writeln!(s, "item --gap -- (no Windows ISOs uploaded)");
    }
    let _ = writeln!(
        s,
        "item --gap -- -------------------------- Tools --------------------------"
    );
    let _ = writeln!(s, "item tools            Tools  >");
    let _ = writeln!(
        s,
        "item --gap -- ---------------------- Queued Deployment ---------------------"
    );
    let _ = writeln!(s, "item queue            Queued Deployment  (join queue)");
    let _ = writeln!(s, "item --gap");
    let _ = writeln!(s, "item --key x exit     Exit iPXE");
    // v0.4.6 footer line. Sits just above the `choose` line so it's
    // always visible regardless of how the menu paginates. iPXE
    // interpolates `${arch-label}` (set near the top of this script)
    // and `${version}` is the binary-baked iPXE version — *not* the
    // OpenPXE version — so we hard-code the OpenPXE version string
    // here.
    let openpxe_version = env!("CARGO_PKG_VERSION");
    let _ = writeln!(s, "item --gap");
    let _ = writeln!(
        s,
        "item --gap --                 OpenPXE v{openpxe_version}  -  ${{arch-label}}"
    );

    if matches!(settings.timeout_action, TimeoutAction::Stay) {
        let _ = writeln!(s, "choose --default {default_item} target || goto menu");
    } else {
        let _ = writeln!(
            s,
            "choose --default {default_item} --timeout {timeout_ms} target || goto menu"
        );
    }

    // iPXE's `||` is strict about what follows. Each test uses `goto menu`
    // as the fallthrough target so the parser never sees a bare `||` with
    // trailing whitespace — some iPXE builds reject that.
    let _ = writeln!(
        s,
        "iseq ${{target}} local   && chain {base}/boot/_local.ipxe        || goto menu"
    );
    let _ = writeln!(
        s,
        "iseq ${{target}} linux   && chain {base}/boot/_linux_menu.ipxe   || goto menu"
    );
    let _ = writeln!(
        s,
        "iseq ${{target}} windows && chain {base}/boot/_windows_menu.ipxe || goto menu"
    );
    let _ = writeln!(
        s,
        "iseq ${{target}} tools   && chain {base}/boot/_tools_menu.ipxe   || goto menu"
    );
    let _ = writeln!(
        s,
        "iseq ${{target}} queue   && chain {base}/boot/_queue.ipxe         || goto menu"
    );
    let _ = writeln!(
        s,
        "iseq ${{target}} exit    && exit                                 || goto menu"
    );
    let _ = writeln!(s, "goto menu");
    s
}

/// Per-family submenu (Linux or Windows). Each item shows the ISO size
/// in MiB, iVentoy-style (`[ 4376 MB] ubuntu-22.04-desktop-amd64`).
#[must_use]
pub fn render_family_menu(isos: &[IsoMeta], base_url: &str, is_windows: bool) -> String {
    let base = base_url.trim_end_matches('/');
    let title = if is_windows {
        "Windows Installers"
    } else {
        "Linux Installers"
    };
    let label = if is_windows { "windows" } else { "linux" };
    let mut s = String::new();
    let _ = writeln!(s, "#!ipxe");
    let _ = writeln!(s, "set base-url {base}");
    let _ = writeln!(s, ":menu");
    let _ = writeln!(s, "menu OpenPXE - {title}");
    let filter: fn(DistroFamily) -> bool = if is_windows {
        is_windows_family
    } else {
        is_linux_family
    };
    let mut count = 0;
    for iso in isos {
        if !filter(iso.introspection.family) {
            continue;
        }
        // v0.4.4: ISOs the operator flipped to the Tools category move
        // out of the OS installer submenus entirely — they only appear
        // under Tools. Without this filter the operator would see the
        // same ISO in both menus.
        if matches!(iso.category, openpxe_iso_store::IsoCategory::Tools) {
            continue;
        }
        for entry in &iso.boot_entries {
            let size_label = fmt_size_mib(iso.size_bytes);
            let key = hotkey_for_index(count);
            // Visual hint: a leading `*` marks password-protected entries.
            // ASCII only — iPXE's menu console mangles non-ASCII on some
            // firmwares.
            let lock = if iso.is_password_protected() {
                "*"
            } else {
                " "
            };
            let _ = writeln!(
                s,
                "item {}{} {}[{:>6}] {}",
                key,
                entry.id,
                lock,
                size_label,
                escape_label(&entry.title),
            );
            count += 1;
        }
    }
    if count == 0 {
        let _ = writeln!(s, "item --gap -- (no {label} images uploaded yet)");
    }
    let _ = writeln!(s, "item --gap");
    let _ = writeln!(s, "item --key b back   < Back to main menu");
    let _ = writeln!(s, "choose target || goto menu");
    let _ = writeln!(
        s,
        "iseq ${{target}} back && chain {base}/boot.ipxe || goto menu"
    );
    // Pass `?mac=${mac}` so the per-entry handler can record the booting
    // client into the Host log. iPXE substitutes `${mac}` before
    // the HTTP fetch; if the firmware can't resolve it the literal
    // `${mac}` is sent and the server treats it as "unknown".
    let _ = writeln!(
        s,
        "chain {base}/boot/${{target}}.ipxe?mac=${{mac}} || goto menu"
    );
    s
}

/// Format a byte count as `NNNN MB` (iVentoy-style — MB not MiB, to match
/// operator expectations from the original tool).
fn fmt_size_mib(bytes: u64) -> String {
    let mib = bytes / (1024 * 1024);
    format!("{mib} MB")
}

/// Assign `--key N <id>` hotkeys 1..9, then nothing for positions >=9.
/// iPXE's menu needs the --key prefix as a separate token before the id.
fn hotkey_for_index(i: usize) -> String {
    if i < 9 {
        format!("--key {} ", i + 1)
    } else {
        String::new()
    }
}

/// Tools submenu — Utilities, Shell, NIC Info, Reboot, Exit to firmware,
/// plus any ISOs the operator flipped to [`IsoCategory::Tools`] in the
/// Storage tab. The category-Tools ISOs render first so frequently used
/// recovery / hardware tools are reachable with a single number key
/// before the built-in shortcuts.
#[must_use]
pub fn render_tools_menu(isos: &[IsoMeta], base_url: &str) -> String {
    let base = base_url.trim_end_matches('/');
    let mut s = String::new();
    let _ = writeln!(s, "#!ipxe");
    let _ = writeln!(s, "set base-url {base}");
    let _ = writeln!(s, ":menu");
    let _ = writeln!(s, "menu OpenPXE - Tools");

    // Operator-categorized tool ISOs (hotkeys 1..9), each chained the
    // same way as a per-family menu pick — through the boot-entry id
    // route, carrying `?mac=${mac}` for Host log attribution.
    let mut count = 0;
    for iso in isos {
        if !matches!(iso.category, openpxe_iso_store::IsoCategory::Tools) {
            continue;
        }
        for entry in &iso.boot_entries {
            let size_label = fmt_size_mib(iso.size_bytes);
            let key = hotkey_for_index(count);
            let lock = if iso.is_password_protected() {
                "*"
            } else {
                " "
            };
            let _ = writeln!(
                s,
                "item {}{} {}[{:>6}] {}",
                key,
                entry.id,
                lock,
                size_label,
                escape_label(&entry.title),
            );
            count += 1;
        }
    }
    if count > 0 {
        let _ = writeln!(s, "item --gap");
    }

    let _ = writeln!(s, "item --key u util     Utilities  (memtest, ...)");
    let _ = writeln!(s, "item --key s shell    OpenPXE Shell");
    let _ = writeln!(s, "item --key n nic      Network Card Info");
    let _ = writeln!(s, "item --gap");
    let _ = writeln!(s, "item --key r reboot   Reboot Computer");
    let _ = writeln!(s, "item --key e firmware Exit and continue BIOS boot");
    let _ = writeln!(s, "item --gap");
    let _ = writeln!(s, "item --key b back     < Back to main menu");
    let _ = writeln!(s, "choose target || goto menu");
    let _ = writeln!(
        s,
        "iseq ${{target}} util     && chain {base}/boot/_util.ipxe  || goto menu"
    );
    let _ = writeln!(
        s,
        "iseq ${{target}} shell    && chain {base}/boot/_shell.ipxe || goto menu"
    );
    let _ = writeln!(
        s,
        "iseq ${{target}} nic      && chain {base}/boot/_nic.ipxe   || goto menu"
    );
    let _ = writeln!(
        s,
        "iseq ${{target}} reboot   && reboot                        || goto menu"
    );
    let _ = writeln!(
        s,
        "iseq ${{target}} firmware && exit 0                        || goto menu"
    );
    let _ = writeln!(
        s,
        "iseq ${{target}} back     && chain {base}/boot.ipxe        || goto menu"
    );
    // Fall-through for category-Tools ISO ids — same as the family
    // submenu, carrying `?mac=${mac}` for the boot log.
    let _ = writeln!(
        s,
        "chain {base}/boot/${{target}}.ipxe?mac=${{mac}} || goto menu"
    );
    s
}

/// "Boot from Local HDD". On BIOS, we sanboot the first local drive; on
/// UEFI we `exit` so the firmware moves to the next boot entry (normally
/// the internal disk).
#[must_use]
pub fn render_local_hdd(base_url: &str) -> String {
    let base = base_url.trim_end_matches('/');
    let mut s = String::new();
    let _ = writeln!(s, "#!ipxe");
    let _ = writeln!(s, "# Boot from Local HDD - platform-sensitive");
    let _ = writeln!(
        s,
        "iseq ${{platform}} pcbios && sanboot --no-describe --drive 0x80 || goto uefi"
    );
    let _ = writeln!(s, ":uefi");
    let _ = writeln!(
        s,
        "# UEFI path: fall through to the firmware's next boot entry"
    );
    let _ = writeln!(s, "exit 0");
    let _ = writeln!(s, "# If the above exit returns, loop back to the main menu");
    let _ = writeln!(s, "chain {base}/boot.ipxe");
    s
}

/// Utilities submenu. For Phase 2 we bundle memtest86+ as an optional
/// asset (if absent, the item is listed but errors gracefully). No third-
/// party tools are fetched at runtime.
#[must_use]
pub fn render_util(base_url: &str) -> String {
    let base = base_url.trim_end_matches('/');
    let mut s = String::new();
    let _ = writeln!(s, "#!ipxe");
    let _ = writeln!(s, ":menu");
    let _ = writeln!(s, "menu OpenPXE - Utilities");
    let _ = writeln!(s, "item memtest      MemTest86+ (RAM diagnostic)");
    let _ = writeln!(s, "item --gap");
    let _ = writeln!(s, "item back         < Back");
    let _ = writeln!(s, "choose target || goto menu");
    let _ = writeln!(
        s,
        "iseq ${{target}} memtest && chain {base}/ipxe/memtest.bin || goto menu"
    );
    let _ = writeln!(
        s,
        "iseq ${{target}} back && chain {base}/boot/_tools_menu.ipxe || goto menu"
    );
    let _ = writeln!(s, "goto menu");
    s
}

/// "OpenPXE Shell" — iPXE shell, branded.
#[must_use]
pub fn render_shell(base_url: &str) -> String {
    let base = base_url.trim_end_matches('/');
    let mut s = String::new();
    let _ = writeln!(s, "#!ipxe");
    let _ = writeln!(s, "echo ==========================================");
    let _ = writeln!(s, "echo   OpenPXE Shell");
    let _ = writeln!(s, "echo   'exit' returns to the main menu");
    let _ = writeln!(s, "echo ==========================================");
    let _ = writeln!(s, "shell");
    let _ = writeln!(s, "chain {base}/boot.ipxe");
    s
}

/// "Network Card Info" — print ifstat + route + config.
#[must_use]
pub fn render_nic_info(base_url: &str) -> String {
    let base = base_url.trim_end_matches('/');
    let mut s = String::new();
    let _ = writeln!(s, "#!ipxe");
    let _ = writeln!(s, "echo ==========================================");
    let _ = writeln!(s, "echo   Network Card Info");
    let _ = writeln!(s, "echo ==========================================");
    let _ = writeln!(s, "ifstat");
    let _ = writeln!(s, "echo");
    let _ = writeln!(s, "route");
    let _ = writeln!(s, "echo");
    let _ = writeln!(s, "echo 'Press any key to return to menu'");
    let _ = writeln!(s, "prompt --timeout 30000");
    let _ = writeln!(s, "chain {base}/boot.ipxe");
    s
}

/// Queued Deployment entry point. Joins the queue, then enters a long-poll
/// loop (iPXE repeats the chain on 3xx redirects / HTTP errors until a
/// real script comes back).
#[must_use]
pub fn render_queue_entry(base_url: &str) -> String {
    let base = base_url.trim_end_matches('/');
    let mut s = String::new();
    let _ = writeln!(s, "#!ipxe");
    let _ = writeln!(
        s,
        "# Queued Deployment - join the queue and wait for operator"
    );
    let _ = writeln!(s, "echo Joining deployment queue...");
    // imgfetch writes the body to a file in iPXE's transient FS; we read
    // the queue entry id out of the Location-style header by asking the server
    // to put it in the response body as a single token.
    let _ = writeln!(s, "chain --replace {base}/api/queue/join?mac=${{mac}}");
    s
}

/// Per-entry boot script (same as Phase 1, with extra_kernel_args appended).
///
/// `unattended_args` (v0.5.2) carries the per-host unattended-install
/// kernel arguments (`inst.ks=…`, `auto=true … url=…`, or
/// `autoinstall ds=nocloud-net;s=…`) when the requesting MAC has a
/// deployment profile with an answer file selected. It's appended to the
/// Linux kernel command line after the operator's global extra args, and
/// ignored for Windows (wimboot) / sanboot entries which don't take a
/// kernel cmdline.
#[must_use]
pub fn render_entry(
    entry: &BootEntry,
    settings: &Settings,
    base_url: &str,
    unattended_args: Option<&str>,
) -> String {
    let mut s = String::new();
    let base = base_url.trim_end_matches('/');
    let _ = writeln!(s, "#!ipxe");
    let _ = writeln!(s, "set base-url {base}");
    match &entry.kind {
        BootKind::LinuxKernel {
            kernel_url,
            initrd_urls,
            args,
        } => {
            let mut cmdline = args.cmdline.replace("${base-url}", base);
            if !settings.extra_kernel_args.trim().is_empty() {
                cmdline.push(' ');
                cmdline.push_str(settings.extra_kernel_args.trim());
            }
            if let Some(extra) = unattended_args {
                if !extra.trim().is_empty() {
                    cmdline.push(' ');
                    cmdline.push_str(extra.trim());
                }
            }
            let _ = writeln!(s, "kernel {base}/{kernel_url} {cmdline}");
            for u in initrd_urls {
                let _ = writeln!(s, "initrd {base}/{u}");
            }
            let _ = writeln!(s, "boot || goto failed");
        }
        BootKind::Wimboot { wimboot_url, files } => {
            let _ = writeln!(s, "kernel {base}/{wimboot_url}");
            for (tag, url) in files {
                let _ = writeln!(s, "initrd --name {tag} {base}/{url} {tag}");
            }
            let _ = writeln!(s, "boot || goto failed");
        }
        BootKind::SanBootIso { iso_url } => {
            let _ = writeln!(s, "sanboot --no-describe {base}/{iso_url} || goto failed");
        }
    }
    let _ = writeln!(s, ":failed");
    let _ = writeln!(s, "echo Boot failed - returning to menu in 5s");
    let _ = writeln!(s, "sleep 5");
    let _ = writeln!(s, "chain {base}/boot.ipxe");
    s
}

fn is_linux_family(f: DistroFamily) -> bool {
    matches!(
        f,
        DistroFamily::DebianUbuntu
            | DistroFamily::RhelFedora
            | DistroFamily::OpenSuse
            | DistroFamily::Arch
            | DistroFamily::Alpine
            | DistroFamily::Unknown
    )
}

fn is_windows_family(f: DistroFamily) -> bool {
    matches!(f, DistroFamily::WindowsPe)
}

fn has_family(isos: &[IsoMeta], pred: fn(DistroFamily) -> bool) -> bool {
    isos.iter().any(|i| pred(i.introspection.family))
}

fn escape_label(s: &str) -> String {
    s.chars()
        .map(|c| match c {
            '\n' | '\r' => ' ',
            c => c,
        })
        .collect()
}

/// Render the password-prompt script for a protected boot entry.
///
/// Flow on the client:
///   1. iPXE clears any leftover ${password}, prints a banner naming the
///      ISO so the operator knows what they're being asked for.
///   2. `read --secret password` accepts input without echoing it to
///      the screen.
///   3. An empty input bails back to the main menu (lets the operator
///      back out of a misclick).
///   4. Otherwise the script chains the same /boot/<id>.ipxe URL but
///      with `?token=${password:uristring}`. iPXE's `:uristring`
///      modifier URL-encodes the value so `&`, `?`, `=`, spaces, etc.
///      survive transport.
///   5. The server replies with either the boot script (correct
///      password) or [`render_password_failed`] (wrong password). On
///      transport failure we fall back to the main menu.
#[must_use]
pub fn render_password_prompt(entry_id: &str, iso_filename: &str, base_url: &str) -> String {
    let base = base_url.trim_end_matches('/');
    let label = escape_label(iso_filename);
    let mut s = String::new();
    let _ = writeln!(s, "#!ipxe");
    let _ = writeln!(s, "# OpenPXE password prompt for {label}");
    let _ = writeln!(s, "echo");
    let _ = writeln!(s, "echo ==========================================");
    let _ = writeln!(s, "echo   This image requires a password");
    let _ = writeln!(s, "echo   {label}");
    let _ = writeln!(s, "echo   (enter alone returns to main menu)");
    let _ = writeln!(s, "echo ==========================================");
    let _ = writeln!(s, "set password ");
    let _ = writeln!(s, "read --secret password");
    let _ = writeln!(
        s,
        "iseq ${{password}} \"\" && chain {base}/boot.ipxe || goto submit"
    );
    let _ = writeln!(s, ":submit");
    let _ = writeln!(s, "echo Verifying...");
    // Carry `mac=${mac}` alongside the token so a successful unlock
    // records the actual client MAC into the Host log. On
    // older iPXE that can't resolve `${mac}` the server just stores it
    // as "unknown" rather than refusing to boot.
    let _ = writeln!(
        s,
        "chain {base}/boot/{entry_id}.ipxe?token=${{password:uristring}}&mac=${{mac}} \
         || chain {base}/boot.ipxe"
    );
    s
}

/// Render the "wrong password" script. Tells the operator, sleeps for
/// two seconds (gives the eye time to register the message and dampens
/// brute-force rate without help from the server), and chains back to
/// the same entry — which sends them through the prompt flow again.
#[must_use]
pub fn render_password_failed(entry_id: &str, base_url: &str) -> String {
    let base = base_url.trim_end_matches('/');
    let mut s = String::new();
    let _ = writeln!(s, "#!ipxe");
    let _ = writeln!(s, "echo");
    let _ = writeln!(s, "echo Wrong password.");
    let _ = writeln!(s, "sleep 2");
    let _ = writeln!(
        s,
        "chain {base}/boot/{entry_id}.ipxe || chain {base}/boot.ipxe"
    );
    s
}

#[cfg(test)]
mod password_tests {
    use super::*;

    #[test]
    fn prompt_uses_secret_read_and_uri_escape() {
        let s = render_password_prompt("alpha-linux", "Alpha Test.iso", "http://10.0.0.5");
        assert!(s.starts_with("#!ipxe\n"));
        assert!(s.contains("read --secret password"));
        assert!(s.contains("Alpha Test.iso"));
        // URI-string modifier on the var so passwords with `&`/spaces survive.
        assert!(s.contains("token=${password:uristring}"));
        // Empty enter sends back to the main menu, not back into the prompt
        // (avoids a wedged client if the operator chose by mistake).
        assert!(s.contains("&& chain http://10.0.0.5/boot.ipxe || goto submit"));
        // Never log/echo the value.
        assert!(!s.contains("echo ${password"));
    }

    #[test]
    fn failed_chains_back_to_entry() {
        let s = render_password_failed("alpha-linux", "http://10.0.0.5");
        assert!(s.contains("Wrong password."));
        // Re-target the entry so the prompt flow runs again.
        assert!(s.contains("chain http://10.0.0.5/boot/alpha-linux.ipxe"));
    }

    #[test]
    fn top_menu_has_polished_branding_and_arch_footer() {
        // v0.4.69: the menu emits a `console --picture` line that paints
        // a full-screen PNG background (the operator's logo, or the
        // default OpenPXE mark) reserving a top margin for it, then
        // falls back to a clean text console on iPXE builds without PNG
        // support. The ASCII wordmark is gone — the graphical
        // background carries the branding now. A single-line footer
        // still carries the OpenPXE version + arch.
        let settings = Settings::default();
        let s = render_menu(&[], &settings, "http://10.0.0.5");
        assert!(
            s.contains("console --picture http://10.0.0.5/branding/pxe-logo"),
            "missing console --picture line:\n{s}"
        );
        // The picture call reserves a top margin for the logo band.
        assert!(s.contains("--top 290"), "missing --top margin:\n{s}");
        // Picture-or-text-console must be a single statement so older
        // iPXE parsers don't choke on the chain.
        assert!(s.contains("|| console"), "missing graceful fallback:\n{s}");
        // The ASCII wordmark must be GONE — its removal is the whole
        // point of v0.4.69's graphical background.
        assert!(
            !s.contains("___                   ___  __  __ ___"),
            "ASCII banner should have been removed:\n{s}"
        );
        // Footer with version + arch interpolation. The version comes
        // from CARGO_PKG_VERSION at compile time.
        let version = env!("CARGO_PKG_VERSION");
        assert!(
            s.contains(&format!("OpenPXE v{version}")),
            "footer missing OpenPXE version:\n{s}"
        );
        assert!(
            s.contains("${arch-label}"),
            "footer missing arch-label interpolation:\n{s}"
        );
        // No website URL — the design brief calls that out as tacky.
        assert!(
            !s.to_ascii_lowercase().contains("openpxe.com"),
            "footer should not advertise the website:\n{s}"
        );
        // Arch-label mapping covers the three labels from the brief:
        // "x86 BIOS", "x86_64 UEFI", "arm64 UEFI".
        assert!(s.contains("x86 BIOS"), "{s}");
        assert!(s.contains("x86_64 UEFI"), "{s}");
        assert!(s.contains("arm64 UEFI"), "{s}");
    }

    // v0.5.4: a full snapshot of the rendered top menu. The fragment
    // `assert!`s above check specific invariants; this catches *any* other
    // drift (a reordered item, a dropped line, changed spacing) so it's
    // reviewed deliberately. The OpenPXE version is filtered out so the
    // snapshot doesn't churn on every release bump.
    #[test]
    fn render_menu_snapshot() {
        // Normalize the compile-time version so the snapshot doesn't churn
        // on every release bump (no insta `filters` feature needed).
        let rendered = render_menu(&[], &Settings::default(), "http://10.0.0.5").replace(
            concat!("OpenPXE v", env!("CARGO_PKG_VERSION")),
            "OpenPXE vX.Y.Z",
        );
        insta::assert_snapshot!(rendered);
    }

    #[test]
    fn generated_scripts_do_not_emit_bare_or_trailing_fallbacks() {
        let settings = Settings::default();
        let scripts = [
            render_menu(&[], &settings, "http://10.0.0.5"),
            render_tools_menu(&[], "http://10.0.0.5"),
            render_local_hdd("http://10.0.0.5"),
            render_util("http://10.0.0.5"),
            render_shell("http://10.0.0.5"),
            render_nic_info("http://10.0.0.5"),
            render_queue_entry("http://10.0.0.5"),
            render_password_failed("alpha-linux", "http://10.0.0.5"),
        ];
        for script in scripts {
            for line in script.lines() {
                assert!(
                    !line.trim_end().ends_with("||"),
                    "bare iPXE fallback operator in line: {line}\nscript:\n{script}"
                );
            }
        }
    }
}
