nut_shell/shell/
mod.rs

1//! Shell orchestration and command processing.
2//!
3//! Brings together I/O, command trees, authentication, and history into interactive CLI.
4//! Follows unified architecture: single code path handles both auth-enabled and auth-disabled modes.
5//! Lifecycle: `Inactive` → `activate()` → (`LoggedOut` →) `LoggedIn` → `deactivate()`.
6
7use crate::auth::{AccessLevel, User};
8use crate::config::ShellConfig;
9use crate::error::CliError;
10use crate::io::CharIo;
11use crate::response::Response;
12use crate::tree::{CommandKind, Directory, Node};
13use core::marker::PhantomData;
14
15#[cfg(feature = "completion")]
16use crate::tree::completion::suggest_completions;
17
18// Sub-modules
19pub mod decoder;
20pub mod handler;
21pub mod history;
22
23// Re-export key types
24pub use decoder::{InputDecoder, InputEvent};
25pub use handler::CommandHandler;
26pub use history::CommandHistory;
27
28/// History navigation direction.
29///
30/// Used by `Request::History` variant. Self-documenting alternative to bool.
31#[repr(u8)]
32#[derive(Debug, Copy, Clone, PartialEq, Eq)]
33pub enum HistoryDirection {
34    /// Up arrow key (navigate to older command)
35    Previous = 0,
36
37    /// Down arrow key (navigate to newer command or restore original)
38    Next = 1,
39}
40
41/// CLI state (authentication state).
42///
43/// Tracks whether the CLI is active and whether user is authenticated.
44/// Used by unified architecture pattern to drive behavior.
45#[derive(Debug, Copy, Clone, PartialEq, Eq)]
46pub enum CliState {
47    /// CLI not active
48    Inactive,
49
50    /// Awaiting authentication
51    #[cfg(feature = "authentication")]
52    LoggedOut,
53
54    /// Authenticated or auth-disabled mode
55    LoggedIn,
56}
57
58/// Request type representing parsed user input.
59///
60/// Generic over `C: ShellConfig` to use configured buffer sizes.
61/// Variants are feature-gated based on available features.
62#[derive(Debug, Clone)]
63#[allow(clippy::large_enum_variant)]
64pub enum Request<C: ShellConfig> {
65    /// Valid authentication attempt
66    #[cfg(feature = "authentication")]
67    Login {
68        /// Username
69        username: heapless::String<32>,
70        /// Password
71        password: heapless::String<64>,
72    },
73
74    /// Invalid authentication attempt
75    #[cfg(feature = "authentication")]
76    InvalidLogin,
77
78    /// Execute command
79    Command {
80        /// Command path
81        path: heapless::String<128>, // TODO: Use C::MAX_INPUT when const generics stabilize
82        /// Command arguments
83        args: heapless::Vec<heapless::String<128>, 16>, // TODO: Use C::MAX_INPUT and C::MAX_ARGS
84        /// Original command string for history
85        #[cfg(feature = "history")]
86        original: heapless::String<128>, // TODO: Use C::MAX_INPUT
87        /// Phantom data for config type (will be used when const generics stabilize)
88        _phantom: PhantomData<C>,
89    },
90
91    /// Request completions
92    #[cfg(feature = "completion")]
93    TabComplete {
94        /// Partial path to complete
95        path: heapless::String<128>, // TODO: Use C::MAX_INPUT
96    },
97
98    /// Navigate history
99    #[cfg(feature = "history")]
100    History {
101        /// Navigation direction
102        direction: HistoryDirection,
103        /// Current buffer content
104        buffer: heapless::String<128>, // TODO: Use C::MAX_INPUT
105    },
106}
107
108/// Shell orchestration struct.
109///
110/// Brings together all components following the unified architecture pattern.
111/// Uses single code path for both auth-enabled and auth-disabled modes.
112pub struct Shell<'tree, L, IO, H, C>
113where
114    L: AccessLevel,
115    IO: CharIo,
116    H: CommandHandler<C>,
117    C: ShellConfig,
118{
119    /// Command tree root
120    tree: &'tree Directory<L>,
121
122    /// Current user (None when logged out or auth disabled)
123    current_user: Option<User<L>>,
124
125    /// CLI state (auth state)
126    state: CliState,
127
128    /// Input buffer (using concrete size for now - TODO: use C::MAX_INPUT when const generics stabilize)
129    input_buffer: heapless::String<128>,
130
131    /// Current directory path (stack of child indices, using concrete size - TODO: use C::MAX_PATH_DEPTH when const generics stabilize)
132    current_path: heapless::Vec<usize, 8>,
133
134    /// Input decoder (escape sequence state machine)
135    decoder: InputDecoder,
136
137    /// Command history (using concrete sizes - TODO: use C::HISTORY_SIZE and C::MAX_INPUT when const generics stabilize)
138    #[cfg_attr(not(feature = "history"), allow(dead_code))]
139    history: CommandHistory<10, 128>,
140
141    /// I/O interface
142    io: IO,
143
144    /// Command handler
145    handler: H,
146
147    /// Credential provider
148    #[cfg(feature = "authentication")]
149    credential_provider: &'tree (dyn crate::auth::CredentialProvider<L, Error = ()> + 'tree),
150
151    /// Config type marker (zero-size)
152    _config: PhantomData<C>,
153}
154
155// ============================================================================
156// Debug implementation
157// ============================================================================
158
159impl<'tree, L, IO, H, C> core::fmt::Debug for Shell<'tree, L, IO, H, C>
160where
161    L: AccessLevel,
162    IO: CharIo,
163    H: CommandHandler<C>,
164    C: ShellConfig,
165{
166    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
167        let mut debug_struct = f.debug_struct("Shell");
168        debug_struct
169            .field("state", &self.state)
170            .field("input_buffer", &self.input_buffer.as_str())
171            .field("current_path", &self.current_path);
172
173        if let Some(user) = &self.current_user {
174            debug_struct.field("current_user", &user.username.as_str());
175        } else {
176            debug_struct.field("current_user", &"None");
177        }
178
179        #[cfg(feature = "authentication")]
180        debug_struct.field("credential_provider", &"<dyn CredentialProvider>");
181
182        debug_struct.finish_non_exhaustive()
183    }
184}
185
186// ============================================================================
187// Constructors (feature-conditional)
188// ============================================================================
189
190#[cfg(feature = "authentication")]
191impl<'tree, L, IO, H, C> Shell<'tree, L, IO, H, C>
192where
193    L: AccessLevel,
194    IO: CharIo,
195    H: CommandHandler<C>,
196    C: ShellConfig,
197{
198    /// Create shell with authentication enabled (starts `Inactive`).
199    /// Call `activate()` to show welcome message and login prompt.
200    pub fn new(
201        tree: &'tree Directory<L>,
202        handler: H,
203        credential_provider: &'tree (dyn crate::auth::CredentialProvider<L, Error = ()> + 'tree),
204        io: IO,
205    ) -> Self {
206        Self {
207            tree,
208            handler,
209            current_user: None,
210            state: CliState::Inactive,
211            input_buffer: heapless::String::new(),
212            current_path: heapless::Vec::new(),
213            decoder: InputDecoder::new(),
214            history: CommandHistory::new(),
215            io,
216            credential_provider,
217            _config: PhantomData,
218        }
219    }
220}
221
222#[cfg(not(feature = "authentication"))]
223impl<'tree, L, IO, H, C> Shell<'tree, L, IO, H, C>
224where
225    L: AccessLevel,
226    IO: CharIo,
227    H: CommandHandler<C>,
228    C: ShellConfig,
229{
230    /// Create new Shell
231    ///
232    /// Starts in `Inactive` state. Call `activate()` to show welcome message and prompt.
233    pub fn new(tree: &'tree Directory<L>, handler: H, io: IO) -> Self {
234        Self {
235            tree,
236            handler,
237            current_user: None,
238            state: CliState::Inactive,
239            input_buffer: heapless::String::new(),
240            current_path: heapless::Vec::new(),
241            decoder: InputDecoder::new(),
242            history: CommandHistory::new(),
243            io,
244            _config: PhantomData,
245        }
246    }
247}
248
249// ============================================================================
250// Core methods (unified implementation for both modes)
251// ============================================================================
252
253impl<'tree, L, IO, H, C> Shell<'tree, L, IO, H, C>
254where
255    L: AccessLevel,
256    IO: CharIo,
257    H: CommandHandler<C>,
258    C: ShellConfig,
259{
260    /// Activate the shell (show welcome message and initial prompt).
261    ///
262    /// Transitions from `Inactive` to appropriate state (LoggedOut or LoggedIn).
263    pub fn activate(&mut self) -> Result<(), IO::Error> {
264        self.io.write_str(C::MSG_WELCOME)?;
265        self.io.write_str("\r\n")?;
266
267        #[cfg(feature = "authentication")]
268        {
269            self.state = CliState::LoggedOut;
270            self.io.write_str(C::MSG_LOGIN_PROMPT)?;
271        }
272
273        #[cfg(not(feature = "authentication"))]
274        {
275            self.state = CliState::LoggedIn;
276            self.generate_and_write_prompt()?;
277        }
278
279        Ok(())
280    }
281
282    /// Deactivate shell (transition to `Inactive`).
283    /// Clears session and resets to root directory.
284    pub fn deactivate(&mut self) {
285        self.state = CliState::Inactive;
286        self.current_user = None;
287        self.input_buffer.clear();
288        self.current_path.clear();
289    }
290
291    /// Process single character of input (main entry point for char-by-char processing).
292    pub fn process_char(&mut self, c: char) -> Result<(), IO::Error> {
293        // Decode character into logical event
294        let event = self.decoder.decode_char(c);
295
296        match event {
297            InputEvent::None => Ok(()), // Still accumulating sequence
298
299            InputEvent::Char(ch) => {
300                // Try to add to buffer
301                match self.input_buffer.push(ch) {
302                    Ok(_) => {
303                        // Successfully added - echo (with password masking if applicable)
304                        let echo_char = self.get_echo_char(ch);
305                        self.io.put_char(echo_char)?;
306                        Ok(())
307                    }
308                    Err(_) => {
309                        // Buffer full - beep and ignore
310                        self.io.put_char('\x07')?; // Bell character
311                        Ok(())
312                    }
313                }
314            }
315
316            InputEvent::Backspace => {
317                // Remove from buffer if not empty
318                if !self.input_buffer.is_empty() {
319                    self.input_buffer.pop();
320                    // Echo backspace sequence
321                    self.io.write_str("\x08 \x08")?;
322                }
323                Ok(())
324            }
325
326            InputEvent::DoubleEsc => {
327                // Clear buffer and redraw (Shell's interpretation of double-ESC)
328                self.input_buffer.clear();
329                self.clear_line_and_redraw()
330            }
331
332            InputEvent::Enter => self.handle_enter(),
333
334            InputEvent::Tab => self.handle_tab(),
335
336            InputEvent::UpArrow => self.handle_history(HistoryDirection::Previous),
337
338            InputEvent::DownArrow => self.handle_history(HistoryDirection::Next),
339        }
340    }
341
342    /// Process single character of input (async version).
343    /// Can execute both sync and async commands.
344    #[cfg(feature = "async")]
345    pub async fn process_char_async(&mut self, c: char) -> Result<(), IO::Error> {
346        // Decode character into logical event
347        let event = self.decoder.decode_char(c);
348
349        match event {
350            InputEvent::None => Ok(()), // Still accumulating sequence
351
352            InputEvent::Char(ch) => {
353                // Try to add to buffer
354                match self.input_buffer.push(ch) {
355                    Ok(_) => {
356                        // Successfully added - echo (with password masking if applicable)
357                        let echo_char = self.get_echo_char(ch);
358                        self.io.put_char(echo_char)?;
359                        Ok(())
360                    }
361                    Err(_) => {
362                        // Buffer full - beep and ignore
363                        self.io.put_char('\x07')?; // Bell character
364                        Ok(())
365                    }
366                }
367            }
368
369            InputEvent::Backspace => {
370                // Remove from buffer if not empty
371                if !self.input_buffer.is_empty() {
372                    self.input_buffer.pop();
373                    // Echo backspace sequence
374                    self.io.write_str("\x08 \x08")?;
375                }
376                Ok(())
377            }
378
379            InputEvent::DoubleEsc => {
380                // Clear buffer and redraw (Shell's interpretation of double-ESC)
381                self.input_buffer.clear();
382                self.clear_line_and_redraw()
383            }
384
385            InputEvent::Enter => self.handle_enter_async().await,
386
387            InputEvent::Tab => self.handle_tab(),
388
389            InputEvent::UpArrow => self.handle_history(HistoryDirection::Previous),
390
391            InputEvent::DownArrow => self.handle_history(HistoryDirection::Next),
392        }
393    }
394
395    /// Poll for incoming characters and process them.
396    /// For interrupt-driven/DMA/async/RTOS use `process_char()` directly.
397    pub fn poll(&mut self) -> Result<(), IO::Error> {
398        if let Some(c) = self.io.get_char()? {
399            self.process_char(c)?;
400        }
401        Ok(())
402    }
403
404    /// Determine what character to echo based on password masking rules.
405    ///
406    /// During login, masks characters after `:` delimiter with `*` for password privacy.
407    fn get_echo_char(&self, ch: char) -> char {
408        #[cfg(feature = "authentication")]
409        {
410            // Password masking only applies during login (LoggedOut state)
411            if self.state == CliState::LoggedOut {
412                // Count colons in buffer (parser has already added current char)
413                let colon_count = self.input_buffer.matches(':').count();
414
415                // Logic: Mask if buffer had at least one colon before this character
416                // - colon_count == 0: No delimiter yet, echo normally
417                // - colon_count == 1 && ch == ':': First colon (just added), echo normally
418                // - Otherwise: We're in password territory, mask it
419                if colon_count == 0 || (colon_count == 1 && ch == ':') {
420                    return ch; // Username or delimiter
421                } else {
422                    return '*'; // Password
423                }
424            }
425        }
426
427        // Default: echo character as-is
428        ch
429    }
430
431    /// Generate prompt string.
432    ///
433    /// Format: `username@path> ` (or `@path> ` when no user/auth disabled)
434    // TODO: Use C::MAX_PROMPT when const generics stabilize
435    fn generate_prompt(&self) -> heapless::String<128> {
436        let mut prompt = heapless::String::new();
437
438        // Username part
439        if let Some(user) = &self.current_user {
440            prompt.push_str(user.username.as_str()).ok();
441        }
442        prompt.push('@').ok();
443
444        // Path part
445        prompt.push('/').ok();
446        if !self.current_path.is_empty()
447            && let Ok(path_str) = self.get_current_path_string()
448        {
449            prompt.push_str(&path_str).ok();
450        }
451
452        prompt.push_str("> ").ok();
453        prompt
454    }
455
456    /// Write prompt to I/O.
457    fn generate_and_write_prompt(&mut self) -> Result<(), IO::Error> {
458        let prompt = self.generate_prompt();
459        self.io.write_str(prompt.as_str())
460    }
461
462    /// Write formatted response to I/O, applying Response formatting flags.
463    ///
464    /// Applies `prefix_newline`, `indent_message`, and `postfix_newline` flags.
465    /// Note: `inline_message` and `show_prompt` are handled by callers.
466    fn write_formatted_response(&mut self, response: &Response<C>) -> Result<(), IO::Error> {
467        // Prefix newline (blank line before output)
468        if response.prefix_newline {
469            self.io.write_str("\r\n")?;
470        }
471
472        // Write message (with optional indentation)
473        if response.indent_message {
474            // Split by lines and indent each
475            for (i, line) in response.message.split("\r\n").enumerate() {
476                if i > 0 {
477                    self.io.write_str("\r\n")?;
478                }
479                self.io.write_str("  ")?; // 2-space indent
480                self.io.write_str(line)?;
481            }
482        } else {
483            // Write message as-is
484            self.io.write_str(&response.message)?;
485        }
486
487        // Postfix newline
488        if response.postfix_newline {
489            self.io.write_str("\r\n")?;
490        }
491
492        Ok(())
493    }
494
495    /// Format error message using Display trait.
496    ///
497    /// Converts CliError to a heapless string using its Display implementation.
498    /// Returns a buffer containing the formatted error message.
499    // TODO: Use C::MAX_RESPONSE when const generics stabilize
500    fn format_error(error: &CliError) -> heapless::String<256> {
501        use core::fmt::Write;
502        let mut buffer = heapless::String::new();
503        // Write using Display trait implementation
504        // Ignore write errors (buffer full) - partial message is better than none
505        let _ = write!(&mut buffer, "{}", error);
506        buffer
507    }
508
509    /// Get current directory node.
510    fn get_current_dir(&self) -> Result<&'tree Directory<L>, CliError> {
511        let mut current: &Directory<L> = self.tree;
512
513        for &index in self.current_path.iter() {
514            match current.children.get(index) {
515                Some(Node::Directory(dir)) => current = dir,
516                Some(Node::Command(_)) | None => return Err(CliError::InvalidPath),
517            }
518        }
519
520        Ok(current)
521    }
522
523    /// Get current path as string (for prompt).
524    // TODO: Use C::MAX_INPUT when const generics stabilize
525    fn get_current_path_string(&self) -> Result<heapless::String<128>, CliError> {
526        let mut path_str = heapless::String::new();
527        let mut current: &Directory<L> = self.tree;
528
529        for (i, &index) in self.current_path.iter().enumerate() {
530            match current.children.get(index) {
531                Some(Node::Directory(dir)) => {
532                    if i > 0 {
533                        path_str.push('/').map_err(|_| CliError::BufferFull)?;
534                    }
535                    path_str
536                        .push_str(dir.name)
537                        .map_err(|_| CliError::BufferFull)?;
538                    current = dir;
539                }
540                _ => return Err(CliError::InvalidPath),
541            }
542        }
543
544        Ok(path_str)
545    }
546
547    /// Handle Enter key (submit command or login).
548    fn handle_enter(&mut self) -> Result<(), IO::Error> {
549        // Note: Newline after input is written by the handler
550        // (conditionally based on Response.inline_message flag for commands)
551
552        let input = self.input_buffer.clone();
553        self.input_buffer.clear();
554
555        match self.state {
556            CliState::Inactive => Ok(()),
557
558            #[cfg(feature = "authentication")]
559            CliState::LoggedOut => self.handle_login_input(&input),
560
561            CliState::LoggedIn => self.handle_input_line(&input),
562        }
563    }
564
565    /// Handle Enter key press - async version.
566    ///
567    /// Dispatches to appropriate handler based on current state.
568    #[cfg(feature = "async")]
569    async fn handle_enter_async(&mut self) -> Result<(), IO::Error> {
570        // Note: Newline after input is written by the handler
571        // (conditionally based on Response.inline_message flag for commands)
572
573        let input = self.input_buffer.clone();
574        self.input_buffer.clear();
575
576        match self.state {
577            CliState::Inactive => Ok(()),
578
579            #[cfg(feature = "authentication")]
580            CliState::LoggedOut => self.handle_login_input(&input),
581
582            CliState::LoggedIn => self.handle_input_line_async(&input).await,
583        }
584    }
585
586    /// Handle a valid login attempt.
587    #[cfg(feature = "authentication")]
588    fn handle_login_input(&mut self, input: &str) -> Result<(), IO::Error> {
589        // Login doesn't support inline mode - always add newline
590        self.io.write_str("\r\n  ")?;
591
592        if input.contains(':') {
593            // Format: username:password
594            let parts: heapless::Vec<&str, 2> = input.splitn(2, ':').collect();
595            if parts.len() == 2 {
596                let username = parts[0];
597                let password = parts[1];
598
599                // Attempt authentication
600                match self.credential_provider.find_user(username) {
601                    Ok(Some(user)) if self.credential_provider.verify_password(&user, password) => {
602                        // Login successful
603                        self.current_user = Some(user);
604                        self.state = CliState::LoggedIn;
605                        self.io.write_str(C::MSG_LOGIN_SUCCESS)?;
606                        self.io.write_str("\r\n")?;
607                        self.generate_and_write_prompt()?;
608                    }
609                    _ => {
610                        // Login failed (user not found or wrong password)
611                        self.io.write_str(C::MSG_LOGIN_FAILED)?;
612                        self.io.write_str("\r\n")?;
613                        self.io.write_str(C::MSG_LOGIN_PROMPT)?;
614                    }
615                }
616            } else {
617                self.io.write_str(C::MSG_INVALID_LOGIN_FORMAT)?;
618                self.io.write_str("\r\n")?;
619                self.io.write_str(C::MSG_LOGIN_PROMPT)?;
620            }
621        } else {
622            // No colon - invalid format, show error
623            self.io.write_str(C::MSG_INVALID_LOGIN_FORMAT)?;
624            self.io.write_str("\r\n")?;
625            self.io.write_str(C::MSG_LOGIN_PROMPT)?;
626        }
627
628        Ok(())
629    }
630
631    /// Process global commands (?, ls, clear, logout).
632    ///
633    /// Returns true if a global command was handled, false otherwise.
634    fn handle_global_commands(&mut self, input: &str) -> Result<bool, IO::Error> {
635        // Check for global commands first (non-tree operations)
636        // Global commands don't support inline mode
637        match input.trim() {
638            "?" => {
639                self.io.write_str("\r\n")?;
640                self.show_help()?;
641                self.generate_and_write_prompt()?;
642                Ok(true)
643            }
644            "ls" => {
645                self.io.write_str("\r\n")?;
646                self.show_ls()?;
647                self.generate_and_write_prompt()?;
648                Ok(true)
649            }
650            "clear" => {
651                // Clear screen - no newline needed before ANSI clear sequence
652                self.io.write_str("\x1b[2J\x1b[H")?; // ANSI clear screen
653                self.generate_and_write_prompt()?;
654                Ok(true)
655            }
656            #[cfg(feature = "authentication")]
657            "logout" => {
658                self.io.write_str("\r\n  ")?;
659                self.current_user = None;
660                self.state = CliState::LoggedOut;
661                self.current_path.clear();
662                self.io.write_str(C::MSG_LOGOUT)?;
663                self.io.write_str("\r\n")?;
664                self.io.write_str(C::MSG_LOGIN_PROMPT)?;
665                Ok(true)
666            }
667            _ => Ok(false),
668        }
669    }
670
671    /// Write response and handle history/prompt based on Response flags.
672    fn write_response_and_prompt(
673        &mut self,
674        response: Response<C>,
675        #[cfg_attr(not(feature = "history"), allow(unused_variables))] input: &str,
676    ) -> Result<(), IO::Error> {
677        // Add newline after input UNLESS response wants inline mode
678        if !response.inline_message {
679            self.io.write_str("\r\n")?;
680        }
681
682        // Write formatted response (implements all Response flags!)
683        self.write_formatted_response(&response)?;
684
685        // Add to history if not excluded
686        #[cfg(feature = "history")]
687        if !response.exclude_from_history {
688            self.history.add(input);
689        }
690
691        // Show prompt if requested by response
692        if response.show_prompt {
693            self.generate_and_write_prompt()?;
694        }
695
696        Ok(())
697    }
698
699    /// Write error message with formatting.
700    fn write_error_and_prompt(&mut self, error: CliError) -> Result<(), IO::Error> {
701        // Errors don't support inline mode - add newline
702        self.io.write_str("\r\n  ")?;
703
704        // Format and write error message using Display trait
705        self.io.write_str("Error: ")?;
706        let error_msg = Self::format_error(&error);
707        self.io.write_str(error_msg.as_str())?;
708        self.io.write_str("\r\n")?;
709        self.generate_and_write_prompt()?;
710
711        Ok(())
712    }
713
714    /// Handle user input line when in LoggedIn state.
715    ///
716    /// Processes three types of input:
717    /// 1. Global commands (?, ls, clear, logout)
718    /// 2. Tree navigation (paths resolving to directories)
719    /// 3. Tree commands (paths resolving to Node::Command)
720    fn handle_input_line(&mut self, input: &str) -> Result<(), IO::Error> {
721        // Skip empty input
722        if input.trim().is_empty() {
723            self.io.write_str("\r\n")?;
724            self.generate_and_write_prompt()?;
725            return Ok(());
726        }
727
728        // Check for global commands first (non-tree operations)
729        if self.handle_global_commands(input)? {
730            return Ok(());
731        }
732
733        // Handle tree operations (navigation or command execution)
734        match self.execute_tree_path(input) {
735            Ok(response) => self.write_response_and_prompt(response, input),
736            Err(e) => self.write_error_and_prompt(e),
737        }
738    }
739
740    /// Handle user input line when in LoggedIn state - async version.
741    ///
742    /// Processes three types of input:
743    /// 1. Global commands (?, ls, clear, logout)
744    /// 2. Tree navigation (paths resolving to directories)
745    /// 3. Tree commands (paths resolving to Node::Command - both sync and async)
746    #[cfg(feature = "async")]
747    async fn handle_input_line_async(&mut self, input: &str) -> Result<(), IO::Error> {
748        // Skip empty input
749        if input.trim().is_empty() {
750            self.io.write_str("\r\n")?;
751            self.generate_and_write_prompt()?;
752            return Ok(());
753        }
754
755        // Check for global commands first (non-tree operations)
756        if self.handle_global_commands(input)? {
757            return Ok(());
758        }
759
760        // Handle tree operations (navigation or command execution) - async version
761        match self.execute_tree_path_async(input).await {
762            Ok(response) => self.write_response_and_prompt(response, input),
763            Err(e) => self.write_error_and_prompt(e),
764        }
765    }
766
767    /// Execute a tree path (navigation or command execution).
768    ///
769    /// Resolves the path and either:
770    /// - Navigates to a directory (if path resolves to Node::Directory)
771    /// - Executes a tree command (if path resolves to Node::Command)
772    ///
773    /// Note: "command" here refers specifically to Node::Command,
774    /// not generic user input.
775    fn execute_tree_path(&mut self, input: &str) -> Result<Response<C>, CliError> {
776        // Parse path and arguments
777        // TODO: Use C::MAX_ARGS + 1 when const generics stabilize (command + args)
778        let parts: heapless::Vec<&str, 17> = input.split_whitespace().collect();
779        if parts.is_empty() {
780            return Err(CliError::CommandNotFound);
781        }
782
783        let path_str = parts[0];
784        let args = &parts[1..];
785
786        // Resolve path to node (None represents root directory)
787        let (target_node, new_path) = self.resolve_path(path_str)?;
788
789        // Case 1: Directory navigation
790        match target_node {
791            None | Some(Node::Directory(_)) => {
792                // Directory navigation - update path and return
793                self.current_path = new_path;
794                #[cfg(feature = "history")]
795                return Ok(Response::success("")
796                    .without_history()
797                    .without_postfix_newline());
798                #[cfg(not(feature = "history"))]
799                return Ok(Response::success("").without_postfix_newline());
800            }
801            Some(Node::Command(cmd_meta)) => {
802                // Case 2: Tree command execution
803                // Check access control - use InvalidPath for security (don't reveal access denied)
804                if let Some(user) = &self.current_user
805                    && user.access_level < cmd_meta.access_level
806                {
807                    return Err(CliError::InvalidPath);
808                }
809
810                // Validate argument count
811                if args.len() < cmd_meta.min_args || args.len() > cmd_meta.max_args {
812                    return Err(CliError::InvalidArgumentCount {
813                        expected_min: cmd_meta.min_args,
814                        expected_max: cmd_meta.max_args,
815                        received: args.len(),
816                    });
817                }
818
819                // Dispatch to command handler
820                match cmd_meta.kind {
821                    CommandKind::Sync => {
822                        // Execute synchronous tree command (dispatch by unique ID)
823                        self.handler.execute_sync(cmd_meta.id, args)
824                    }
825                    #[cfg(feature = "async")]
826                    CommandKind::Async => {
827                        // Async tree command called from sync context
828                        Err(CliError::AsyncInSyncContext)
829                    }
830                }
831            }
832        }
833    }
834
835    /// Execute a tree path (navigation or command execution) - async version.
836    ///
837    /// Resolves the path and either:
838    /// - Navigates to a directory (if path resolves to Node::Directory)
839    /// - Executes a tree command (if path resolves to Node::Command)
840    ///
841    /// This async version can execute both sync and async commands.
842    /// Sync commands are called directly, async commands are awaited.
843    ///
844    /// Note: "command" here refers specifically to Node::Command,
845    /// not generic user input.
846    #[cfg(feature = "async")]
847    async fn execute_tree_path_async(&mut self, input: &str) -> Result<Response<C>, CliError> {
848        // Parse path and arguments
849        // TODO: Use C::MAX_ARGS + 1 when const generics stabilize (command + args)
850        let parts: heapless::Vec<&str, 17> = input.split_whitespace().collect();
851        if parts.is_empty() {
852            return Err(CliError::CommandNotFound);
853        }
854
855        let path_str = parts[0];
856        let args = &parts[1..];
857
858        // Resolve path to node (None represents root directory)
859        let (target_node, new_path) = self.resolve_path(path_str)?;
860
861        // Case 1: Directory navigation
862        match target_node {
863            None | Some(Node::Directory(_)) => {
864                // Directory navigation - update path and return
865                self.current_path = new_path;
866                #[cfg(feature = "history")]
867                return Ok(Response::success("")
868                    .without_history()
869                    .without_postfix_newline());
870                #[cfg(not(feature = "history"))]
871                return Ok(Response::success("").without_postfix_newline());
872            }
873            Some(Node::Command(cmd_meta)) => {
874                // Case 2: Tree command execution
875                // Check access control - use InvalidPath for security (don't reveal access denied)
876                if let Some(user) = &self.current_user
877                    && user.access_level < cmd_meta.access_level
878                {
879                    return Err(CliError::InvalidPath);
880                }
881
882                // Validate argument count
883                if args.len() < cmd_meta.min_args || args.len() > cmd_meta.max_args {
884                    return Err(CliError::InvalidArgumentCount {
885                        expected_min: cmd_meta.min_args,
886                        expected_max: cmd_meta.max_args,
887                        received: args.len(),
888                    });
889                }
890
891                // Dispatch to command handler (handle both sync and async)
892                match cmd_meta.kind {
893                    CommandKind::Sync => {
894                        // Sync command in async context - call directly
895                        self.handler.execute_sync(cmd_meta.id, args)
896                    }
897                    CommandKind::Async => {
898                        // Async command - await execution
899                        self.handler.execute_async(cmd_meta.id, args).await
900                    }
901                }
902            }
903        }
904    }
905
906    /// Resolve a path string to a node.
907    ///
908    /// Returns (node, path_stack) where path_stack is the navigation path.
909    /// Node is None when path resolves to root directory.
910    // TODO: Use C::MAX_PATH_DEPTH when const generics stabilize
911    fn resolve_path(
912        &self,
913        path_str: &str,
914    ) -> Result<(Option<&'tree Node<L>>, heapless::Vec<usize, 8>), CliError> {
915        // Start from current directory or root
916        // TODO: Use C::MAX_PATH_DEPTH when const generics stabilize
917        let mut working_path: heapless::Vec<usize, 8> = if path_str.starts_with('/') {
918            heapless::Vec::new() // Absolute path starts from root
919        } else {
920            self.current_path.clone() // Relative path starts from current
921        };
922
923        // Parse path
924        // TODO: Use C::MAX_PATH_DEPTH when const generics stabilize
925        let segments: heapless::Vec<&str, 8> = path_str
926            .trim_start_matches('/')
927            .split('/')
928            .filter(|s| !s.is_empty() && *s != ".")
929            .collect();
930
931        // Navigate through segments
932        for (seg_idx, segment) in segments.iter().enumerate() {
933            if *segment == ".." {
934                // Parent directory
935                working_path.pop();
936                continue;
937            }
938
939            let is_last_segment = seg_idx == segments.len() - 1;
940
941            // Find child with this name
942            let current_dir = self.get_dir_at_path(&working_path)?;
943            let mut found = false;
944
945            for (index, child) in current_dir.children.iter().enumerate() {
946                // Check access control
947                let node_level = match child {
948                    Node::Command(cmd) => cmd.access_level,
949                    Node::Directory(dir) => dir.access_level,
950                };
951
952                if let Some(user) = &self.current_user
953                    && user.access_level < node_level
954                {
955                    continue; // User lacks access, skip this node
956                }
957
958                if child.name() == *segment {
959                    // Found it!
960                    if child.is_directory() {
961                        // Navigate into directory
962                        working_path
963                            .push(index)
964                            .map_err(|_| CliError::PathTooDeep)?;
965                    } else {
966                        // It's a command - can only return if this is the last segment
967                        if is_last_segment {
968                            return Ok((Some(child), working_path));
969                        } else {
970                            // Trying to navigate through a command - invalid path structure
971                            return Err(CliError::InvalidPath);
972                        }
973                    }
974                    found = true;
975                    break;
976                }
977            }
978
979            if !found {
980                return Err(CliError::CommandNotFound);
981            }
982        }
983
984        // Path resolved to a directory
985        // Handle root directory specially (when path is empty)
986        if working_path.is_empty() {
987            // Return None to represent root directory
988            return Ok((None, working_path));
989        }
990
991        let dir_node = self.get_node_at_path(&working_path)?;
992        Ok((Some(dir_node), working_path))
993    }
994
995    /// Get directory at specific path.
996    // TODO: Use C::MAX_PATH_DEPTH when const generics stabilize
997    fn get_dir_at_path(
998        &self,
999        path: &heapless::Vec<usize, 8>,
1000    ) -> Result<&'tree Directory<L>, CliError> {
1001        let mut current: &Directory<L> = self.tree;
1002
1003        for &index in path.iter() {
1004            match current.children.get(index) {
1005                Some(Node::Directory(dir)) => current = dir,
1006                Some(Node::Command(_)) | None => return Err(CliError::InvalidPath),
1007            }
1008        }
1009
1010        Ok(current)
1011    }
1012
1013    /// Get node at specific path.
1014    // TODO: Use C::MAX_PATH_DEPTH when const generics stabilize
1015    fn get_node_at_path(&self, path: &heapless::Vec<usize, 8>) -> Result<&'tree Node<L>, CliError> {
1016        if path.is_empty() {
1017            // Root directory - need to find a way to return it as a Node
1018            // For now, return error since we can't construct Node::Directory here
1019            return Err(CliError::InvalidPath);
1020        }
1021
1022        // TODO: Use C::MAX_PATH_DEPTH when const generics stabilize
1023        let parent_path: heapless::Vec<usize, 8> =
1024            path.iter().take(path.len() - 1).copied().collect();
1025        let parent_dir = self.get_dir_at_path(&parent_path)?;
1026
1027        let last_index = *path.last().ok_or(CliError::InvalidPath)?;
1028        parent_dir
1029            .children
1030            .get(last_index)
1031            .ok_or(CliError::InvalidPath)
1032    }
1033
1034    /// Handle Tab completion.
1035    fn handle_tab(&mut self) -> Result<(), IO::Error> {
1036        #[cfg(feature = "completion")]
1037        {
1038            // Get current directory
1039            let current_dir = match self.get_current_dir() {
1040                Ok(dir) => dir,
1041                Err(_) => return self.generate_and_write_prompt(), // Error, just redraw prompt
1042            };
1043
1044            // Suggest completions
1045            let result = suggest_completions::<L, 16>(
1046                current_dir,
1047                self.input_buffer.as_str(),
1048                self.current_user.as_ref(),
1049            );
1050
1051            match result {
1052                Ok(crate::tree::completion::CompletionResult::Single { completion, .. }) => {
1053                    // Single match - replace buffer and update display
1054                    self.input_buffer.clear();
1055                    match self.input_buffer.push_str(&completion) {
1056                        Ok(()) => {
1057                            // Redraw line
1058                            self.io.write_str("\r")?; // Carriage return
1059                            let prompt = self.generate_prompt();
1060                            self.io.write_str(prompt.as_str())?;
1061                            self.io.write_str(self.input_buffer.as_str())?;
1062                        }
1063                        Err(_) => {
1064                            // Completion too long for buffer - beep
1065                            self.io.put_char('\x07')?;
1066                        }
1067                    }
1068                }
1069                Ok(crate::tree::completion::CompletionResult::Multiple { all_matches, .. }) => {
1070                    // Multiple matches - show them
1071                    self.io.write_str("\r\n")?;
1072                    for m in all_matches.iter() {
1073                        self.io.write_str("  ")?; // 2-space indentation
1074                        self.io.write_str(m.as_str())?;
1075                        self.io.write_str("  ")?;
1076                    }
1077                    self.io.write_str("\r\n")?;
1078                    self.generate_and_write_prompt()?;
1079                    self.io.write_str(self.input_buffer.as_str())?;
1080                }
1081                _ => {
1082                    // No matches or error - just beep
1083                    self.io.put_char('\x07')?; // Bell character
1084                }
1085            }
1086        }
1087
1088        #[cfg(not(feature = "completion"))]
1089        {
1090            // Completion disabled - just beep
1091            self.io.put_char('\x07')?; // Bell character
1092        }
1093
1094        Ok(())
1095    }
1096
1097    /// Handle history navigation.
1098    fn handle_history(&mut self, direction: HistoryDirection) -> Result<(), IO::Error> {
1099        #[cfg(feature = "history")]
1100        {
1101            let history_entry = match direction {
1102                HistoryDirection::Previous => self.history.previous_command(),
1103                HistoryDirection::Next => self.history.next_command(),
1104            };
1105
1106            if let Some(entry) = history_entry {
1107                // Replace buffer with history entry
1108                self.input_buffer = entry;
1109                // Redraw line
1110                self.clear_line_and_redraw()?;
1111            }
1112        }
1113
1114        #[cfg(not(feature = "history"))]
1115        {
1116            // History disabled - ignore
1117            let _ = direction; // Silence unused warning
1118        }
1119
1120        Ok(())
1121    }
1122
1123    /// Show help (? command).
1124    fn show_help(&mut self) -> Result<(), IO::Error> {
1125        self.io.write_str("  ?        - List global commands\r\n")?;
1126        self.io
1127            .write_str("  ls       - List directory contents\r\n")?;
1128
1129        #[cfg(feature = "authentication")]
1130        self.io.write_str("  logout   - End session\r\n")?;
1131
1132        self.io.write_str("  clear    - Clear screen\r\n")?;
1133        self.io.write_str("  ESC ESC  - Clear input buffer\r\n")?;
1134
1135        Ok(())
1136    }
1137
1138    /// Show directory listing (ls command).
1139    fn show_ls(&mut self) -> Result<(), IO::Error> {
1140        let current_dir = match self.get_current_dir() {
1141            Ok(dir) => dir,
1142            Err(_) => {
1143                self.io.write_str("Error accessing directory\r\n")?;
1144                return Ok(());
1145            }
1146        };
1147
1148        for child in current_dir.children.iter() {
1149            // Check access control
1150            let node_level = match child {
1151                Node::Command(cmd) => cmd.access_level,
1152                Node::Directory(dir) => dir.access_level,
1153            };
1154
1155            if let Some(user) = &self.current_user
1156                && user.access_level < node_level
1157            {
1158                continue; // User lacks access, skip this node
1159            }
1160
1161            // Format output
1162            match child {
1163                Node::Command(cmd) => {
1164                    self.io.write_str("  ")?;
1165                    self.io.write_str(cmd.name)?;
1166                    self.io.write_str("  - ")?;
1167                    self.io.write_str(cmd.description)?;
1168                    self.io.write_str("\r\n")?;
1169                }
1170                Node::Directory(dir) => {
1171                    self.io.write_str("  ")?;
1172                    self.io.write_str(dir.name)?;
1173                    self.io.write_str("/  - Directory\r\n")?;
1174                }
1175            }
1176        }
1177
1178        Ok(())
1179    }
1180
1181    /// Clear current line and redraw with prompt and buffer.
1182    fn clear_line_and_redraw(&mut self) -> Result<(), IO::Error> {
1183        self.io.write_str("\r\x1b[K")?; // CR + clear to end of line
1184        self.generate_and_write_prompt()?;
1185        self.io.write_str(self.input_buffer.as_str())?;
1186        Ok(())
1187    }
1188
1189    // ========================================
1190    // I/O Access
1191    // ========================================
1192
1193    /// Get reference to I/O interface (for inspection or direct control).
1194    pub fn io(&self) -> &IO {
1195        &self.io
1196    }
1197
1198    /// Get mutable reference to I/O interface (for manipulation or direct control).
1199    pub fn io_mut(&mut self) -> &mut IO {
1200        &mut self.io
1201    }
1202}
1203
1204// ============================================================================
1205// Tests
1206// ============================================================================
1207
1208#[cfg(test)]
1209mod tests {
1210    use super::*;
1211    use crate::auth::AccessLevel;
1212    use crate::config::DefaultConfig;
1213    use crate::io::CharIo;
1214    use crate::tree::{CommandKind, CommandMeta, Directory, Node};
1215
1216    // Mock access level
1217    #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
1218    enum MockLevel {
1219        User = 0,
1220    }
1221
1222    impl AccessLevel for MockLevel {
1223        fn from_str(s: &str) -> Option<Self> {
1224            match s {
1225                "User" => Some(Self::User),
1226                _ => None,
1227            }
1228        }
1229
1230        fn as_str(&self) -> &'static str {
1231            "User"
1232        }
1233    }
1234
1235    // Mock I/O that captures output
1236    struct MockIo {
1237        output: heapless::String<512>,
1238    }
1239    impl MockIo {
1240        fn new() -> Self {
1241            Self {
1242                output: heapless::String::new(),
1243            }
1244        }
1245
1246        #[allow(dead_code)]
1247        fn get_output(&self) -> &str {
1248            &self.output
1249        }
1250    }
1251    impl CharIo for MockIo {
1252        type Error = ();
1253        fn get_char(&mut self) -> Result<Option<char>, ()> {
1254            Ok(None)
1255        }
1256        fn put_char(&mut self, c: char) -> Result<(), ()> {
1257            self.output.push(c).map_err(|_| ())
1258        }
1259        fn write_str(&mut self, s: &str) -> Result<(), ()> {
1260            self.output.push_str(s).map_err(|_| ())
1261        }
1262    }
1263
1264    // Mock handler
1265    struct MockHandler;
1266    impl CommandHandler<DefaultConfig> for MockHandler {
1267        fn execute_sync(
1268            &self,
1269            _id: &str,
1270            _args: &[&str],
1271        ) -> Result<crate::response::Response<DefaultConfig>, crate::error::CliError> {
1272            Err(crate::error::CliError::CommandNotFound)
1273        }
1274
1275        #[cfg(feature = "async")]
1276        async fn execute_async(
1277            &self,
1278            _id: &str,
1279            _args: &[&str],
1280        ) -> Result<crate::response::Response<DefaultConfig>, crate::error::CliError> {
1281            Err(crate::error::CliError::CommandNotFound)
1282        }
1283    }
1284
1285    // Test commands
1286    const CMD_TEST: CommandMeta<MockLevel> = CommandMeta {
1287        id: "test-cmd",
1288        name: "test-cmd",
1289        description: "Test command",
1290        access_level: MockLevel::User,
1291        kind: CommandKind::Sync,
1292        min_args: 0,
1293        max_args: 0,
1294    };
1295
1296    const CMD_REBOOT: CommandMeta<MockLevel> = CommandMeta {
1297        id: "reboot",
1298        name: "reboot",
1299        description: "Reboot the system",
1300        access_level: MockLevel::User,
1301        kind: CommandKind::Sync,
1302        min_args: 0,
1303        max_args: 0,
1304    };
1305
1306    const CMD_STATUS: CommandMeta<MockLevel> = CommandMeta {
1307        id: "status",
1308        name: "status",
1309        description: "Show status",
1310        access_level: MockLevel::User,
1311        kind: CommandKind::Sync,
1312        min_args: 0,
1313        max_args: 0,
1314    };
1315
1316    const CMD_LED: CommandMeta<MockLevel> = CommandMeta {
1317        id: "led",
1318        name: "led",
1319        description: "Control LED",
1320        access_level: MockLevel::User,
1321        kind: CommandKind::Sync,
1322        min_args: 1,
1323        max_args: 1,
1324    };
1325
1326    const CMD_NETWORK_STATUS: CommandMeta<MockLevel> = CommandMeta {
1327        id: "network_status",
1328        name: "status",
1329        description: "Network status",
1330        access_level: MockLevel::User,
1331        kind: CommandKind::Sync,
1332        min_args: 0,
1333        max_args: 0,
1334    };
1335
1336    // Test directories
1337    const DIR_HARDWARE: Directory<MockLevel> = Directory {
1338        name: "hardware",
1339        children: &[Node::Command(&CMD_LED)],
1340        access_level: MockLevel::User,
1341    };
1342
1343    const DIR_NETWORK: Directory<MockLevel> = Directory {
1344        name: "network",
1345        children: &[Node::Command(&CMD_NETWORK_STATUS)],
1346        access_level: MockLevel::User,
1347    };
1348
1349    const DIR_SYSTEM: Directory<MockLevel> = Directory {
1350        name: "system",
1351        children: &[
1352            Node::Command(&CMD_REBOOT),
1353            Node::Command(&CMD_STATUS),
1354            Node::Directory(&DIR_HARDWARE),
1355            Node::Directory(&DIR_NETWORK),
1356        ],
1357        access_level: MockLevel::User,
1358    };
1359
1360    // Test tree
1361    const TEST_TREE: Directory<MockLevel> = Directory {
1362        name: "/",
1363        children: &[Node::Command(&CMD_TEST), Node::Directory(&DIR_SYSTEM)],
1364        access_level: MockLevel::User,
1365    };
1366
1367    #[test]
1368    fn test_request_command_no_args() {
1369        let mut path = heapless::String::<128>::new();
1370        path.push_str("help").unwrap();
1371        let args = heapless::Vec::new();
1372        #[cfg(feature = "history")]
1373        let original = {
1374            let mut s = heapless::String::<128>::new();
1375            s.push_str("help").unwrap();
1376            s
1377        };
1378
1379        let request = Request::<DefaultConfig>::Command {
1380            path,
1381            args,
1382            #[cfg(feature = "history")]
1383            original,
1384            _phantom: core::marker::PhantomData,
1385        };
1386
1387        match request {
1388            Request::Command { path, args, .. } => {
1389                assert_eq!(path.as_str(), "help");
1390                assert_eq!(args.len(), 0);
1391            }
1392            #[allow(unreachable_patterns)]
1393            _ => panic!("Expected Command variant"),
1394        }
1395    }
1396
1397    #[test]
1398    fn test_request_command_with_args() {
1399        let mut path = heapless::String::<128>::new();
1400        path.push_str("echo").unwrap();
1401
1402        let mut args = heapless::Vec::new();
1403        let mut hello = heapless::String::<128>::new();
1404        hello.push_str("hello").unwrap();
1405        let mut world = heapless::String::<128>::new();
1406        world.push_str("world").unwrap();
1407        args.push(hello).unwrap();
1408        args.push(world).unwrap();
1409
1410        #[cfg(feature = "history")]
1411        let original = {
1412            let mut s = heapless::String::<128>::new();
1413            s.push_str("echo hello world").unwrap();
1414            s
1415        };
1416
1417        let request = Request::<DefaultConfig>::Command {
1418            path,
1419            args,
1420            #[cfg(feature = "history")]
1421            original,
1422            _phantom: core::marker::PhantomData,
1423        };
1424
1425        match request {
1426            Request::Command { path, args, .. } => {
1427                assert_eq!(path.as_str(), "echo");
1428                assert_eq!(args.len(), 2);
1429                assert_eq!(args[0].as_str(), "hello");
1430                assert_eq!(args[1].as_str(), "world");
1431            }
1432            #[allow(unreachable_patterns)]
1433            _ => panic!("Expected Command variant"),
1434        }
1435    }
1436
1437    #[test]
1438    #[cfg(feature = "history")]
1439    fn test_request_command_with_original() {
1440        let mut path = heapless::String::<128>::new();
1441        path.push_str("reboot").unwrap();
1442        let mut original = heapless::String::<128>::new();
1443        original.push_str("reboot").unwrap();
1444
1445        let request = Request::<DefaultConfig>::Command {
1446            path,
1447            args: heapless::Vec::new(),
1448            original,
1449            _phantom: core::marker::PhantomData,
1450        };
1451
1452        match request {
1453            Request::Command { path, original, .. } => {
1454                assert_eq!(path.as_str(), "reboot");
1455                assert_eq!(original.as_str(), "reboot");
1456            }
1457            #[allow(unreachable_patterns)]
1458            _ => panic!("Expected Command variant"),
1459        }
1460    }
1461
1462    #[test]
1463    #[cfg(feature = "authentication")]
1464    fn test_request_login() {
1465        let mut username = heapless::String::<32>::new();
1466        username.push_str("admin").unwrap();
1467        let mut password = heapless::String::<64>::new();
1468        password.push_str("secret123").unwrap();
1469
1470        let request = Request::<DefaultConfig>::Login { username, password };
1471
1472        match request {
1473            Request::Login { username, password } => {
1474                assert_eq!(username.as_str(), "admin");
1475                assert_eq!(password.as_str(), "secret123");
1476            }
1477            #[allow(unreachable_patterns)]
1478            _ => panic!("Expected Login variant"),
1479        }
1480    }
1481
1482    #[test]
1483    #[cfg(feature = "authentication")]
1484    fn test_request_invalid_login() {
1485        let request = Request::<DefaultConfig>::InvalidLogin;
1486
1487        match request {
1488            Request::InvalidLogin => {}
1489            #[allow(unreachable_patterns)]
1490            _ => panic!("Expected InvalidLogin variant"),
1491        }
1492    }
1493
1494    #[test]
1495    #[cfg(feature = "completion")]
1496    fn test_request_tab_complete() {
1497        let mut path = heapless::String::<128>::new();
1498        path.push_str("sys").unwrap();
1499
1500        let request = Request::<DefaultConfig>::TabComplete { path };
1501
1502        match request {
1503            Request::TabComplete { path } => {
1504                assert_eq!(path.as_str(), "sys");
1505            }
1506            #[allow(unreachable_patterns)]
1507            _ => panic!("Expected TabComplete variant"),
1508        }
1509    }
1510
1511    #[test]
1512    #[cfg(feature = "completion")]
1513    fn test_request_tab_complete_empty() {
1514        let request = Request::<DefaultConfig>::TabComplete {
1515            path: heapless::String::new(),
1516        };
1517
1518        match request {
1519            Request::TabComplete { path } => {
1520                assert_eq!(path.as_str(), "");
1521            }
1522            #[allow(unreachable_patterns)]
1523            _ => panic!("Expected TabComplete variant"),
1524        }
1525    }
1526
1527    #[test]
1528    #[cfg(feature = "history")]
1529    fn test_request_history_previous() {
1530        let mut buffer = heapless::String::<128>::new();
1531        buffer.push_str("current input").unwrap();
1532
1533        let request = Request::<DefaultConfig>::History {
1534            direction: HistoryDirection::Previous,
1535            buffer,
1536        };
1537
1538        match request {
1539            Request::History { direction, buffer } => {
1540                assert_eq!(direction, HistoryDirection::Previous);
1541                assert_eq!(buffer.as_str(), "current input");
1542            }
1543            #[allow(unreachable_patterns)]
1544            _ => panic!("Expected History variant"),
1545        }
1546    }
1547
1548    #[test]
1549    #[cfg(feature = "history")]
1550    fn test_request_history_next() {
1551        let request = Request::<DefaultConfig>::History {
1552            direction: HistoryDirection::Next,
1553            buffer: heapless::String::new(),
1554        };
1555
1556        match request {
1557            Request::History { direction, buffer } => {
1558                assert_eq!(direction, HistoryDirection::Next);
1559                assert_eq!(buffer.as_str(), "");
1560            }
1561            #[allow(unreachable_patterns)]
1562            _ => panic!("Expected History variant"),
1563        }
1564    }
1565
1566    #[test]
1567    fn test_request_variants_match_features() {
1568        let _cmd = Request::<DefaultConfig>::Command {
1569            path: heapless::String::new(),
1570            args: heapless::Vec::new(),
1571            #[cfg(feature = "history")]
1572            original: heapless::String::new(),
1573            _phantom: core::marker::PhantomData,
1574        };
1575
1576        #[cfg(feature = "authentication")]
1577        let _login = Request::<DefaultConfig>::Login {
1578            username: heapless::String::new(),
1579            password: heapless::String::new(),
1580        };
1581
1582        #[cfg(feature = "completion")]
1583        let _complete = Request::<DefaultConfig>::TabComplete {
1584            path: heapless::String::new(),
1585        };
1586
1587        #[cfg(feature = "history")]
1588        let _history = Request::<DefaultConfig>::History {
1589            direction: HistoryDirection::Previous,
1590            buffer: heapless::String::new(),
1591        };
1592    }
1593
1594    #[test]
1595    fn test_activate_deactivate_lifecycle() {
1596        let io = MockIo::new();
1597        let handler = MockHandler;
1598
1599        // Create shell - should start in Inactive state
1600        #[cfg(feature = "authentication")]
1601        {
1602            use crate::auth::CredentialProvider;
1603            struct MockProvider;
1604            impl CredentialProvider<MockLevel> for MockProvider {
1605                type Error = ();
1606                fn find_user(
1607                    &self,
1608                    _username: &str,
1609                ) -> Result<Option<crate::auth::User<MockLevel>>, ()> {
1610                    Ok(None)
1611                }
1612                fn verify_password(
1613                    &self,
1614                    _user: &crate::auth::User<MockLevel>,
1615                    _password: &str,
1616                ) -> bool {
1617                    false
1618                }
1619            }
1620            let provider = MockProvider;
1621            let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1622                Shell::new(&TEST_TREE, handler, &provider, io);
1623
1624            // Should start in Inactive state
1625            assert_eq!(shell.state, CliState::Inactive);
1626            assert!(shell.current_user.is_none());
1627
1628            // Activate should transition to LoggedOut (auth enabled)
1629            shell.activate().unwrap();
1630            assert_eq!(shell.state, CliState::LoggedOut);
1631
1632            // Deactivate should return to Inactive
1633            shell.deactivate();
1634            assert_eq!(shell.state, CliState::Inactive);
1635            assert!(shell.current_user.is_none());
1636            assert!(shell.input_buffer.is_empty());
1637            assert!(shell.current_path.is_empty());
1638        }
1639
1640        #[cfg(not(feature = "authentication"))]
1641        {
1642            let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1643                Shell::new(&TEST_TREE, handler, io);
1644
1645            // Should start in Inactive state
1646            assert_eq!(shell.state, CliState::Inactive);
1647
1648            // Activate should transition to LoggedIn (auth disabled)
1649            shell.activate().unwrap();
1650            assert_eq!(shell.state, CliState::LoggedIn);
1651
1652            // Deactivate should return to Inactive
1653            shell.deactivate();
1654            assert_eq!(shell.state, CliState::Inactive);
1655            assert!(shell.current_user.is_none());
1656            assert!(shell.input_buffer.is_empty());
1657            assert!(shell.current_path.is_empty());
1658        }
1659    }
1660
1661    #[test]
1662    #[cfg(not(feature = "authentication"))]
1663    fn test_write_formatted_response_default() {
1664        // Test default formatting (no flags set)
1665        let io = MockIo::new();
1666        let handler = MockHandler;
1667        let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1668            Shell::new(&TEST_TREE, handler, io);
1669
1670        let response = crate::response::Response::<DefaultConfig>::success("Test message");
1671        shell.write_formatted_response(&response).unwrap();
1672
1673        // Default: message + postfix newline
1674        assert_eq!(shell.io.get_output(), "Test message\r\n");
1675    }
1676
1677    #[test]
1678    #[cfg(not(feature = "authentication"))]
1679    fn test_write_formatted_response_with_prefix_newline() {
1680        let io = MockIo::new();
1681        let handler = MockHandler;
1682        let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1683            Shell::new(&TEST_TREE, handler, io);
1684
1685        let response =
1686            crate::response::Response::<DefaultConfig>::success("Test").with_prefix_newline();
1687        shell.write_formatted_response(&response).unwrap();
1688
1689        // prefix newline + message + postfix newline
1690        assert_eq!(shell.io.get_output(), "\r\nTest\r\n");
1691    }
1692
1693    #[test]
1694    #[cfg(not(feature = "authentication"))]
1695    fn test_write_formatted_response_indented() {
1696        let io = MockIo::new();
1697        let handler = MockHandler;
1698        let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1699            Shell::new(&TEST_TREE, handler, io);
1700
1701        let response =
1702            crate::response::Response::<DefaultConfig>::success("Line 1\r\nLine 2").indented();
1703        shell.write_formatted_response(&response).unwrap();
1704
1705        // Each line indented with 2 spaces + postfix newline
1706        assert_eq!(shell.io.get_output(), "  Line 1\r\n  Line 2\r\n");
1707    }
1708
1709    #[test]
1710    #[cfg(not(feature = "authentication"))]
1711    fn test_write_formatted_response_indented_single_line() {
1712        let io = MockIo::new();
1713        let handler = MockHandler;
1714        let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1715            Shell::new(&TEST_TREE, handler, io);
1716
1717        let response =
1718            crate::response::Response::<DefaultConfig>::success("Single line").indented();
1719        shell.write_formatted_response(&response).unwrap();
1720
1721        // Single line indented
1722        assert_eq!(shell.io.get_output(), "  Single line\r\n");
1723    }
1724
1725    #[test]
1726    #[cfg(not(feature = "authentication"))]
1727    fn test_write_formatted_response_without_postfix_newline() {
1728        let io = MockIo::new();
1729        let handler = MockHandler;
1730        let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1731            Shell::new(&TEST_TREE, handler, io);
1732
1733        let response = crate::response::Response::<DefaultConfig>::success("No newline")
1734            .without_postfix_newline();
1735        shell.write_formatted_response(&response).unwrap();
1736
1737        // Message without trailing newline
1738        assert_eq!(shell.io.get_output(), "No newline");
1739    }
1740
1741    #[test]
1742    #[cfg(not(feature = "authentication"))]
1743    fn test_write_formatted_response_combined_flags() {
1744        let io = MockIo::new();
1745        let handler = MockHandler;
1746        let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1747            Shell::new(&TEST_TREE, handler, io);
1748
1749        let response = crate::response::Response::<DefaultConfig>::success("Multi\r\nLine")
1750            .with_prefix_newline()
1751            .indented();
1752        shell.write_formatted_response(&response).unwrap();
1753
1754        // Prefix newline + indented lines + postfix newline
1755        assert_eq!(shell.io.get_output(), "\r\n  Multi\r\n  Line\r\n");
1756    }
1757
1758    #[test]
1759    #[cfg(not(feature = "authentication"))]
1760    fn test_write_formatted_response_all_flags_off() {
1761        let io = MockIo::new();
1762        let handler = MockHandler;
1763        let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1764            Shell::new(&TEST_TREE, handler, io);
1765
1766        let response =
1767            crate::response::Response::<DefaultConfig>::success("Raw").without_postfix_newline();
1768        shell.write_formatted_response(&response).unwrap();
1769
1770        // No formatting at all
1771        assert_eq!(shell.io.get_output(), "Raw");
1772    }
1773
1774    #[test]
1775    #[cfg(not(feature = "authentication"))]
1776    fn test_write_formatted_response_empty_message() {
1777        let io = MockIo::new();
1778        let handler = MockHandler;
1779        let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1780            Shell::new(&TEST_TREE, handler, io);
1781
1782        let response = crate::response::Response::<DefaultConfig>::success("");
1783        shell.write_formatted_response(&response).unwrap();
1784
1785        // Empty message still gets postfix newline
1786        assert_eq!(shell.io.get_output(), "\r\n");
1787    }
1788
1789    #[test]
1790    #[cfg(not(feature = "authentication"))]
1791    fn test_write_formatted_response_indented_multiline() {
1792        let io = MockIo::new();
1793        let handler = MockHandler;
1794        let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1795            Shell::new(&TEST_TREE, handler, io);
1796
1797        let response = crate::response::Response::<DefaultConfig>::success("A\r\nB\r\nC\r\nD")
1798            .indented()
1799            .without_postfix_newline();
1800        shell.write_formatted_response(&response).unwrap();
1801
1802        // All 4 lines indented, no trailing newline
1803        assert_eq!(shell.io.get_output(), "  A\r\n  B\r\n  C\r\n  D");
1804    }
1805
1806    #[test]
1807    #[cfg(not(feature = "authentication"))]
1808    fn test_inline_message_flag() {
1809        // Test that inline_message flag is properly recognized
1810        let response =
1811            crate::response::Response::<DefaultConfig>::success("... processing").inline();
1812
1813        assert!(
1814            response.inline_message,
1815            "inline() should set inline_message flag"
1816        );
1817
1818        // Note: The actual inline behavior (no newline after input) is tested
1819        // via integration tests, as it requires simulating full command execution.
1820        // This test verifies the flag is set correctly.
1821    }
1822
1823    #[test]
1824    #[cfg(not(feature = "authentication"))]
1825    fn test_resolve_path_cannot_navigate_through_command() {
1826        // Test that resolve_path returns InvalidPath when trying to navigate through a command
1827        let io = MockIo::new();
1828        let handler = MockHandler;
1829        let shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1830            Shell::new(&TEST_TREE, handler, io);
1831
1832        // Valid: Command as last segment should succeed
1833        let result = shell.resolve_path("test-cmd");
1834        assert!(result.is_ok(), "Should resolve path to command");
1835        if let Ok((node, _)) = result {
1836            assert!(node.is_some());
1837            if let Some(Node::Command(cmd)) = node {
1838                assert_eq!(cmd.name, "test-cmd");
1839            } else {
1840                panic!("Expected Command node");
1841            }
1842        }
1843
1844        // Invalid: Cannot navigate through command to another segment
1845        let result = shell.resolve_path("test-cmd/invalid");
1846        assert!(
1847            result.is_err(),
1848            "Should fail when navigating through command"
1849        );
1850        assert_eq!(
1851            result.unwrap_err(),
1852            CliError::InvalidPath,
1853            "Should return InvalidPath when trying to navigate through command"
1854        );
1855
1856        // Invalid: Multiple segments after command
1857        let result = shell.resolve_path("test-cmd/extra/path");
1858        assert!(
1859            result.is_err(),
1860            "Should fail with multiple segments after command"
1861        );
1862        assert_eq!(
1863            result.unwrap_err(),
1864            CliError::InvalidPath,
1865            "Should return InvalidPath for multiple segments after command"
1866        );
1867    }
1868
1869    #[test]
1870    #[cfg(not(feature = "authentication"))]
1871    fn test_resolve_path_comprehensive() {
1872        let io = MockIo::new();
1873        let handler = MockHandler;
1874        let shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1875            Shell::new(&TEST_TREE, handler, io);
1876
1877        // Test 1: Root level command
1878        let result = shell.resolve_path("test-cmd");
1879        assert!(result.is_ok(), "Should resolve root-level command");
1880        if let Ok((node, _)) = result {
1881            assert!(node.is_some());
1882            if let Some(Node::Command(cmd)) = node {
1883                assert_eq!(cmd.name, "test-cmd");
1884            }
1885        }
1886
1887        // Test 2: Verify command metadata properties
1888        let result = shell.resolve_path("system/reboot");
1889        assert!(result.is_ok(), "Should resolve system/reboot");
1890        if let Ok((node, _)) = result {
1891            assert!(node.is_some());
1892            if let Some(Node::Command(cmd)) = node {
1893                assert_eq!(cmd.name, "reboot");
1894                assert_eq!(cmd.description, "Reboot the system");
1895                assert_eq!(cmd.access_level, MockLevel::User);
1896                assert_eq!(cmd.kind, CommandKind::Sync);
1897            }
1898        }
1899
1900        // Test 3: Verify unique command ID (critical for handler dispatch)
1901        let result = shell.resolve_path("system/status");
1902        assert!(result.is_ok(), "Should resolve system/status");
1903        if let Ok((node, _)) = result {
1904            assert!(node.is_some());
1905            if let Some(Node::Command(cmd)) = node {
1906                assert_eq!(cmd.name, "status");
1907                assert_eq!(cmd.id, "status");
1908                // Verify this is different from network/status which has id "network_status"
1909            }
1910        }
1911
1912        // Test 4: Second-level nested command (system/network/status)
1913        let result = shell.resolve_path("system/network/status");
1914        assert!(result.is_ok(), "Should resolve system/network/status");
1915        if let Ok((node, _)) = result {
1916            assert!(node.is_some());
1917            if let Some(Node::Command(cmd)) = node {
1918                assert_eq!(cmd.name, "status");
1919            }
1920        }
1921
1922        // Test 5: Second-level nested command (system/hardware/led)
1923        let result = shell.resolve_path("system/hardware/led");
1924        assert!(result.is_ok(), "Should resolve system/hardware/led");
1925        if let Ok((node, _)) = result {
1926            assert!(node.is_some());
1927            if let Some(Node::Command(cmd)) = node {
1928                assert_eq!(cmd.name, "led");
1929                assert_eq!(cmd.min_args, 1);
1930                assert_eq!(cmd.max_args, 1);
1931            }
1932        }
1933
1934        // Test 6: Non-existent command at root
1935        let result = shell.resolve_path("nonexistent");
1936        assert!(result.is_err(), "Should fail for non-existent command");
1937        assert_eq!(
1938            result.unwrap_err(),
1939            CliError::CommandNotFound,
1940            "Should return CommandNotFound for non-existent command"
1941        );
1942
1943        // Test 7: Invalid path (nonexistent directory)
1944        let result = shell.resolve_path("invalid/path/command");
1945        assert!(result.is_err(), "Should fail for nonexistent path");
1946        assert_eq!(
1947            result.unwrap_err(),
1948            CliError::CommandNotFound,
1949            "Should return CommandNotFound when first segment doesn't exist"
1950        );
1951
1952        // Test 7b: Invalid path (attempting to navigate through a command)
1953        let result = shell.resolve_path("test-cmd/something");
1954        assert!(
1955            result.is_err(),
1956            "Should fail when navigating through command"
1957        );
1958        assert_eq!(
1959            result.unwrap_err(),
1960            CliError::InvalidPath,
1961            "Should return InvalidPath when trying to navigate through a command"
1962        );
1963
1964        // Test 8: Resolve to directory (system)
1965        let result = shell.resolve_path("system");
1966        assert!(result.is_ok(), "Should resolve directory path");
1967        if let Ok((node, _)) = result {
1968            assert!(node.is_some());
1969            if let Some(Node::Directory(dir)) = node {
1970                assert_eq!(dir.name, "system");
1971            }
1972        }
1973
1974        // Test 9: Resolve nested directory (system/network)
1975        let result = shell.resolve_path("system/network");
1976        assert!(result.is_ok(), "Should resolve nested directory");
1977        if let Ok((node, _)) = result {
1978            assert!(node.is_some());
1979            if let Some(Node::Directory(dir)) = node {
1980                assert_eq!(dir.name, "network");
1981            }
1982        }
1983    }
1984
1985    #[test]
1986    #[cfg(not(feature = "authentication"))]
1987    fn test_resolve_path_parent_directory() {
1988        let io = MockIo::new();
1989        let handler = MockHandler;
1990        let shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1991            Shell::new(&TEST_TREE, handler, io);
1992
1993        // Test 1: Navigate into directory then back up with ..
1994        // First navigate to system/network/status
1995        let result = shell.resolve_path("system/network/status");
1996        assert!(result.is_ok(), "Should resolve system/network/status");
1997        let (_, path) = result.unwrap();
1998        // Path should have indices for system (1), network (3)
1999        // [1] = system (index 1 in children of root: test-cmd, system)
2000        // [3] = network (index 3 in children of system: reboot, status, hardware, network)
2001        assert_eq!(
2002            path.len(),
2003            2,
2004            "Path should have 2 elements (system, network)"
2005        );
2006        assert_eq!(path[0], 1, "system should be at index 1 in root");
2007        assert_eq!(path[1], 3, "network should be at index 3 in system");
2008
2009        // Test 2: Use .. to go back to system from system/network
2010        let result = shell.resolve_path("system/network/..");
2011        assert!(result.is_ok(), "Should resolve system/network/..");
2012        if let Ok((node, path)) = result {
2013            assert!(node.is_some());
2014            if let Some(Node::Directory(dir)) = node {
2015                assert_eq!(dir.name, "system", "Should be back at system directory");
2016            }
2017            assert_eq!(path.len(), 1, "Path should have 1 element");
2018            assert_eq!(path[0], 1, "system should be at index 1 in root");
2019        }
2020
2021        // Test 3: Multiple .. to go back to root
2022        let result = shell.resolve_path("system/network/../..");
2023        assert!(result.is_ok(), "Should resolve system/network/../..");
2024        if let Ok((node, path)) = result {
2025            assert_eq!(path.len(), 0, "Path should be empty (at root)");
2026            assert!(node.is_none(), "Node should be None (representing root)");
2027        }
2028
2029        // Test 4: Go beyond root with .. (should stay at root)
2030        let result = shell.resolve_path("..");
2031        assert!(result.is_ok(), "Should handle .. at root");
2032        if let Ok((node, path)) = result {
2033            assert_eq!(path.len(), 0, "Path should stay at root");
2034            assert!(node.is_none(), "Node should be None (representing root)");
2035        }
2036    }
2037}