Skip to main content

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                if !args.is_empty() {
793                    return Err(CliError::InvalidArgumentCount {
794                        expected_min: 0,
795                        expected_max: 0,
796                        received: args.len(),
797                    });
798                }
799                // Directory navigation - update path and return
800                self.current_path = new_path;
801                #[cfg(feature = "history")]
802                return Ok(Response::success("")
803                    .without_history()
804                    .without_postfix_newline());
805                #[cfg(not(feature = "history"))]
806                return Ok(Response::success("").without_postfix_newline());
807            }
808            Some(Node::Command(cmd_meta)) => {
809                // Case 2: Tree command execution
810                // Check access control - use InvalidPath for security (don't reveal access denied)
811                if let Some(user) = &self.current_user
812                    && user.access_level < cmd_meta.access_level
813                {
814                    return Err(CliError::InvalidPath);
815                }
816
817                // Validate argument count
818                if args.len() < cmd_meta.min_args || args.len() > cmd_meta.max_args {
819                    return Err(CliError::InvalidArgumentCount {
820                        expected_min: cmd_meta.min_args,
821                        expected_max: cmd_meta.max_args,
822                        received: args.len(),
823                    });
824                }
825
826                // Dispatch to command handler
827                match cmd_meta.kind {
828                    CommandKind::Sync => {
829                        // Execute synchronous tree command (dispatch by unique ID)
830                        self.handler.execute_sync(cmd_meta.id, args)
831                    }
832                    #[cfg(feature = "async")]
833                    CommandKind::Async => {
834                        // Async tree command called from sync context
835                        Err(CliError::AsyncInSyncContext)
836                    }
837                }
838            }
839        }
840    }
841
842    /// Execute a tree path (navigation or command execution) - async version.
843    ///
844    /// Resolves the path and either:
845    /// - Navigates to a directory (if path resolves to Node::Directory)
846    /// - Executes a tree command (if path resolves to Node::Command)
847    ///
848    /// This async version can execute both sync and async commands.
849    /// Sync commands are called directly, async commands are awaited.
850    ///
851    /// Note: "command" here refers specifically to Node::Command,
852    /// not generic user input.
853    #[cfg(feature = "async")]
854    async fn execute_tree_path_async(&mut self, input: &str) -> Result<Response<C>, CliError> {
855        // Parse path and arguments
856        // TODO: Use C::MAX_ARGS + 1 when const generics stabilize (command + args)
857        let parts: heapless::Vec<&str, 17> = input.split_whitespace().collect();
858        if parts.is_empty() {
859            return Err(CliError::CommandNotFound);
860        }
861
862        let path_str = parts[0];
863        let args = &parts[1..];
864
865        // Resolve path to node (None represents root directory)
866        let (target_node, new_path) = self.resolve_path(path_str)?;
867
868        // Case 1: Directory navigation
869        match target_node {
870            None | Some(Node::Directory(_)) => {
871                if !args.is_empty() {
872                    return Err(CliError::InvalidArgumentCount {
873                        expected_min: 0,
874                        expected_max: 0,
875                        received: args.len(),
876                    });
877                }
878                // Directory navigation - update path and return
879                self.current_path = new_path;
880                #[cfg(feature = "history")]
881                return Ok(Response::success("")
882                    .without_history()
883                    .without_postfix_newline());
884                #[cfg(not(feature = "history"))]
885                return Ok(Response::success("").without_postfix_newline());
886            }
887            Some(Node::Command(cmd_meta)) => {
888                // Case 2: Tree command execution
889                // Check access control - use InvalidPath for security (don't reveal access denied)
890                if let Some(user) = &self.current_user
891                    && user.access_level < cmd_meta.access_level
892                {
893                    return Err(CliError::InvalidPath);
894                }
895
896                // Validate argument count
897                if args.len() < cmd_meta.min_args || args.len() > cmd_meta.max_args {
898                    return Err(CliError::InvalidArgumentCount {
899                        expected_min: cmd_meta.min_args,
900                        expected_max: cmd_meta.max_args,
901                        received: args.len(),
902                    });
903                }
904
905                // Dispatch to command handler (handle both sync and async)
906                match cmd_meta.kind {
907                    CommandKind::Sync => {
908                        // Sync command in async context - call directly
909                        self.handler.execute_sync(cmd_meta.id, args)
910                    }
911                    CommandKind::Async => {
912                        // Async command - await execution
913                        self.handler.execute_async(cmd_meta.id, args).await
914                    }
915                }
916            }
917        }
918    }
919
920    /// Resolve a path string to a node.
921    ///
922    /// Returns (node, path_stack) where path_stack is the navigation path.
923    /// Node is None when path resolves to root directory.
924    // TODO: Use C::MAX_PATH_DEPTH when const generics stabilize
925    fn resolve_path(
926        &self,
927        path_str: &str,
928    ) -> Result<(Option<&'tree Node<L>>, heapless::Vec<usize, 8>), CliError> {
929        // Start from current directory or root
930        // TODO: Use C::MAX_PATH_DEPTH when const generics stabilize
931        let mut working_path: heapless::Vec<usize, 8> = if path_str.starts_with('/') {
932            heapless::Vec::new() // Absolute path starts from root
933        } else {
934            self.current_path.clone() // Relative path starts from current
935        };
936
937        // Parse path
938        // TODO: Use C::MAX_PATH_DEPTH when const generics stabilize
939        let segments: heapless::Vec<&str, 8> = path_str
940            .trim_start_matches('/')
941            .split('/')
942            .filter(|s| !s.is_empty() && *s != ".")
943            .collect();
944
945        // Navigate through segments
946        for (seg_idx, segment) in segments.iter().enumerate() {
947            if *segment == ".." {
948                // Parent directory
949                working_path.pop();
950                continue;
951            }
952
953            let is_last_segment = seg_idx == segments.len() - 1;
954
955            // Find child with this name
956            let current_dir = self.get_dir_at_path(&working_path)?;
957            let mut found = false;
958
959            for (index, child) in current_dir.children.iter().enumerate() {
960                // Check access control
961                let node_level = match child {
962                    Node::Command(cmd) => cmd.access_level,
963                    Node::Directory(dir) => dir.access_level,
964                };
965
966                if let Some(user) = &self.current_user
967                    && user.access_level < node_level
968                {
969                    continue; // User lacks access, skip this node
970                }
971
972                if child.name() == *segment {
973                    // Found it!
974                    if child.is_directory() {
975                        // Navigate into directory
976                        working_path
977                            .push(index)
978                            .map_err(|_| CliError::PathTooDeep)?;
979                    } else {
980                        // It's a command - can only return if this is the last segment
981                        if is_last_segment {
982                            return Ok((Some(child), working_path));
983                        } else {
984                            // Trying to navigate through a command - invalid path structure
985                            return Err(CliError::InvalidPath);
986                        }
987                    }
988                    found = true;
989                    break;
990                }
991            }
992
993            if !found {
994                return Err(CliError::CommandNotFound);
995            }
996        }
997
998        // Path resolved to a directory
999        // Handle root directory specially (when path is empty)
1000        if working_path.is_empty() {
1001            // Return None to represent root directory
1002            return Ok((None, working_path));
1003        }
1004
1005        let dir_node = self.get_node_at_path(&working_path)?;
1006        Ok((Some(dir_node), working_path))
1007    }
1008
1009    /// Get directory at specific path.
1010    // TODO: Use C::MAX_PATH_DEPTH when const generics stabilize
1011    fn get_dir_at_path(
1012        &self,
1013        path: &heapless::Vec<usize, 8>,
1014    ) -> Result<&'tree Directory<L>, CliError> {
1015        let mut current: &Directory<L> = self.tree;
1016
1017        for &index in path.iter() {
1018            match current.children.get(index) {
1019                Some(Node::Directory(dir)) => current = dir,
1020                Some(Node::Command(_)) | None => return Err(CliError::InvalidPath),
1021            }
1022        }
1023
1024        Ok(current)
1025    }
1026
1027    /// Get node at specific path.
1028    // TODO: Use C::MAX_PATH_DEPTH when const generics stabilize
1029    fn get_node_at_path(&self, path: &heapless::Vec<usize, 8>) -> Result<&'tree Node<L>, CliError> {
1030        if path.is_empty() {
1031            // Root directory - need to find a way to return it as a Node
1032            // For now, return error since we can't construct Node::Directory here
1033            return Err(CliError::InvalidPath);
1034        }
1035
1036        // TODO: Use C::MAX_PATH_DEPTH when const generics stabilize
1037        let parent_path: heapless::Vec<usize, 8> =
1038            path.iter().take(path.len() - 1).copied().collect();
1039        let parent_dir = self.get_dir_at_path(&parent_path)?;
1040
1041        let last_index = *path.last().ok_or(CliError::InvalidPath)?;
1042        parent_dir
1043            .children
1044            .get(last_index)
1045            .ok_or(CliError::InvalidPath)
1046    }
1047
1048    /// Handle Tab completion.
1049    fn handle_tab(&mut self) -> Result<(), IO::Error> {
1050        #[cfg(feature = "completion")]
1051        {
1052            // Get current directory
1053            let current_dir = match self.get_current_dir() {
1054                Ok(dir) => dir,
1055                Err(_) => return self.generate_and_write_prompt(), // Error, just redraw prompt
1056            };
1057
1058            // Suggest completions
1059            let result = suggest_completions::<L, 16>(
1060                current_dir,
1061                self.input_buffer.as_str(),
1062                self.current_user.as_ref(),
1063            );
1064
1065            match result {
1066                Ok(crate::tree::completion::CompletionResult::Single { completion, .. }) => {
1067                    // Single match - replace buffer and update display
1068                    self.input_buffer.clear();
1069                    match self.input_buffer.push_str(&completion) {
1070                        Ok(()) => {
1071                            // Redraw line
1072                            self.io.write_str("\r")?; // Carriage return
1073                            let prompt = self.generate_prompt();
1074                            self.io.write_str(prompt.as_str())?;
1075                            self.io.write_str(self.input_buffer.as_str())?;
1076                        }
1077                        Err(_) => {
1078                            // Completion too long for buffer - beep
1079                            self.io.put_char('\x07')?;
1080                        }
1081                    }
1082                }
1083                Ok(crate::tree::completion::CompletionResult::Multiple { all_matches, .. }) => {
1084                    // Multiple matches - show them
1085                    self.io.write_str("\r\n")?;
1086                    for m in all_matches.iter() {
1087                        self.io.write_str("  ")?; // 2-space indentation
1088                        self.io.write_str(m.as_str())?;
1089                        self.io.write_str("  ")?;
1090                    }
1091                    self.io.write_str("\r\n")?;
1092                    self.generate_and_write_prompt()?;
1093                    self.io.write_str(self.input_buffer.as_str())?;
1094                }
1095                _ => {
1096                    // No matches or error - just beep
1097                    self.io.put_char('\x07')?; // Bell character
1098                }
1099            }
1100        }
1101
1102        #[cfg(not(feature = "completion"))]
1103        {
1104            // Completion disabled - just beep
1105            self.io.put_char('\x07')?; // Bell character
1106        }
1107
1108        Ok(())
1109    }
1110
1111    /// Handle history navigation.
1112    fn handle_history(&mut self, direction: HistoryDirection) -> Result<(), IO::Error> {
1113        #[cfg(feature = "history")]
1114        {
1115            let history_entry = match direction {
1116                HistoryDirection::Previous => self.history.previous_command(),
1117                HistoryDirection::Next => self.history.next_command(),
1118            };
1119
1120            if let Some(entry) = history_entry {
1121                // Replace buffer with history entry
1122                self.input_buffer = entry;
1123                // Redraw line
1124                self.clear_line_and_redraw()?;
1125            }
1126        }
1127
1128        #[cfg(not(feature = "history"))]
1129        {
1130            // History disabled - ignore
1131            let _ = direction; // Silence unused warning
1132        }
1133
1134        Ok(())
1135    }
1136
1137    /// Show help (? command).
1138    fn show_help(&mut self) -> Result<(), IO::Error> {
1139        self.io.write_str("  ?        - List global commands\r\n")?;
1140        self.io
1141            .write_str("  ls       - List directory contents\r\n")?;
1142
1143        #[cfg(feature = "authentication")]
1144        self.io.write_str("  logout   - End session\r\n")?;
1145
1146        self.io.write_str("  clear    - Clear screen\r\n")?;
1147        self.io.write_str("  ESC ESC  - Clear input buffer\r\n")?;
1148
1149        Ok(())
1150    }
1151
1152    /// Show directory listing (ls command).
1153    fn show_ls(&mut self) -> Result<(), IO::Error> {
1154        let current_dir = match self.get_current_dir() {
1155            Ok(dir) => dir,
1156            Err(_) => {
1157                self.io.write_str("Error accessing directory\r\n")?;
1158                return Ok(());
1159            }
1160        };
1161
1162        for child in current_dir.children.iter() {
1163            // Check access control
1164            let node_level = match child {
1165                Node::Command(cmd) => cmd.access_level,
1166                Node::Directory(dir) => dir.access_level,
1167            };
1168
1169            if let Some(user) = &self.current_user
1170                && user.access_level < node_level
1171            {
1172                continue; // User lacks access, skip this node
1173            }
1174
1175            // Format output
1176            match child {
1177                Node::Command(cmd) => {
1178                    self.io.write_str("  ")?;
1179                    self.io.write_str(cmd.name)?;
1180                    self.io.write_str("  - ")?;
1181                    self.io.write_str(cmd.description)?;
1182                    self.io.write_str("\r\n")?;
1183                }
1184                Node::Directory(dir) => {
1185                    self.io.write_str("  ")?;
1186                    self.io.write_str(dir.name)?;
1187                    self.io.write_str("/  - Directory\r\n")?;
1188                }
1189            }
1190        }
1191
1192        Ok(())
1193    }
1194
1195    /// Clear current line and redraw with prompt and buffer.
1196    fn clear_line_and_redraw(&mut self) -> Result<(), IO::Error> {
1197        self.io.write_str("\r\x1b[K")?; // CR + clear to end of line
1198        self.generate_and_write_prompt()?;
1199        self.io.write_str(self.input_buffer.as_str())?;
1200        Ok(())
1201    }
1202
1203    // ========================================
1204    // I/O Access
1205    // ========================================
1206
1207    /// Get reference to I/O interface (for inspection or direct control).
1208    pub fn io(&self) -> &IO {
1209        &self.io
1210    }
1211
1212    /// Get mutable reference to I/O interface (for manipulation or direct control).
1213    pub fn io_mut(&mut self) -> &mut IO {
1214        &mut self.io
1215    }
1216}
1217
1218// ============================================================================
1219// Tests
1220// ============================================================================
1221
1222#[cfg(test)]
1223mod tests {
1224    use super::*;
1225    use crate::auth::AccessLevel;
1226    use crate::config::DefaultConfig;
1227    use crate::io::CharIo;
1228    use crate::tree::{CommandKind, CommandMeta, Directory, Node};
1229
1230    // Mock access level
1231    #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
1232    enum MockLevel {
1233        User = 0,
1234    }
1235
1236    impl AccessLevel for MockLevel {
1237        fn from_str(s: &str) -> Option<Self> {
1238            match s {
1239                "User" => Some(Self::User),
1240                _ => None,
1241            }
1242        }
1243
1244        fn as_str(&self) -> &'static str {
1245            "User"
1246        }
1247    }
1248
1249    // Mock I/O that captures output
1250    struct MockIo {
1251        output: heapless::String<512>,
1252    }
1253    impl MockIo {
1254        fn new() -> Self {
1255            Self {
1256                output: heapless::String::new(),
1257            }
1258        }
1259
1260        #[allow(dead_code)]
1261        fn get_output(&self) -> &str {
1262            &self.output
1263        }
1264    }
1265    impl CharIo for MockIo {
1266        type Error = ();
1267        fn get_char(&mut self) -> Result<Option<char>, ()> {
1268            Ok(None)
1269        }
1270        fn put_char(&mut self, c: char) -> Result<(), ()> {
1271            self.output.push(c).map_err(|_| ())
1272        }
1273        fn write_str(&mut self, s: &str) -> Result<(), ()> {
1274            self.output.push_str(s).map_err(|_| ())
1275        }
1276    }
1277
1278    // Mock handler
1279    struct MockHandler;
1280    impl CommandHandler<DefaultConfig> for MockHandler {
1281        fn execute_sync(
1282            &self,
1283            _id: &str,
1284            _args: &[&str],
1285        ) -> Result<crate::response::Response<DefaultConfig>, crate::error::CliError> {
1286            Err(crate::error::CliError::CommandNotFound)
1287        }
1288
1289        #[cfg(feature = "async")]
1290        async fn execute_async(
1291            &self,
1292            _id: &str,
1293            _args: &[&str],
1294        ) -> Result<crate::response::Response<DefaultConfig>, crate::error::CliError> {
1295            Err(crate::error::CliError::CommandNotFound)
1296        }
1297    }
1298
1299    // Test commands
1300    const CMD_TEST: CommandMeta<MockLevel> = CommandMeta {
1301        id: "test-cmd",
1302        name: "test-cmd",
1303        description: "Test command",
1304        access_level: MockLevel::User,
1305        kind: CommandKind::Sync,
1306        min_args: 0,
1307        max_args: 0,
1308    };
1309
1310    const CMD_REBOOT: CommandMeta<MockLevel> = CommandMeta {
1311        id: "reboot",
1312        name: "reboot",
1313        description: "Reboot the system",
1314        access_level: MockLevel::User,
1315        kind: CommandKind::Sync,
1316        min_args: 0,
1317        max_args: 0,
1318    };
1319
1320    const CMD_STATUS: CommandMeta<MockLevel> = CommandMeta {
1321        id: "status",
1322        name: "status",
1323        description: "Show status",
1324        access_level: MockLevel::User,
1325        kind: CommandKind::Sync,
1326        min_args: 0,
1327        max_args: 0,
1328    };
1329
1330    const CMD_LED: CommandMeta<MockLevel> = CommandMeta {
1331        id: "led",
1332        name: "led",
1333        description: "Control LED",
1334        access_level: MockLevel::User,
1335        kind: CommandKind::Sync,
1336        min_args: 1,
1337        max_args: 1,
1338    };
1339
1340    const CMD_NETWORK_STATUS: CommandMeta<MockLevel> = CommandMeta {
1341        id: "network_status",
1342        name: "status",
1343        description: "Network status",
1344        access_level: MockLevel::User,
1345        kind: CommandKind::Sync,
1346        min_args: 0,
1347        max_args: 0,
1348    };
1349
1350    // Test directories
1351    const DIR_HARDWARE: Directory<MockLevel> = Directory {
1352        name: "hardware",
1353        children: &[Node::Command(&CMD_LED)],
1354        access_level: MockLevel::User,
1355    };
1356
1357    const DIR_NETWORK: Directory<MockLevel> = Directory {
1358        name: "network",
1359        children: &[Node::Command(&CMD_NETWORK_STATUS)],
1360        access_level: MockLevel::User,
1361    };
1362
1363    const DIR_SYSTEM: Directory<MockLevel> = Directory {
1364        name: "system",
1365        children: &[
1366            Node::Command(&CMD_REBOOT),
1367            Node::Command(&CMD_STATUS),
1368            Node::Directory(&DIR_HARDWARE),
1369            Node::Directory(&DIR_NETWORK),
1370        ],
1371        access_level: MockLevel::User,
1372    };
1373
1374    // Test tree
1375    const TEST_TREE: Directory<MockLevel> = Directory {
1376        name: "/",
1377        children: &[Node::Command(&CMD_TEST), Node::Directory(&DIR_SYSTEM)],
1378        access_level: MockLevel::User,
1379    };
1380
1381    #[test]
1382    fn test_request_command_no_args() {
1383        let mut path = heapless::String::<128>::new();
1384        path.push_str("help").unwrap();
1385        let args = heapless::Vec::new();
1386        #[cfg(feature = "history")]
1387        let original = {
1388            let mut s = heapless::String::<128>::new();
1389            s.push_str("help").unwrap();
1390            s
1391        };
1392
1393        let request = Request::<DefaultConfig>::Command {
1394            path,
1395            args,
1396            #[cfg(feature = "history")]
1397            original,
1398            _phantom: core::marker::PhantomData,
1399        };
1400
1401        match request {
1402            Request::Command { path, args, .. } => {
1403                assert_eq!(path.as_str(), "help");
1404                assert_eq!(args.len(), 0);
1405            }
1406            #[allow(unreachable_patterns)]
1407            _ => panic!("Expected Command variant"),
1408        }
1409    }
1410
1411    #[test]
1412    fn test_request_command_with_args() {
1413        let mut path = heapless::String::<128>::new();
1414        path.push_str("echo").unwrap();
1415
1416        let mut args = heapless::Vec::new();
1417        let mut hello = heapless::String::<128>::new();
1418        hello.push_str("hello").unwrap();
1419        let mut world = heapless::String::<128>::new();
1420        world.push_str("world").unwrap();
1421        args.push(hello).unwrap();
1422        args.push(world).unwrap();
1423
1424        #[cfg(feature = "history")]
1425        let original = {
1426            let mut s = heapless::String::<128>::new();
1427            s.push_str("echo hello world").unwrap();
1428            s
1429        };
1430
1431        let request = Request::<DefaultConfig>::Command {
1432            path,
1433            args,
1434            #[cfg(feature = "history")]
1435            original,
1436            _phantom: core::marker::PhantomData,
1437        };
1438
1439        match request {
1440            Request::Command { path, args, .. } => {
1441                assert_eq!(path.as_str(), "echo");
1442                assert_eq!(args.len(), 2);
1443                assert_eq!(args[0].as_str(), "hello");
1444                assert_eq!(args[1].as_str(), "world");
1445            }
1446            #[allow(unreachable_patterns)]
1447            _ => panic!("Expected Command variant"),
1448        }
1449    }
1450
1451    #[test]
1452    #[cfg(feature = "history")]
1453    fn test_request_command_with_original() {
1454        let mut path = heapless::String::<128>::new();
1455        path.push_str("reboot").unwrap();
1456        let mut original = heapless::String::<128>::new();
1457        original.push_str("reboot").unwrap();
1458
1459        let request = Request::<DefaultConfig>::Command {
1460            path,
1461            args: heapless::Vec::new(),
1462            original,
1463            _phantom: core::marker::PhantomData,
1464        };
1465
1466        match request {
1467            Request::Command { path, original, .. } => {
1468                assert_eq!(path.as_str(), "reboot");
1469                assert_eq!(original.as_str(), "reboot");
1470            }
1471            #[allow(unreachable_patterns)]
1472            _ => panic!("Expected Command variant"),
1473        }
1474    }
1475
1476    #[test]
1477    #[cfg(feature = "authentication")]
1478    fn test_request_login() {
1479        let mut username = heapless::String::<32>::new();
1480        username.push_str("admin").unwrap();
1481        let mut password = heapless::String::<64>::new();
1482        password.push_str("secret123").unwrap();
1483
1484        let request = Request::<DefaultConfig>::Login { username, password };
1485
1486        match request {
1487            Request::Login { username, password } => {
1488                assert_eq!(username.as_str(), "admin");
1489                assert_eq!(password.as_str(), "secret123");
1490            }
1491            #[allow(unreachable_patterns)]
1492            _ => panic!("Expected Login variant"),
1493        }
1494    }
1495
1496    #[test]
1497    #[cfg(feature = "authentication")]
1498    fn test_request_invalid_login() {
1499        let request = Request::<DefaultConfig>::InvalidLogin;
1500
1501        match request {
1502            Request::InvalidLogin => {}
1503            #[allow(unreachable_patterns)]
1504            _ => panic!("Expected InvalidLogin variant"),
1505        }
1506    }
1507
1508    #[test]
1509    #[cfg(feature = "completion")]
1510    fn test_request_tab_complete() {
1511        let mut path = heapless::String::<128>::new();
1512        path.push_str("sys").unwrap();
1513
1514        let request = Request::<DefaultConfig>::TabComplete { path };
1515
1516        match request {
1517            Request::TabComplete { path } => {
1518                assert_eq!(path.as_str(), "sys");
1519            }
1520            #[allow(unreachable_patterns)]
1521            _ => panic!("Expected TabComplete variant"),
1522        }
1523    }
1524
1525    #[test]
1526    #[cfg(feature = "completion")]
1527    fn test_request_tab_complete_empty() {
1528        let request = Request::<DefaultConfig>::TabComplete {
1529            path: heapless::String::new(),
1530        };
1531
1532        match request {
1533            Request::TabComplete { path } => {
1534                assert_eq!(path.as_str(), "");
1535            }
1536            #[allow(unreachable_patterns)]
1537            _ => panic!("Expected TabComplete variant"),
1538        }
1539    }
1540
1541    #[test]
1542    #[cfg(feature = "history")]
1543    fn test_request_history_previous() {
1544        let mut buffer = heapless::String::<128>::new();
1545        buffer.push_str("current input").unwrap();
1546
1547        let request = Request::<DefaultConfig>::History {
1548            direction: HistoryDirection::Previous,
1549            buffer,
1550        };
1551
1552        match request {
1553            Request::History { direction, buffer } => {
1554                assert_eq!(direction, HistoryDirection::Previous);
1555                assert_eq!(buffer.as_str(), "current input");
1556            }
1557            #[allow(unreachable_patterns)]
1558            _ => panic!("Expected History variant"),
1559        }
1560    }
1561
1562    #[test]
1563    #[cfg(feature = "history")]
1564    fn test_request_history_next() {
1565        let request = Request::<DefaultConfig>::History {
1566            direction: HistoryDirection::Next,
1567            buffer: heapless::String::new(),
1568        };
1569
1570        match request {
1571            Request::History { direction, buffer } => {
1572                assert_eq!(direction, HistoryDirection::Next);
1573                assert_eq!(buffer.as_str(), "");
1574            }
1575            #[allow(unreachable_patterns)]
1576            _ => panic!("Expected History variant"),
1577        }
1578    }
1579
1580    #[test]
1581    fn test_request_variants_match_features() {
1582        let _cmd = Request::<DefaultConfig>::Command {
1583            path: heapless::String::new(),
1584            args: heapless::Vec::new(),
1585            #[cfg(feature = "history")]
1586            original: heapless::String::new(),
1587            _phantom: core::marker::PhantomData,
1588        };
1589
1590        #[cfg(feature = "authentication")]
1591        let _login = Request::<DefaultConfig>::Login {
1592            username: heapless::String::new(),
1593            password: heapless::String::new(),
1594        };
1595
1596        #[cfg(feature = "completion")]
1597        let _complete = Request::<DefaultConfig>::TabComplete {
1598            path: heapless::String::new(),
1599        };
1600
1601        #[cfg(feature = "history")]
1602        let _history = Request::<DefaultConfig>::History {
1603            direction: HistoryDirection::Previous,
1604            buffer: heapless::String::new(),
1605        };
1606    }
1607
1608    #[test]
1609    fn test_activate_deactivate_lifecycle() {
1610        let io = MockIo::new();
1611        let handler = MockHandler;
1612
1613        // Create shell - should start in Inactive state
1614        #[cfg(feature = "authentication")]
1615        {
1616            use crate::auth::CredentialProvider;
1617            struct MockProvider;
1618            impl CredentialProvider<MockLevel> for MockProvider {
1619                type Error = ();
1620                fn find_user(
1621                    &self,
1622                    _username: &str,
1623                ) -> Result<Option<crate::auth::User<MockLevel>>, ()> {
1624                    Ok(None)
1625                }
1626                fn verify_password(
1627                    &self,
1628                    _user: &crate::auth::User<MockLevel>,
1629                    _password: &str,
1630                ) -> bool {
1631                    false
1632                }
1633            }
1634            let provider = MockProvider;
1635            let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1636                Shell::new(&TEST_TREE, handler, &provider, io);
1637
1638            // Should start in Inactive state
1639            assert_eq!(shell.state, CliState::Inactive);
1640            assert!(shell.current_user.is_none());
1641
1642            // Activate should transition to LoggedOut (auth enabled)
1643            shell.activate().unwrap();
1644            assert_eq!(shell.state, CliState::LoggedOut);
1645
1646            // Deactivate should return to Inactive
1647            shell.deactivate();
1648            assert_eq!(shell.state, CliState::Inactive);
1649            assert!(shell.current_user.is_none());
1650            assert!(shell.input_buffer.is_empty());
1651            assert!(shell.current_path.is_empty());
1652        }
1653
1654        #[cfg(not(feature = "authentication"))]
1655        {
1656            let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1657                Shell::new(&TEST_TREE, handler, io);
1658
1659            // Should start in Inactive state
1660            assert_eq!(shell.state, CliState::Inactive);
1661
1662            // Activate should transition to LoggedIn (auth disabled)
1663            shell.activate().unwrap();
1664            assert_eq!(shell.state, CliState::LoggedIn);
1665
1666            // Deactivate should return to Inactive
1667            shell.deactivate();
1668            assert_eq!(shell.state, CliState::Inactive);
1669            assert!(shell.current_user.is_none());
1670            assert!(shell.input_buffer.is_empty());
1671            assert!(shell.current_path.is_empty());
1672        }
1673    }
1674
1675    #[test]
1676    #[cfg(not(feature = "authentication"))]
1677    fn test_write_formatted_response_default() {
1678        // Test default formatting (no flags set)
1679        let io = MockIo::new();
1680        let handler = MockHandler;
1681        let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1682            Shell::new(&TEST_TREE, handler, io);
1683
1684        let response = crate::response::Response::<DefaultConfig>::success("Test message");
1685        shell.write_formatted_response(&response).unwrap();
1686
1687        // Default: message + postfix newline
1688        assert_eq!(shell.io.get_output(), "Test message\r\n");
1689    }
1690
1691    #[test]
1692    #[cfg(not(feature = "authentication"))]
1693    fn test_write_formatted_response_with_prefix_newline() {
1694        let io = MockIo::new();
1695        let handler = MockHandler;
1696        let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1697            Shell::new(&TEST_TREE, handler, io);
1698
1699        let response =
1700            crate::response::Response::<DefaultConfig>::success("Test").with_prefix_newline();
1701        shell.write_formatted_response(&response).unwrap();
1702
1703        // prefix newline + message + postfix newline
1704        assert_eq!(shell.io.get_output(), "\r\nTest\r\n");
1705    }
1706
1707    #[test]
1708    #[cfg(not(feature = "authentication"))]
1709    fn test_write_formatted_response_indented() {
1710        let io = MockIo::new();
1711        let handler = MockHandler;
1712        let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1713            Shell::new(&TEST_TREE, handler, io);
1714
1715        let response =
1716            crate::response::Response::<DefaultConfig>::success("Line 1\r\nLine 2").indented();
1717        shell.write_formatted_response(&response).unwrap();
1718
1719        // Each line indented with 2 spaces + postfix newline
1720        assert_eq!(shell.io.get_output(), "  Line 1\r\n  Line 2\r\n");
1721    }
1722
1723    #[test]
1724    #[cfg(not(feature = "authentication"))]
1725    fn test_write_formatted_response_indented_single_line() {
1726        let io = MockIo::new();
1727        let handler = MockHandler;
1728        let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1729            Shell::new(&TEST_TREE, handler, io);
1730
1731        let response =
1732            crate::response::Response::<DefaultConfig>::success("Single line").indented();
1733        shell.write_formatted_response(&response).unwrap();
1734
1735        // Single line indented
1736        assert_eq!(shell.io.get_output(), "  Single line\r\n");
1737    }
1738
1739    #[test]
1740    #[cfg(not(feature = "authentication"))]
1741    fn test_write_formatted_response_without_postfix_newline() {
1742        let io = MockIo::new();
1743        let handler = MockHandler;
1744        let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1745            Shell::new(&TEST_TREE, handler, io);
1746
1747        let response = crate::response::Response::<DefaultConfig>::success("No newline")
1748            .without_postfix_newline();
1749        shell.write_formatted_response(&response).unwrap();
1750
1751        // Message without trailing newline
1752        assert_eq!(shell.io.get_output(), "No newline");
1753    }
1754
1755    #[test]
1756    #[cfg(not(feature = "authentication"))]
1757    fn test_write_formatted_response_combined_flags() {
1758        let io = MockIo::new();
1759        let handler = MockHandler;
1760        let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1761            Shell::new(&TEST_TREE, handler, io);
1762
1763        let response = crate::response::Response::<DefaultConfig>::success("Multi\r\nLine")
1764            .with_prefix_newline()
1765            .indented();
1766        shell.write_formatted_response(&response).unwrap();
1767
1768        // Prefix newline + indented lines + postfix newline
1769        assert_eq!(shell.io.get_output(), "\r\n  Multi\r\n  Line\r\n");
1770    }
1771
1772    #[test]
1773    #[cfg(not(feature = "authentication"))]
1774    fn test_write_formatted_response_all_flags_off() {
1775        let io = MockIo::new();
1776        let handler = MockHandler;
1777        let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1778            Shell::new(&TEST_TREE, handler, io);
1779
1780        let response =
1781            crate::response::Response::<DefaultConfig>::success("Raw").without_postfix_newline();
1782        shell.write_formatted_response(&response).unwrap();
1783
1784        // No formatting at all
1785        assert_eq!(shell.io.get_output(), "Raw");
1786    }
1787
1788    #[test]
1789    #[cfg(not(feature = "authentication"))]
1790    fn test_write_formatted_response_empty_message() {
1791        let io = MockIo::new();
1792        let handler = MockHandler;
1793        let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1794            Shell::new(&TEST_TREE, handler, io);
1795
1796        let response = crate::response::Response::<DefaultConfig>::success("");
1797        shell.write_formatted_response(&response).unwrap();
1798
1799        // Empty message still gets postfix newline
1800        assert_eq!(shell.io.get_output(), "\r\n");
1801    }
1802
1803    #[test]
1804    #[cfg(not(feature = "authentication"))]
1805    fn test_write_formatted_response_indented_multiline() {
1806        let io = MockIo::new();
1807        let handler = MockHandler;
1808        let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1809            Shell::new(&TEST_TREE, handler, io);
1810
1811        let response = crate::response::Response::<DefaultConfig>::success("A\r\nB\r\nC\r\nD")
1812            .indented()
1813            .without_postfix_newline();
1814        shell.write_formatted_response(&response).unwrap();
1815
1816        // All 4 lines indented, no trailing newline
1817        assert_eq!(shell.io.get_output(), "  A\r\n  B\r\n  C\r\n  D");
1818    }
1819
1820    #[test]
1821    #[cfg(not(feature = "authentication"))]
1822    fn test_inline_message_flag() {
1823        // Test that inline_message flag is properly recognized
1824        let response =
1825            crate::response::Response::<DefaultConfig>::success("... processing").inline();
1826
1827        assert!(
1828            response.inline_message,
1829            "inline() should set inline_message flag"
1830        );
1831
1832        // Note: The actual inline behavior (no newline after input) is tested
1833        // via integration tests, as it requires simulating full command execution.
1834        // This test verifies the flag is set correctly.
1835    }
1836
1837    #[test]
1838    #[cfg(not(feature = "authentication"))]
1839    fn test_resolve_path_cannot_navigate_through_command() {
1840        // Test that resolve_path returns InvalidPath when trying to navigate through a command
1841        let io = MockIo::new();
1842        let handler = MockHandler;
1843        let shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1844            Shell::new(&TEST_TREE, handler, io);
1845
1846        // Valid: Command as last segment should succeed
1847        let result = shell.resolve_path("test-cmd");
1848        assert!(result.is_ok(), "Should resolve path to command");
1849        if let Ok((node, _)) = result {
1850            assert!(node.is_some());
1851            if let Some(Node::Command(cmd)) = node {
1852                assert_eq!(cmd.name, "test-cmd");
1853            } else {
1854                panic!("Expected Command node");
1855            }
1856        }
1857
1858        // Invalid: Cannot navigate through command to another segment
1859        let result = shell.resolve_path("test-cmd/invalid");
1860        assert!(
1861            result.is_err(),
1862            "Should fail when navigating through command"
1863        );
1864        assert_eq!(
1865            result.unwrap_err(),
1866            CliError::InvalidPath,
1867            "Should return InvalidPath when trying to navigate through command"
1868        );
1869
1870        // Invalid: Multiple segments after command
1871        let result = shell.resolve_path("test-cmd/extra/path");
1872        assert!(
1873            result.is_err(),
1874            "Should fail with multiple segments after command"
1875        );
1876        assert_eq!(
1877            result.unwrap_err(),
1878            CliError::InvalidPath,
1879            "Should return InvalidPath for multiple segments after command"
1880        );
1881    }
1882
1883    #[test]
1884    #[cfg(not(feature = "authentication"))]
1885    fn test_resolve_path_comprehensive() {
1886        let io = MockIo::new();
1887        let handler = MockHandler;
1888        let shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1889            Shell::new(&TEST_TREE, handler, io);
1890
1891        // Test 1: Root level command
1892        let result = shell.resolve_path("test-cmd");
1893        assert!(result.is_ok(), "Should resolve root-level command");
1894        if let Ok((node, _)) = result {
1895            assert!(node.is_some());
1896            if let Some(Node::Command(cmd)) = node {
1897                assert_eq!(cmd.name, "test-cmd");
1898            }
1899        }
1900
1901        // Test 2: Verify command metadata properties
1902        let result = shell.resolve_path("system/reboot");
1903        assert!(result.is_ok(), "Should resolve system/reboot");
1904        if let Ok((node, _)) = result {
1905            assert!(node.is_some());
1906            if let Some(Node::Command(cmd)) = node {
1907                assert_eq!(cmd.name, "reboot");
1908                assert_eq!(cmd.description, "Reboot the system");
1909                assert_eq!(cmd.access_level, MockLevel::User);
1910                assert_eq!(cmd.kind, CommandKind::Sync);
1911            }
1912        }
1913
1914        // Test 3: Verify unique command ID (critical for handler dispatch)
1915        let result = shell.resolve_path("system/status");
1916        assert!(result.is_ok(), "Should resolve system/status");
1917        if let Ok((node, _)) = result {
1918            assert!(node.is_some());
1919            if let Some(Node::Command(cmd)) = node {
1920                assert_eq!(cmd.name, "status");
1921                assert_eq!(cmd.id, "status");
1922                // Verify this is different from network/status which has id "network_status"
1923            }
1924        }
1925
1926        // Test 4: Second-level nested command (system/network/status)
1927        let result = shell.resolve_path("system/network/status");
1928        assert!(result.is_ok(), "Should resolve system/network/status");
1929        if let Ok((node, _)) = result {
1930            assert!(node.is_some());
1931            if let Some(Node::Command(cmd)) = node {
1932                assert_eq!(cmd.name, "status");
1933            }
1934        }
1935
1936        // Test 5: Second-level nested command (system/hardware/led)
1937        let result = shell.resolve_path("system/hardware/led");
1938        assert!(result.is_ok(), "Should resolve system/hardware/led");
1939        if let Ok((node, _)) = result {
1940            assert!(node.is_some());
1941            if let Some(Node::Command(cmd)) = node {
1942                assert_eq!(cmd.name, "led");
1943                assert_eq!(cmd.min_args, 1);
1944                assert_eq!(cmd.max_args, 1);
1945            }
1946        }
1947
1948        // Test 6: Non-existent command at root
1949        let result = shell.resolve_path("nonexistent");
1950        assert!(result.is_err(), "Should fail for non-existent command");
1951        assert_eq!(
1952            result.unwrap_err(),
1953            CliError::CommandNotFound,
1954            "Should return CommandNotFound for non-existent command"
1955        );
1956
1957        // Test 7: Invalid path (nonexistent directory)
1958        let result = shell.resolve_path("invalid/path/command");
1959        assert!(result.is_err(), "Should fail for nonexistent path");
1960        assert_eq!(
1961            result.unwrap_err(),
1962            CliError::CommandNotFound,
1963            "Should return CommandNotFound when first segment doesn't exist"
1964        );
1965
1966        // Test 7b: Invalid path (attempting to navigate through a command)
1967        let result = shell.resolve_path("test-cmd/something");
1968        assert!(
1969            result.is_err(),
1970            "Should fail when navigating through command"
1971        );
1972        assert_eq!(
1973            result.unwrap_err(),
1974            CliError::InvalidPath,
1975            "Should return InvalidPath when trying to navigate through a command"
1976        );
1977
1978        // Test 8: Resolve to directory (system)
1979        let result = shell.resolve_path("system");
1980        assert!(result.is_ok(), "Should resolve directory path");
1981        if let Ok((node, _)) = result {
1982            assert!(node.is_some());
1983            if let Some(Node::Directory(dir)) = node {
1984                assert_eq!(dir.name, "system");
1985            }
1986        }
1987
1988        // Test 9: Resolve nested directory (system/network)
1989        let result = shell.resolve_path("system/network");
1990        assert!(result.is_ok(), "Should resolve nested directory");
1991        if let Ok((node, _)) = result {
1992            assert!(node.is_some());
1993            if let Some(Node::Directory(dir)) = node {
1994                assert_eq!(dir.name, "network");
1995            }
1996        }
1997    }
1998
1999    #[test]
2000    #[cfg(not(feature = "authentication"))]
2001    fn test_resolve_path_parent_directory() {
2002        let io = MockIo::new();
2003        let handler = MockHandler;
2004        let shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
2005            Shell::new(&TEST_TREE, handler, io);
2006
2007        // Test 1: Navigate into directory then back up with ..
2008        // First navigate to system/network/status
2009        let result = shell.resolve_path("system/network/status");
2010        assert!(result.is_ok(), "Should resolve system/network/status");
2011        let (_, path) = result.unwrap();
2012        // Path should have indices for system (1), network (3)
2013        // [1] = system (index 1 in children of root: test-cmd, system)
2014        // [3] = network (index 3 in children of system: reboot, status, hardware, network)
2015        assert_eq!(
2016            path.len(),
2017            2,
2018            "Path should have 2 elements (system, network)"
2019        );
2020        assert_eq!(path[0], 1, "system should be at index 1 in root");
2021        assert_eq!(path[1], 3, "network should be at index 3 in system");
2022
2023        // Test 2: Use .. to go back to system from system/network
2024        let result = shell.resolve_path("system/network/..");
2025        assert!(result.is_ok(), "Should resolve system/network/..");
2026        if let Ok((node, path)) = result {
2027            assert!(node.is_some());
2028            if let Some(Node::Directory(dir)) = node {
2029                assert_eq!(dir.name, "system", "Should be back at system directory");
2030            }
2031            assert_eq!(path.len(), 1, "Path should have 1 element");
2032            assert_eq!(path[0], 1, "system should be at index 1 in root");
2033        }
2034
2035        // Test 3: Multiple .. to go back to root
2036        let result = shell.resolve_path("system/network/../..");
2037        assert!(result.is_ok(), "Should resolve system/network/../..");
2038        if let Ok((node, path)) = result {
2039            assert_eq!(path.len(), 0, "Path should be empty (at root)");
2040            assert!(node.is_none(), "Node should be None (representing root)");
2041        }
2042
2043        // Test 4: Go beyond root with .. (should stay at root)
2044        let result = shell.resolve_path("..");
2045        assert!(result.is_ok(), "Should handle .. at root");
2046        if let Ok((node, path)) = result {
2047            assert_eq!(path.len(), 0, "Path should stay at root");
2048            assert!(node.is_none(), "Node should be None (representing root)");
2049        }
2050    }
2051}