vkteams_bot_cli/commands/
config.rs

1//! Configuration commands module
2//!
3//! This module contains all commands related to configuration management.
4
5use crate::commands::{Command, CommandExecutor, CommandResult, OutputFormat};
6use crate::config::Config;
7use crate::constants::{help, ui::emoji};
8use crate::errors::prelude::{CliError, Result as CliResult};
9use crate::output::{CliResponse, OutputFormatter};
10use async_trait::async_trait;
11use clap::Subcommand;
12use colored::Colorize;
13use serde_json::json;
14use std::io::{self, Write};
15use vkteams_bot::prelude::*;
16
17/// All configuration-related commands
18#[derive(Subcommand, Debug, Clone)]
19pub enum ConfigCommands {
20    /// Interactive setup wizard for first-time configuration
21    Setup,
22    /// Show examples of how to use the CLI
23    Examples,
24    /// Show detailed information about all available commands
25    ListCommands,
26    /// Validate current configuration and test bot connection
27    Validate,
28    /// Configure the CLI tool
29    Config {
30        /// Show current configuration
31        #[arg(short, long)]
32        show: bool,
33        /// Initialize a new configuration file
34        #[arg(short, long)]
35        init: bool,
36        /// Interactive configuration wizard
37        #[arg(short = 'w', long)]
38        wizard: bool,
39    },
40    /// Generate shell completion scripts
41    Completion {
42        /// Shell to generate completion for
43        #[arg(value_enum)]
44        shell: crate::completion::CompletionShell,
45        /// Output file path (prints to stdout if not specified)
46        #[arg(short, long)]
47        output: Option<String>,
48        /// Install completion to system location
49        #[arg(short, long)]
50        install: bool,
51        /// Generate completions for all shells
52        #[arg(short, long)]
53        all: bool,
54    },
55}
56
57#[async_trait]
58impl Command for ConfigCommands {
59    async fn execute(&self, bot: &Bot) -> CliResult<()> {
60        match self {
61            ConfigCommands::Setup => execute_setup().await,
62            ConfigCommands::Examples => execute_examples().await,
63            ConfigCommands::ListCommands => execute_list_commands().await,
64            ConfigCommands::Validate => execute_validate(bot).await,
65            ConfigCommands::Config { show, init, wizard } => {
66                execute_config(*show, *init, *wizard).await
67            }
68            ConfigCommands::Completion {
69                shell,
70                output,
71                install,
72                all,
73            } => execute_completion(*shell, output.as_deref(), *install, *all).await,
74        }
75    }
76
77    fn name(&self) -> &'static str {
78        match self {
79            ConfigCommands::Setup => "setup",
80            ConfigCommands::Examples => "examples",
81            ConfigCommands::ListCommands => "list-commands",
82            ConfigCommands::Validate => "validate",
83            ConfigCommands::Config { .. } => "config",
84            ConfigCommands::Completion { .. } => "completion",
85        }
86    }
87
88    fn validate(&self) -> CliResult<()> {
89        // Configuration commands don't need pre-validation
90        Ok(())
91    }
92
93    /// New method for structured output support
94    async fn execute_with_output(&self, bot: &Bot, output_format: &OutputFormat) -> CliResult<()> {
95        // For interactive commands like Setup and Config wizard, use regular execute when in Pretty mode
96        let is_interactive = matches!(self, ConfigCommands::Setup)
97            || matches!(self, ConfigCommands::Config { wizard: true, .. });
98
99        if is_interactive && matches!(output_format, OutputFormat::Pretty) {
100            return self.execute(bot).await;
101        }
102
103        let response = match self {
104            ConfigCommands::Setup => execute_setup_structured().await,
105            ConfigCommands::Examples => execute_examples_structured().await,
106            ConfigCommands::ListCommands => execute_list_commands_structured().await,
107            ConfigCommands::Validate => execute_validate_structured(bot).await,
108            ConfigCommands::Config { show, init, wizard } => {
109                execute_config_structured(*show, *init, *wizard).await
110            }
111            ConfigCommands::Completion {
112                shell,
113                output,
114                install,
115                all,
116            } => execute_completion_structured(*shell, output.as_deref(), *install, *all).await,
117        };
118
119        OutputFormatter::print(&response, output_format)?;
120
121        if !response.success {
122            return Err(CliError::UnexpectedError("Command failed".to_string()));
123        }
124
125        Ok(())
126    }
127}
128
129#[async_trait]
130impl CommandExecutor for ConfigCommands {
131    async fn execute_with_result(&self, bot: &Bot) -> CommandResult {
132        match self {
133            ConfigCommands::Setup => execute_setup_with_result().await,
134            ConfigCommands::Examples => execute_examples_with_result().await,
135            ConfigCommands::ListCommands => execute_list_commands_with_result().await,
136            ConfigCommands::Validate => execute_validate_with_result(bot).await,
137            ConfigCommands::Config { show, init, wizard } => {
138                execute_config_with_result(*show, *init, *wizard).await
139            }
140            ConfigCommands::Completion {
141                shell,
142                output,
143                install,
144                all,
145            } => execute_completion_with_result(*shell, output.as_deref(), *install, *all).await,
146        }
147    }
148
149    fn name(&self) -> &'static str {
150        match self {
151            ConfigCommands::Setup => "setup",
152            ConfigCommands::Examples => "examples",
153            ConfigCommands::ListCommands => "list-commands",
154            ConfigCommands::Validate => "validate",
155            ConfigCommands::Config { .. } => "config",
156            ConfigCommands::Completion { .. } => "completion",
157        }
158    }
159
160    fn validate(&self) -> CliResult<()> {
161        // Configuration commands don't need pre-validation
162        Ok(())
163    }
164}
165
166// Command execution functions
167
168async fn execute_setup() -> CliResult<()> {
169    println!(
170        "{} VK Teams Bot CLI Setup Wizard",
171        emoji::ROBOT.bold().blue()
172    );
173    println!("This wizard will help you configure the CLI tool.\n");
174
175    let mut new_config = toml::from_str::<Config>("").unwrap();
176
177    // Get API token
178    print!("Enter your VK Teams Bot API token: ");
179    io::stdout().flush().unwrap();
180    let mut token = String::new();
181    io::stdin().read_line(&mut token).unwrap();
182    new_config.api.token = Some(token.trim().to_string());
183
184    // Get API URL
185    print!("Enter your VK Teams Bot API URL: ");
186    io::stdout().flush().unwrap();
187    let mut url = String::new();
188    io::stdin().read_line(&mut url).unwrap();
189    new_config.api.url = Some(url.trim().to_string());
190
191    // Ask about proxy
192    print!("Do you need to configure a proxy? (y/N): ");
193    io::stdout().flush().unwrap();
194    let mut proxy_choice = String::new();
195    io::stdin().read_line(&mut proxy_choice).unwrap();
196    if proxy_choice.trim().to_lowercase() == "y" {
197        print!("Enter proxy URL: ");
198        io::stdout().flush().unwrap();
199        let mut proxy_url = String::new();
200        io::stdin().read_line(&mut proxy_url).unwrap();
201        new_config.proxy = Some(crate::config::ProxyConfig {
202            url: proxy_url.trim().to_string(),
203            user: None,
204            password: None,
205        });
206    }
207
208    // Test and save configuration
209    println!("\n{} Testing configuration...", emoji::TEST_TUBE);
210
211    if let Err(e) = new_config.save(None) {
212        eprintln!(
213            "{}  Warning: Could not save configuration: {}",
214            emoji::WARNING,
215            e
216        );
217    } else {
218        println!("{} Configuration saved successfully!", emoji::FLOPPY_DISK);
219    }
220
221    println!(
222        "\n{} Setup complete! You can now use the CLI tool.",
223        emoji::PARTY
224    );
225    println!(
226        "Try: {} to test your setup",
227        "vkteams-bot-cli get-self".green()
228    );
229
230    Ok(())
231}
232
233async fn execute_completion(
234    shell: crate::completion::CompletionShell,
235    output: Option<&str>,
236    install: bool,
237    all: bool,
238) -> CliResult<()> {
239    use crate::completion::{
240        generate_all_completions, generate_completion, get_default_completion_dir,
241        install_completion,
242    };
243    use std::path::Path;
244
245    if all {
246        let output_dir = if let Some(dir) = output {
247            Path::new(dir).to_path_buf()
248        } else if let Some(default_dir) = get_default_completion_dir() {
249            default_dir
250        } else {
251            std::env::current_dir()
252                .map_err(|e| CliError::FileError(format!("Failed to get current directory: {e}")))?
253        };
254
255        generate_all_completions(&output_dir)?;
256        return Ok(());
257    }
258
259    if install {
260        install_completion(shell)?;
261        return Ok(());
262    }
263
264    let output_path = output.map(Path::new);
265    generate_completion(shell, output_path)?;
266
267    Ok(())
268}
269
270// New CommandResult-based execution functions
271
272async fn execute_setup_with_result() -> CommandResult {
273    match execute_setup().await {
274        Ok(()) => CommandResult::success_with_message("Setup completed successfully"),
275        Err(e) => CommandResult::error(format!("Setup failed: {e}")),
276    }
277}
278
279async fn execute_examples_with_result() -> CommandResult {
280    match execute_examples().await {
281        Ok(()) => CommandResult::success(),
282        Err(e) => CommandResult::error(format!("Failed to show examples: {e}")),
283    }
284}
285
286async fn execute_list_commands_with_result() -> CommandResult {
287    match execute_list_commands().await {
288        Ok(()) => CommandResult::success(),
289        Err(e) => CommandResult::error(format!("Failed to list commands: {e}")),
290    }
291}
292
293async fn execute_validate_with_result(bot: &Bot) -> CommandResult {
294    match execute_validate(bot).await {
295        Ok(()) => CommandResult::success_with_message("Validation completed successfully"),
296        Err(e) => CommandResult::error(format!("Validation failed: {e}")),
297    }
298}
299
300async fn execute_config_with_result(show: bool, init: bool, wizard: bool) -> CommandResult {
301    match execute_config(show, init, wizard).await {
302        Ok(()) => CommandResult::success_with_message("Configuration operation completed"),
303        Err(e) => CommandResult::error(format!("Configuration operation failed: {e}")),
304    }
305}
306
307async fn execute_completion_with_result(
308    shell: crate::completion::CompletionShell,
309    output: Option<&str>,
310    install: bool,
311    all: bool,
312) -> CommandResult {
313    match execute_completion(shell, output, install, all).await {
314        Ok(()) => CommandResult::success_with_message("Completion operation completed"),
315        Err(e) => CommandResult::error(format!("Completion operation failed: {e}")),
316    }
317}
318
319async fn execute_examples() -> CliResult<()> {
320    println!("{} VK Teams Bot CLI Examples", emoji::BOOKS.bold().blue());
321    println!();
322
323    println!("{}", "Basic Message Operations:".bold().green());
324    println!(
325        "  {}",
326        "vkteams-bot-cli send-text -u USER_ID -m \"Hello World!\"".cyan()
327    );
328    println!(
329        "  {}",
330        "vkteams-bot-cli send-file -u USER_ID -p /path/to/file.pdf".cyan()
331    );
332    println!(
333        "  {}",
334        "vkteams-bot-cli send-voice -u USER_ID -p /path/to/voice.ogg".cyan()
335    );
336    println!();
337
338    println!("{}", "Chat Management:".bold().green());
339    println!("  {}", "vkteams-bot-cli get-chat-info -c CHAT_ID".cyan());
340    println!("  {}", "vkteams-bot-cli get-chat-members -c CHAT_ID".cyan());
341    println!(
342        "  {}",
343        "vkteams-bot-cli set-chat-title -c CHAT_ID -t \"New Title\"".cyan()
344    );
345    println!(
346        "  {}",
347        "vkteams-bot-cli set-chat-about -c CHAT_ID -a \"Chat description\"".cyan()
348    );
349    println!();
350
351    println!("{}", "Message Operations:".bold().green());
352    println!(
353        "  {}",
354        "vkteams-bot-cli edit-message -c CHAT_ID -m MSG_ID -t \"Updated text\"".cyan()
355    );
356    println!(
357        "  {}",
358        "vkteams-bot-cli delete-message -c CHAT_ID -m MSG_ID".cyan()
359    );
360    println!(
361        "  {}",
362        "vkteams-bot-cli pin-message -c CHAT_ID -m MSG_ID".cyan()
363    );
364    println!(
365        "  {}",
366        "vkteams-bot-cli unpin-message -c CHAT_ID -m MSG_ID".cyan()
367    );
368    println!();
369
370    println!("{}", "File Operations:".bold().green());
371    println!(
372        "  {}",
373        "vkteams-bot-cli get-file -f FILE_ID -p /download/path/".cyan()
374    );
375    println!();
376
377    println!("{}", "Bot Information:".bold().green());
378    println!("  {}", "vkteams-bot-cli get-self".cyan());
379    println!("  {}", "vkteams-bot-cli get-self --detailed".cyan());
380    println!("  {}", "vkteams-bot-cli get-profile -u USER_ID".cyan());
381    println!();
382
383    println!("{}", "Event Monitoring:".bold().green());
384    println!("  {}", "vkteams-bot-cli get-events".cyan());
385    println!(
386        "  {}",
387        "vkteams-bot-cli get-events -l true | jq '.events[]'".cyan()
388    );
389    println!();
390
391    println!("{}", "Configuration:".bold().green());
392    println!("  {}", "vkteams-bot-cli setup".cyan());
393    println!("  {}", "vkteams-bot-cli config --show".cyan());
394    println!("  {}", "vkteams-bot-cli config --wizard".cyan());
395    println!("  {}", "vkteams-bot-cli validate".cyan());
396    println!();
397
398    println!("{}", "Shell Completion:".bold().green());
399    println!("  {}", "vkteams-bot-cli completion bash".cyan());
400    println!(
401        "  {}",
402        "vkteams-bot-cli completion zsh --output _vkteams-bot-cli".cyan()
403    );
404    println!("  {}", "vkteams-bot-cli completion fish --install".cyan());
405    println!(
406        "  {}",
407        "vkteams-bot-cli completion bash --all --output ./completions".cyan()
408    );
409    println!();
410
411    println!("{}", "Scheduled Messages:".bold().green());
412    println!(
413        "  {}",
414        "vkteams-bot-cli schedule text -u CHAT_ID -m \"Hello\" -t \"2024-01-01 10:00\"".cyan()
415    );
416    println!(
417        "  {}",
418        "vkteams-bot-cli schedule text -u CHAT_ID -m \"Daily reminder\" -c \"0 9 * * *\"".cyan()
419    );
420    println!(
421        "  {}",
422        "vkteams-bot-cli schedule text -u CHAT_ID -m \"Every 5 min\" -i 300".cyan()
423    );
424    println!(
425        "  {}",
426        "vkteams-bot-cli schedule file -u CHAT_ID -p \"/path/to/report.pdf\" -t \"30m\"".cyan()
427    );
428    println!();
429
430    println!("{}", "Scheduler Management:".bold().green());
431    println!("  {}", "vkteams-bot-cli scheduler start".cyan());
432    println!("  {}", "vkteams-bot-cli scheduler status".cyan());
433    println!("  {}", "vkteams-bot-cli scheduler list".cyan());
434    println!("  {}", "vkteams-bot-cli task show TASK_ID".cyan());
435    println!("  {}", "vkteams-bot-cli task run TASK_ID".cyan());
436    println!();
437
438    println!("{}", "Chat Actions:".bold().green());
439    println!(
440        "  {}",
441        "vkteams-bot-cli send-action -c CHAT_ID -a typing".cyan()
442    );
443    println!(
444        "  {}",
445        "vkteams-bot-cli send-action -c CHAT_ID -a looking".cyan()
446    );
447    println!();
448
449    Ok(())
450}
451
452async fn execute_list_commands() -> CliResult<()> {
453    println!(
454        "{} VK Teams Bot CLI Commands Reference",
455        emoji::ROBOT.bold().blue()
456    );
457    println!();
458
459    let commands = vec![
460        (
461            "send-text",
462            "Send a text message to a user or chat",
463            "Basic messaging",
464        ),
465        (
466            "send-file",
467            "Upload and send a file to a user or chat",
468            "File sharing",
469        ),
470        (
471            "send-voice",
472            "Send a voice message from an audio file",
473            "Voice messaging",
474        ),
475        (
476            "get-file",
477            "Download a file by its ID to local storage",
478            "File management",
479        ),
480        (
481            "get-events",
482            "Retrieve bot events or start long polling",
483            "Event monitoring",
484        ),
485        (
486            "get-chat-info",
487            "Get detailed information about a chat",
488            "Chat information",
489        ),
490        (
491            "get-profile",
492            "Get user profile information",
493            "User information",
494        ),
495        (
496            "edit-message",
497            "Edit an existing message in a chat",
498            "Message management",
499        ),
500        (
501            "delete-message",
502            "Delete a message from a chat",
503            "Message management",
504        ),
505        (
506            "pin-message",
507            "Pin a message in a chat",
508            "Message management",
509        ),
510        (
511            "unpin-message",
512            "Unpin a message from a chat",
513            "Message management",
514        ),
515        (
516            "get-chat-members",
517            "List all members of a chat",
518            "Chat management",
519        ),
520        (
521            "set-chat-title",
522            "Change the title of a chat",
523            "Chat management",
524        ),
525        (
526            "set-chat-about",
527            "Set the description of a chat",
528            "Chat management",
529        ),
530        (
531            "send-action",
532            "Send typing or looking action to a chat",
533            "Chat interaction",
534        ),
535        (
536            "get-self",
537            "Get bot information and verify connectivity",
538            "Bot management",
539        ),
540        (
541            "schedule",
542            "Schedule messages to be sent at specific times",
543            "Scheduling",
544        ),
545        (
546            "scheduler",
547            "Manage the scheduler daemon service",
548            "Scheduling",
549        ),
550        ("task", "Manage individual scheduled tasks", "Scheduling"),
551        (
552            "setup",
553            "Interactive wizard for first-time configuration",
554            "Configuration",
555        ),
556        ("examples", "Show usage examples for all commands", "Help"),
557        (
558            "list-commands",
559            "Show this detailed command reference",
560            "Help",
561        ),
562        (
563            "validate",
564            "Test configuration and bot connectivity",
565            "Diagnostics",
566        ),
567        (
568            "config",
569            "Manage configuration files and settings",
570            "Configuration",
571        ),
572        (
573            "completion",
574            "Generate shell completion scripts",
575            "Configuration",
576        ),
577    ];
578
579    let mut categories: std::collections::HashMap<&str, Vec<(&str, &str)>> =
580        std::collections::HashMap::new();
581
582    for (cmd, desc, cat) in commands {
583        categories.entry(cat).or_default().push((cmd, desc));
584    }
585
586    for (category, cmds) in categories {
587        println!("{}", format!("{category}:").bold().green());
588        for (cmd, desc) in cmds {
589            println!("  {:<20} {}", cmd.cyan(), desc);
590        }
591        println!();
592    }
593
594    println!("{}", "💡 Tips:".bold().yellow());
595    println!(
596        "  • Use {} for command-specific help",
597        "vkteams-bot-cli <command> --help".cyan()
598    );
599    println!(
600        "  • Use {} to see usage examples",
601        "vkteams-bot-cli examples".cyan()
602    );
603    println!(
604        "  • Use {} to test your configuration",
605        "vkteams-bot-cli validate".cyan()
606    );
607    println!(
608        "  • Use {} for interactive setup",
609        "vkteams-bot-cli setup".cyan()
610    );
611
612    Ok(())
613}
614
615async fn execute_validate(bot: &Bot) -> CliResult<()> {
616    println!(
617        "{} Validating Configuration...",
618        emoji::MAGNIFYING_GLASS.bold().blue()
619    );
620    println!();
621
622    // Check if configuration exists
623    match Config::from_file() {
624        Ok(config) => {
625            println!("{} Configuration file found and readable", emoji::CHECK);
626
627            // Check required fields
628            if config.api.token.is_some() {
629                println!("{} API token is configured", emoji::CHECK);
630            } else {
631                println!("{} API token is missing", emoji::CROSS);
632            }
633
634            if config.api.url.is_some() {
635                println!("{} API URL is configured", emoji::CHECK);
636            } else {
637                println!("{} API URL is missing", emoji::CROSS);
638            }
639
640            // Test bot connection
641            println!("\n{} Testing bot connection...", emoji::GEAR);
642
643            let request = RequestSelfGet::new(());
644            match bot.send_api_request(request).await {
645                Ok(bot_info) => {
646                    println!("{} API connection successful", emoji::CHECK);
647                    println!("{} Bot is working correctly", emoji::CHECK);
648
649                    if let Ok(json_str) = serde_json::to_string_pretty(&bot_info) {
650                        println!("\n{}", "Bot Information:".bold().green());
651                        println!("{}", json_str.green());
652                    }
653                }
654                Err(e) => {
655                    println!("{} API connection failed: {}", emoji::CROSS, e);
656                    return Err(CliError::ApiError(e));
657                }
658            }
659        }
660        Err(_) => {
661            println!("{} No configuration file found", emoji::CROSS);
662            println!(
663                "{} Run {} to create initial configuration",
664                emoji::LIGHTBULB,
665                "vkteams-bot-cli setup".cyan()
666            );
667        }
668    }
669
670    println!("\n{} Validation complete!", emoji::SPARKLES.bold().green());
671    Ok(())
672}
673
674async fn execute_config(show: bool, init: bool, wizard: bool) -> CliResult<()> {
675    if wizard {
676        println!("{} Configuration Wizard", emoji::GEAR.bold().blue());
677        println!("Current configuration will be updated.\n");
678
679        let mut new_config = toml::from_str::<Config>("").unwrap();
680
681        // Update API token
682        if let Ok(current_config) = Config::from_file()
683            && let Some(current_token) = &current_config.api.token
684        {
685            println!(
686                "Current API token: {}***",
687                &current_token[..8.min(current_token.len())]
688            );
689        }
690        print!("Enter new API token (or press Enter to keep current): ");
691        io::stdout().flush().unwrap();
692        let mut token = String::new();
693        io::stdin().read_line(&mut token).unwrap();
694        if !token.trim().is_empty() {
695            new_config.api.token = Some(token.trim().to_string());
696        }
697
698        // Update API URL
699        if let Ok(current_config) = Config::from_file()
700            && let Some(current_url) = &current_config.api.url
701        {
702            println!("Current API URL: {current_url}");
703        }
704        print!("Enter new API URL (or press Enter to keep current): ");
705        io::stdout().flush().unwrap();
706        let mut url = String::new();
707        io::stdin().read_line(&mut url).unwrap();
708        if !url.trim().is_empty() {
709            new_config.api.url = Some(url.trim().to_string());
710        }
711
712        // Save and test
713        if let Err(e) = new_config.save(None) {
714            eprintln!(
715                "{}  Warning: Could not save configuration: {}",
716                emoji::WARNING,
717                e
718            );
719        } else {
720            println!("{} Configuration updated successfully!", emoji::FLOPPY_DISK);
721        }
722    }
723
724    if show {
725        // Print current configuration as TOML
726        match Config::from_file() {
727            Ok(config) => match toml::to_string_pretty(&config) {
728                Ok(config_str) => {
729                    println!("Current configuration:\n{}", config_str.green());
730                }
731                Err(e) => {
732                    return Err(CliError::UnexpectedError(format!(
733                        "Failed to serialize configuration: {e}"
734                    )));
735                }
736            },
737            Err(_) => {
738                println!("{} No configuration file found", emoji::INFO);
739                println!("{} {}", emoji::LIGHTBULB, help::SETUP_HINT.cyan());
740            }
741        }
742    }
743
744    if init {
745        // Create a default configuration file in the home directory
746        let config = toml::from_str::<Config>("").unwrap();
747        config.save(None)?;
748        println!("Configuration file initialized.");
749    }
750
751    // If no flags provided, show help
752    if !show && !init && !wizard {
753        println!(
754            "Use --show to display current configuration, --init to create a new configuration file, or --wizard for interactive configuration."
755        );
756    }
757
758    Ok(())
759}
760
761// Structured output versions
762
763async fn execute_setup_structured() -> CliResponse<serde_json::Value> {
764    // Setup is interactive, return a message for structured output
765    CliResponse::success(
766        "setup",
767        json!({
768            "status": "interactive",
769            "message": "Setup wizard requires interactive terminal. Use regular execute mode.",
770            "help": "Run without --output-format flag for interactive setup"
771        }),
772    )
773}
774
775async fn execute_examples_structured() -> CliResponse<serde_json::Value> {
776    let examples = vec![
777        json!({
778            "category": "Messaging",
779            "examples": [
780                {
781                    "command": "vkteams-bot-cli send-text -u @user -m \"Hello!\"",
782                    "description": "Send text message to user"
783                },
784                {
785                    "command": "vkteams-bot-cli send-file -u chatId -p /path/to/file.pdf",
786                    "description": "Send file to chat"
787                }
788            ]
789        }),
790        json!({
791            "category": "Chat Management",
792            "examples": [
793                {
794                    "command": "vkteams-bot-cli get-chat-info -c chatId",
795                    "description": "Get chat information"
796                },
797                {
798                    "command": "vkteams-bot-cli set-chat-title -c chatId -t \"New Title\"",
799                    "description": "Set chat title"
800                }
801            ]
802        }),
803        json!({
804            "category": "Storage",
805            "examples": [
806                {
807                    "command": "vkteams-bot-cli storage database init",
808                    "description": "Initialize database with migrations"
809                },
810                {
811                    "command": "vkteams-bot-cli storage search semantic -q \"search query\"",
812                    "description": "Search using semantic similarity"
813                }
814            ]
815        }),
816    ];
817
818    CliResponse::success(
819        "examples",
820        json!({
821            "examples": examples,
822            "note": "Use --help with any command for detailed options"
823        }),
824    )
825}
826
827async fn execute_list_commands_structured() -> CliResponse<serde_json::Value> {
828    let commands = vec![
829        json!({
830            "category": "Messaging",
831            "commands": ["send-text", "send-file", "send-voice", "edit-message", "delete-message", "pin-message", "unpin-message"]
832        }),
833        json!({
834            "category": "Chat",
835            "commands": ["get-chat-info", "get-profile", "get-chat-members", "set-chat-title", "set-chat-about", "send-action"]
836        }),
837        json!({
838            "category": "Files",
839            "commands": ["upload", "download", "get-info"]
840        }),
841        json!({
842            "category": "Storage",
843            "commands": ["database", "search", "context"]
844        }),
845        json!({
846            "category": "Diagnostic",
847            "commands": ["get-self", "get-events", "get-file", "health-check", "network-test", "system-info", "rate-limit-test"]
848        }),
849        json!({
850            "category": "Config",
851            "commands": ["setup", "examples", "list-commands", "validate", "config", "completion"]
852        }),
853        json!({
854            "category": "Daemon",
855            "commands": ["start", "stop", "status", "restart", "logs"]
856        }),
857    ];
858
859    CliResponse::success(
860        "list-commands",
861        json!({
862            "command_categories": commands,
863            "total_categories": commands.len()
864        }),
865    )
866}
867
868async fn execute_validate_structured(bot: &Bot) -> CliResponse<serde_json::Value> {
869    let mut validation_results = Vec::new();
870
871    // Test 1: Configuration file
872    let config_result = match Config::from_file() {
873        Ok(config) => {
874            let mut issues = Vec::new();
875            if config.api.token.is_none() {
876                issues.push("Missing API token");
877            }
878            if config.api.url.is_none() {
879                issues.push("Missing API URL");
880            }
881
882            json!({
883                "test": "Configuration File",
884                "status": if issues.is_empty() { "pass" } else { "fail" },
885                "issues": issues
886            })
887        }
888        Err(e) => json!({
889            "test": "Configuration File",
890            "status": "fail",
891            "error": e.to_string()
892        }),
893    };
894    validation_results.push(config_result);
895
896    // Test 2: Bot connection
897    let connection_result = match bot.send_api_request(RequestSelfGet::new(())).await {
898        Ok(result) => json!({
899            "test": "Bot Connection",
900            "status": "pass",
901            "bot_info": {
902                "user_id": result.user_id,
903                "nickname": result.nick,
904                "first_name": result.first_name
905            }
906        }),
907        Err(e) => json!({
908            "test": "Bot Connection",
909            "status": "fail",
910            "error": e.to_string()
911        }),
912    };
913    validation_results.push(connection_result);
914
915    // Test 3: Environment variables
916    let env_vars = [
917        "VKTEAMS_BOT_API_TOKEN",
918        "VKTEAMS_BOT_API_URL",
919        "VKTEAMS_BOT_DOWNLOAD_DIR",
920    ];
921    let mut env_status = Vec::new();
922    for var in &env_vars {
923        env_status.push(json!({
924            "variable": var,
925            "set": std::env::var(var).is_ok()
926        }));
927    }
928    validation_results.push(json!({
929        "test": "Environment Variables",
930        "status": "info",
931        "variables": env_status
932    }));
933
934    let all_passed = validation_results
935        .iter()
936        .all(|r| r.get("status").and_then(|s| s.as_str()) != Some("fail"));
937
938    CliResponse::success(
939        "validate",
940        json!({
941            "validation_status": if all_passed { "valid" } else { "invalid" },
942            "test_results": validation_results
943        }),
944    )
945}
946
947async fn execute_config_structured(
948    show: bool,
949    init: bool,
950    wizard: bool,
951) -> CliResponse<serde_json::Value> {
952    if wizard {
953        return CliResponse::success(
954            "config",
955            json!({
956                "mode": "wizard",
957                "status": "interactive",
958                "message": "Configuration wizard requires interactive terminal",
959                "help": "Run without --output-format flag for interactive wizard"
960            }),
961        );
962    }
963
964    if init {
965        match Config::default().save(None) {
966            Ok(_) => {
967                let config_path = crate::utils::config_helpers::get_config_paths()
968                    .first()
969                    .map(|p| p.display().to_string())
970                    .unwrap_or_else(|| "unknown".to_string());
971
972                CliResponse::success(
973                    "config",
974                    json!({
975                        "action": "init",
976                        "status": "created",
977                        "path": config_path
978                    }),
979                )
980            }
981            Err(e) => CliResponse::error("config", format!("Failed to initialize config: {e}")),
982        }
983    } else if show {
984        match Config::from_file() {
985            Ok(config) => {
986                let config_json = serde_json::to_value(&config).unwrap_or(json!({}));
987                CliResponse::success(
988                    "config",
989                    json!({
990                        "action": "show",
991                        "configuration": config_json
992                    }),
993                )
994            }
995            Err(e) => CliResponse::error("config", format!("Failed to load config: {e}")),
996        }
997    } else {
998        CliResponse::success(
999            "config",
1000            json!({
1001                "message": "No action specified",
1002                "available_flags": ["--show", "--init", "--wizard"]
1003            }),
1004        )
1005    }
1006}
1007
1008async fn execute_completion_structured(
1009    shell: crate::completion::CompletionShell,
1010    output: Option<&str>,
1011    install: bool,
1012    all: bool,
1013) -> CliResponse<serde_json::Value> {
1014    if all {
1015        return CliResponse::success(
1016            "completion",
1017            json!({
1018                "mode": "all",
1019                "message": "Generating completions for all shells",
1020                "shells": ["bash", "zsh", "fish", "powershell"],
1021                "note": "Use regular execute mode to generate files"
1022            }),
1023        );
1024    }
1025
1026    let shell_name = match shell {
1027        crate::completion::CompletionShell::Bash => "bash",
1028        crate::completion::CompletionShell::Zsh => "zsh",
1029        crate::completion::CompletionShell::Fish => "fish",
1030        crate::completion::CompletionShell::PowerShell => "powershell",
1031    };
1032
1033    CliResponse::success(
1034        "completion",
1035        json!({
1036            "shell": shell_name,
1037            "output": output.unwrap_or("stdout"),
1038            "install": install,
1039            "note": "Completion script generation requires regular execute mode"
1040        }),
1041    )
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046    use super::*;
1047    use crate::utils::config_helpers::get_existing_config_path;
1048    use std::fs;
1049    #[cfg(unix)]
1050    use std::os::unix::fs::PermissionsExt;
1051    use tempfile::tempdir;
1052
1053    /// Helper to create a dummy bot for testing
1054    fn dummy_bot() -> Bot {
1055        Bot::with_params(&APIVersionUrl::V1, "dummy_token", "https://dummy.api.com").unwrap()
1056    }
1057
1058    #[tokio::test]
1059    async fn test_config_commands_variants() {
1060        // Check all enum variants are constructible
1061        let _ = ConfigCommands::Setup;
1062        let _ = ConfigCommands::Examples;
1063        let _ = ConfigCommands::ListCommands;
1064        let _ = ConfigCommands::Validate;
1065        let _ = ConfigCommands::Config {
1066            show: true,
1067            init: false,
1068            wizard: false,
1069        };
1070        let _ = ConfigCommands::Completion {
1071            shell: crate::completion::CompletionShell::Bash,
1072            output: None,
1073            install: false,
1074            all: false,
1075        };
1076    }
1077
1078    #[tokio::test]
1079    async fn test_execute_examples_success() {
1080        // Should not return error
1081        let res = execute_examples().await;
1082        assert!(res.is_ok());
1083    }
1084
1085    #[tokio::test]
1086    async fn test_execute_list_commands_success() {
1087        // Should not return error
1088        let res = execute_list_commands().await;
1089        assert!(res.is_ok());
1090    }
1091
1092    #[tokio::test]
1093    async fn test_execute_config_show_success() {
1094        // Should not return error when showing config
1095        let res = execute_config(true, false, false).await;
1096        assert!(res.is_ok());
1097    }
1098
1099    #[tokio::test]
1100    async fn test_execute_config_init_success() {
1101        // Should not return error when initializing config
1102        let res = execute_config(false, true, false).await;
1103        assert!(res.is_ok());
1104    }
1105
1106    #[tokio::test]
1107    #[ignore]
1108    async fn test_execute_config_wizard_success() {
1109        // Should not return error when running wizard
1110        let res = execute_config(false, false, true).await;
1111        assert!(res.is_ok());
1112    }
1113
1114    #[tokio::test]
1115    async fn test_execute_validate_success() {
1116        // Should not return error with dummy bot
1117        let bot = dummy_bot();
1118        let res = execute_validate(&bot).await;
1119        // Accept both Ok and Err (since dummy bot may fail connection)
1120        assert!(res.is_ok() || res.is_err());
1121    }
1122
1123    #[tokio::test]
1124    async fn test_execute_completion_success() {
1125        // Should not return error for valid shell
1126        let res =
1127            execute_completion(crate::completion::CompletionShell::Bash, None, false, false).await;
1128        assert!(res.is_ok());
1129    }
1130
1131    #[tokio::test]
1132    #[ignore]
1133    async fn test_execute_setup_mocked() {
1134        // This function is interactive, so we only check it does not panic
1135        // when called in a test environment (may fail due to lack of stdin)
1136        let res = execute_setup().await;
1137        // Accept both Ok and Err
1138        assert!(res.is_ok() || res.is_err());
1139    }
1140
1141    /// Test error when config file is missing or unreadable
1142    #[tokio::test]
1143    async fn test_execute_config_show_missing_file() {
1144        // Temporarily rename config file if exists
1145        let config_path = get_existing_config_path();
1146        let backup = if let Some(ref config_path) = config_path {
1147            let backup = config_path.with_extension("bak");
1148            fs::rename(config_path, &backup).ok();
1149            Some((config_path.clone(), backup))
1150        } else {
1151            None
1152        };
1153        // Should not panic, but print info about missing file
1154        let res = execute_config(true, false, false).await;
1155        assert!(res.is_ok());
1156        // Restore config
1157        if let Some((config_path, backup)) = backup {
1158            fs::rename(&backup, &config_path).ok();
1159        }
1160    }
1161
1162    /// Test error when config file is corrupted (invalid TOML)
1163    #[tokio::test]
1164    async fn test_execute_config_show_corrupted_file() {
1165        let dir = tempdir().unwrap();
1166        let path = dir.path().join("config.toml");
1167        fs::write(&path, "not a toml").unwrap();
1168        // Patch Config::default_path to use temp file (simulate via env var if supported)
1169        // Here, just check that serialization error is handled
1170        let res = toml::from_str::<Config>("not a toml");
1171        assert!(res.is_err());
1172    }
1173
1174    /// Test error when saving config fails (simulate unwritable dir)
1175    #[tokio::test]
1176    async fn test_execute_config_save_error() {
1177        let dir = tempdir().unwrap();
1178        let path = dir.path().join("readonly.toml");
1179        fs::write(&path, "").unwrap();
1180        let _ = fs::set_permissions(&path, fs::Permissions::from_mode(0o444));
1181        let config = Config::default();
1182        let res = config.save(Some(&path));
1183        assert!(res.is_err());
1184    }
1185
1186    /// Test error in completion generation (invalid shell)
1187    #[tokio::test]
1188    async fn test_execute_completion_invalid_shell() {
1189        // Use an invalid shell by casting from an invalid integer (simulate)
1190        // Here, just check that function returns error for invalid output path
1191        let res = execute_completion(
1192            crate::completion::CompletionShell::Bash,
1193            Some("/invalid/path/doesnotexist"),
1194            false,
1195            false,
1196        )
1197        .await;
1198        assert!(res.is_err());
1199    }
1200
1201    /// Test validate with missing config fields
1202    #[tokio::test]
1203    async fn test_execute_validate_missing_fields() {
1204        // Remove config file if exists
1205        let config_path = get_existing_config_path();
1206        let backup = if let Some(ref config_path) = config_path {
1207            let backup = config_path.with_extension("bak");
1208            fs::rename(config_path, &backup).ok();
1209            Some((config_path.clone(), backup))
1210        } else {
1211            None
1212        };
1213        let bot = dummy_bot();
1214        let res = execute_validate(&bot).await;
1215        // Should not panic, may return Ok or Err
1216        assert!(res.is_ok() || res.is_err());
1217        // Restore config
1218        if let Some((config_path, backup)) = backup {
1219            fs::rename(&backup, &config_path).ok();
1220        }
1221    }
1222
1223    /// Test execute_config with no flags (should print help)
1224    #[tokio::test]
1225    async fn test_execute_config_no_flags() {
1226        let res = execute_config(false, false, false).await;
1227        assert!(res.is_ok());
1228    }
1229
1230    // #[tokio::test]
1231    // async fn test_execute_setup_stdin_error() {
1232    //     // Simulate stdin read_line error by replacing stdin
1233    //     // This test is only illustrative, as replacing stdin is non-trivial in Rust
1234    //     // In real code, consider refactoring to inject input source
1235    //     // Here we just check that setup does not panic on empty input
1236    //     let _ = execute_setup().await;
1237    // }
1238
1239    #[tokio::test]
1240    async fn test_execute_config_toml_deserialize_error() {
1241        // Simulate toml::from_str error by passing invalid TOML
1242        let result = toml::from_str::<Config>("invalid = toml");
1243        assert!(result.is_err());
1244    }
1245
1246    // #[tokio::test]
1247    // async fn test_execute_config_flag_combinations() {
1248    //     // All combinations of show/init/wizard
1249    //     let combos = vec![
1250    //         (true, false, false),
1251    //         (false, true, false),
1252    //         (false, false, true),
1253    //         (true, true, false),
1254    //         (true, false, true),
1255    //         (false, true, true),
1256    //         (true, true, true),
1257    //         (false, false, false),
1258    //     ];
1259    //     for (show, init, wizard) in combos {
1260    //         let _ = execute_config(show, init, wizard).await;
1261    //     }
1262    // }
1263}