Skip to main content

slack_rs/cli/
mod.rs

1//! CLI command routing and handlers
2
3mod context;
4mod handlers;
5mod help;
6pub mod introspection;
7
8pub use context::CliContext;
9pub use handlers::{
10    handle_export_command, handle_import_command, run_api_call, run_auth_login, run_install_skill,
11};
12pub use introspection::{
13    generate_commands_list, generate_help, generate_schema, CommandDef, CommandsListResponse,
14    HelpResponse, SchemaResponse,
15};
16
17use crate::api::{ApiClient, CommandResponse};
18use crate::commands;
19use crate::commands::ConversationSelector;
20use crate::debug;
21use crate::profile::{
22    create_token_store, default_config_path, load_config, make_token_key, resolve_profile_full,
23    TokenStore, TokenType,
24};
25use serde_json::Value;
26
27/// Resolve token with priority: SLACK_TOKEN env > token store
28///
29/// # Arguments
30/// * `slack_token_env` - Value of SLACK_TOKEN environment variable (None if unset)
31/// * `token_store` - Token store to retrieve tokens from
32/// * `token_key` - Key to use for token store lookup
33/// * `fallback_token_key` - Optional fallback key (e.g., bot token when user token not found)
34/// * `explicit_request` - Whether the token type was explicitly requested (via --token-type or default_token_type)
35///
36/// # Returns
37/// * `Ok(token)` - Successfully resolved token
38/// * `Err(message)` - Token resolution failed
39///
40/// # Token Resolution Priority
41/// 1. SLACK_TOKEN environment variable (if set, bypasses token store)
42/// 2. Token store with primary token_key
43/// 3. Token store with fallback_token_key (only if not explicit_request)
44/// 4. Error if no token found
45#[allow(dead_code)]
46pub fn resolve_token_for_wrapper(
47    slack_token_env: Option<String>,
48    token_store: &dyn TokenStore,
49    token_key: &str,
50    fallback_token_key: Option<&str>,
51    explicit_request: bool,
52) -> Result<String, String> {
53    // Priority 1: SLACK_TOKEN environment variable
54    if let Some(env_token) = slack_token_env {
55        return Ok(env_token);
56    }
57
58    // Priority 2: Token store with primary key
59    if let Ok(token) = token_store.get(token_key) {
60        return Ok(token);
61    }
62
63    // Priority 3: Fallback token (only if not explicit_request)
64    if !explicit_request {
65        if let Some(fallback_key) = fallback_token_key {
66            if let Ok(token) = token_store.get(fallback_key) {
67                eprintln!("Warning: Primary token not found, falling back to alternative token");
68                return Ok(token);
69            }
70        }
71    }
72
73    // Priority 4: Error
74    if explicit_request {
75        Err(
76            "No token found for explicitly requested token type. Set SLACK_TOKEN environment variable or run 'slack login' to obtain a token.".to_string()
77        )
78    } else {
79        Err(
80            "No token found. Set SLACK_TOKEN environment variable or run 'slack login' to obtain a token.".to_string()
81        )
82    }
83}
84
85/// Get API client for a profile with optional token type selection
86///
87/// # Arguments
88/// * `profile_name` - Optional profile name (defaults to "default")
89/// * `token_type` - Optional token type (bot/user). If None, uses profile default or bot fallback
90///
91/// # Token Resolution Priority
92/// 1. SLACK_TOKEN environment variable (if set, bypasses token store)
93/// 2. CLI flag token_type parameter (if provided)
94/// 3. Profile's default_token_type (if set)
95/// 4. Try user token first, fall back to bot token
96pub async fn get_api_client_with_token_type(
97    profile_name: Option<String>,
98    token_type: Option<TokenType>,
99) -> Result<ApiClient, String> {
100    // Check for SLACK_TOKEN environment variable first
101    if let Ok(env_token) = std::env::var("SLACK_TOKEN") {
102        return Ok(ApiClient::with_token(env_token));
103    }
104
105    let profile_name = profile_name.unwrap_or_else(|| "default".to_string());
106    let config_path = default_config_path().map_err(|e| e.to_string())?;
107    let config = load_config(&config_path).map_err(|e| e.to_string())?;
108
109    let profile = config
110        .get(&profile_name)
111        .ok_or_else(|| format!("Profile '{}' not found", profile_name))?;
112
113    let token_store = create_token_store().map_err(|e| e.to_string())?;
114
115    // Resolve token type: CLI flag > profile default > try user first with bot fallback
116    let resolved_token_type = token_type.or(profile.default_token_type);
117
118    let bot_token_key = make_token_key(&profile.team_id, &profile.user_id);
119    let user_token_key = format!("{}:{}:user", profile.team_id, profile.user_id);
120
121    let token = match resolved_token_type {
122        Some(TokenType::Bot) => {
123            // Explicitly requested bot token
124            token_store
125                .get(&bot_token_key)
126                .map_err(|e| format!("Failed to get bot token: {}", e))?
127        }
128        Some(TokenType::User) => {
129            // Explicitly requested user token
130            token_store
131                .get(&user_token_key)
132                .map_err(|e| format!("Failed to get user token: {}", e))?
133        }
134        None => {
135            // No explicit preference, try user token first (for APIs that require user scope)
136            match token_store.get(&user_token_key) {
137                Ok(user_token) => user_token,
138                Err(_) => {
139                    // Fall back to bot token
140                    token_store
141                        .get(&bot_token_key)
142                        .map_err(|e| format!("Failed to get token: {}", e))?
143                }
144            }
145        }
146    };
147
148    Ok(ApiClient::with_token(token))
149}
150
151/// Get API client for a profile (legacy function, maintains backward compatibility)
152#[allow(dead_code)]
153pub async fn get_api_client(profile_name: Option<String>) -> Result<ApiClient, String> {
154    get_api_client_with_token_type(profile_name, None).await
155}
156
157/// Check if a flag exists in args
158pub fn has_flag(args: &[String], flag: &str) -> bool {
159    args.iter().any(|arg| arg == flag)
160}
161
162/// Determine if output should be raw based on SLACKRS_OUTPUT environment variable and --raw flag
163///
164/// # Arguments
165/// * `args` - Command line arguments
166///
167/// # Returns
168/// * `true` if output should be raw (without envelope)
169/// * `false` if output should include envelope
170///
171/// # Priority
172/// 1. --raw flag (highest priority)
173/// 2. SLACKRS_OUTPUT environment variable ("raw" or "envelope")
174/// 3. Default to envelope (false)
175pub fn should_output_raw(args: &[String]) -> bool {
176    // Priority 1: --raw flag always wins
177    if has_flag(args, "--raw") {
178        return true;
179    }
180
181    // Priority 2: Check SLACKRS_OUTPUT environment variable
182    if let Ok(output_mode) = std::env::var("SLACKRS_OUTPUT") {
183        return output_mode.trim().to_lowercase() == "raw";
184    }
185
186    // Priority 3: Default to envelope (false)
187    false
188}
189
190/// Check if error message indicates non-interactive mode failure
191pub fn is_non_interactive_error(error_msg: &str) -> bool {
192    error_msg.contains("Non-interactive mode error")
193        || error_msg.contains("Use --yes flag to confirm in non-interactive mode")
194}
195
196/// Wrap response with unified envelope including metadata
197#[allow(dead_code)]
198pub async fn wrap_with_envelope(
199    response: Value,
200    method: &str,
201    command: &str,
202    profile_name: Option<String>,
203) -> Result<CommandResponse, String> {
204    wrap_with_envelope_and_token_type(response, method, command, profile_name, None).await
205}
206
207/// Wrap response with unified envelope including metadata and explicit token type
208pub async fn wrap_with_envelope_and_token_type(
209    response: Value,
210    method: &str,
211    command: &str,
212    profile_name: Option<String>,
213    explicit_token_type: Option<TokenType>,
214) -> Result<CommandResponse, String> {
215    let profile_name_str = profile_name.unwrap_or_else(|| "default".to_string());
216    let config_path = default_config_path().map_err(|e| e.to_string())?;
217    let profile = resolve_profile_full(&config_path, &profile_name_str)
218        .map_err(|e| format!("Failed to resolve profile '{}': {}", profile_name_str, e))?;
219
220    // Resolve token type for metadata
221    let token_type_str = if let Some(explicit) = explicit_token_type {
222        // If explicitly specified via --token-type, use that
223        Some(explicit.to_string())
224    } else if std::env::var("SLACK_TOKEN").is_ok() {
225        // If using SLACK_TOKEN, use profile's default_token_type if set, otherwise "bot"
226        Some(
227            profile
228                .default_token_type
229                .map(|t| t.to_string())
230                .unwrap_or_else(|| "bot".to_string()),
231        )
232    } else {
233        // Resolve from token store (check which token exists)
234        let token_store = create_token_store().map_err(|e| e.to_string())?;
235        let bot_token_key = make_token_key(&profile.team_id, &profile.user_id);
236        let user_token_key = format!("{}:{}:user", profile.team_id, profile.user_id);
237
238        // Try to determine which token was used based on default_token_type
239        let resolved_type = profile.default_token_type.or_else(|| {
240            // If no default, check which token exists (try user first, then bot)
241            if token_store.get(&user_token_key).is_ok() {
242                Some(TokenType::User)
243            } else if token_store.get(&bot_token_key).is_ok() {
244                Some(TokenType::Bot)
245            } else {
246                None
247            }
248        });
249
250        resolved_type.map(|t| t.to_string())
251    };
252
253    Ok(CommandResponse::with_token_type(
254        response,
255        Some(profile_name_str),
256        profile.team_id,
257        profile.user_id,
258        method.to_string(),
259        command.to_string(),
260        token_type_str,
261    ))
262}
263
264/// Resolve profile name with priority: --profile flag > SLACK_PROFILE env > "default"
265///
266/// This function implements the unified profile selection logic across all CLI commands.
267/// It searches for `--profile` in any position within the args array, supporting both
268/// `--profile=name` and `--profile name` formats.
269///
270/// # Arguments
271/// * `args` - Command line arguments (including subcommands and flags)
272///
273/// # Returns
274/// Profile name resolved according to priority rules
275///
276/// # Priority
277/// 1. `--profile` flag from command line (either format)
278/// 2. `SLACK_PROFILE` environment variable
279/// 3. "default" as fallback
280pub fn resolve_profile_name(args: &[String]) -> String {
281    // Priority 1: Check for --profile flag in args
282    if let Some(profile) = get_option(args, "--profile=") {
283        return profile;
284    }
285
286    // Priority 2: Check SLACK_PROFILE environment variable
287    if let Ok(profile) = std::env::var("SLACK_PROFILE") {
288        return profile;
289    }
290
291    // Priority 3: Default to "default"
292    "default".to_string()
293}
294
295/// Get option value from args
296/// Supports both --key=value and --key value formats
297/// When using space-separated format, value must not start with '-'
298pub fn get_option(args: &[String], prefix: &str) -> Option<String> {
299    // First try --key=value format
300    if let Some(value) = args
301        .iter()
302        .find(|arg| arg.starts_with(prefix))
303        .and_then(|arg| arg.strip_prefix(prefix))
304        .map(|s| s.to_string())
305    {
306        return Some(value);
307    }
308
309    // Then try --key value format (space-separated)
310    // Extract the flag name without the '=' suffix
311    let flag = prefix.strip_suffix('=').unwrap_or(prefix);
312    if let Some(pos) = args.iter().position(|arg| arg == flag) {
313        if let Some(value) = args.get(pos + 1) {
314            // Only treat as value if it doesn't start with '-'
315            if !value.starts_with('-') {
316                return Some(value.clone());
317            }
318        }
319    }
320
321    None
322}
323
324/// Parse token type from command line arguments
325/// Supports both --token-type=VALUE and --token-type VALUE formats
326pub fn parse_token_type(args: &[String]) -> Result<Option<TokenType>, String> {
327    // First try --token-type=VALUE format
328    if let Some(token_type_str) = get_option(args, "--token-type=") {
329        return token_type_str
330            .parse::<TokenType>()
331            .map(Some)
332            .map_err(|e| e.to_string());
333    }
334
335    // Then try --token-type VALUE format (space-separated)
336    if let Some(pos) = args.iter().position(|arg| arg == "--token-type") {
337        if let Some(value) = args.get(pos + 1) {
338            return value
339                .parse::<TokenType>()
340                .map(Some)
341                .map_err(|e| e.to_string());
342        } else {
343            return Err("--token-type requires a value (bot or user)".to_string());
344        }
345    }
346
347    Ok(None)
348}
349
350pub async fn run_search(args: &[String]) -> Result<(), String> {
351    let query = args[2].clone();
352    let count = get_option(args, "--count=").and_then(|s| s.parse().ok());
353    let page = get_option(args, "--page=").and_then(|s| s.parse().ok());
354    let sort = get_option(args, "--sort=");
355    let sort_dir = get_option(args, "--sort_dir=");
356    let profile_name = resolve_profile_name(args);
357    let token_type = parse_token_type(args)?;
358    let raw = should_output_raw(args);
359
360    let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
361    let response = commands::search(&client, query, count, page, sort, sort_dir)
362        .await
363        .map_err(|e| e.to_string())?;
364
365    // Display error guidance if response contains a known error
366    crate::api::display_wrapper_error_guidance(&response);
367
368    // Output with or without envelope
369    let output = if raw {
370        serde_json::to_string_pretty(&response).unwrap()
371    } else {
372        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
373        let wrapped = wrap_with_envelope_and_token_type(
374            response_value,
375            "search.messages",
376            "search",
377            Some(profile_name),
378            token_type,
379        )
380        .await?;
381        serde_json::to_string_pretty(&wrapped).unwrap()
382    };
383
384    println!("{}", output);
385    Ok(())
386}
387
388/// Get all options with a specific prefix from args
389/// Supports both --key=value and --key value formats (can be mixed)
390/// When using space-separated format, value must not start with '-'
391pub fn get_all_options(args: &[String], prefix: &str) -> Vec<String> {
392    let mut results = Vec::new();
393
394    // Collect --key=value format
395    results.extend(
396        args.iter()
397            .filter(|arg| arg.starts_with(prefix))
398            .filter_map(|arg| arg.strip_prefix(prefix))
399            .map(|s| s.to_string()),
400    );
401
402    // Collect --key value format (space-separated)
403    let flag = prefix.strip_suffix('=').unwrap_or(prefix);
404    let mut i = 0;
405    while i < args.len() {
406        if args[i] == flag {
407            if let Some(value) = args.get(i + 1) {
408                // Only treat as value if it doesn't start with '-'
409                if !value.starts_with('-') {
410                    results.push(value.clone());
411                    i += 2; // Skip both flag and value
412                    continue;
413                }
414            }
415        }
416        i += 1;
417    }
418
419    results
420}
421
422pub async fn run_conv_list(args: &[String]) -> Result<(), String> {
423    // Check for --help flag before API call
424    if has_flag(args, "--help") || has_flag(args, "-h") {
425        print_conv_usage(&args[0]);
426        return Ok(());
427    }
428
429    let types = get_option(args, "--types=");
430    let include_private = has_flag(args, "--include-private");
431    let all = has_flag(args, "--all");
432    let limit = get_option(args, "--limit=").and_then(|s| s.parse().ok());
433    let profile_name = resolve_profile_name(args);
434    let token_type = parse_token_type(args)?;
435    let filter_strings = get_all_options(args, "--filter=");
436    let raw = should_output_raw(args);
437
438    // Validate: --types is mutually exclusive with --include-private and --all
439    if types.is_some() && (include_private || all) {
440        return Err("Error: --types cannot be used with --include-private or --all".to_string());
441    }
442
443    // Resolve types based on flags
444    let resolved_types = if let Some(explicit_types) = types {
445        // User explicitly specified types
446        Some(explicit_types)
447    } else if all {
448        // --all flag: include all conversation types
449        Some("public_channel,private_channel,im,mpim".to_string())
450    } else if include_private {
451        // --include-private flag: include public and private channels (same as default now)
452        Some("public_channel,private_channel".to_string())
453    } else {
454        // No flags: use default (public and private channels)
455        Some("public_channel,private_channel".to_string())
456    };
457
458    // Parse format option (default: json)
459    let format = if let Some(fmt_str) = get_option(args, "--format=") {
460        commands::OutputFormat::parse(&fmt_str)?
461    } else {
462        commands::OutputFormat::Json
463    };
464
465    // Validate --raw compatibility
466    if raw && format != commands::OutputFormat::Json {
467        return Err(format!(
468            "--raw is only valid with --format json, but got --format {}",
469            format
470        ));
471    }
472
473    // Parse sort options
474    let sort_key = if let Some(sort_str) = get_option(args, "--sort=") {
475        Some(commands::SortKey::parse(&sort_str)?)
476    } else {
477        None
478    };
479
480    let sort_dir = if let Some(dir_str) = get_option(args, "--sort-dir=") {
481        commands::SortDirection::parse(&dir_str)?
482    } else {
483        commands::SortDirection::default()
484    };
485
486    // Parse filters
487    let filters: Result<Vec<_>, _> = filter_strings
488        .iter()
489        .map(|s| commands::ConversationFilter::parse(s))
490        .collect();
491    let filters = filters.map_err(|e| e.to_string())?;
492
493    // Get debug level from args
494    let debug_level = debug::get_debug_level(args);
495
496    // Log debug information if --debug or --trace flag is present
497    let token_store_backend = if std::env::var("SLACK_TOKEN").is_ok() {
498        "environment"
499    } else {
500        "file"
501    };
502
503    // Resolve actual token type for debug output
504    let resolved_token_type = if let Some(explicit) = token_type {
505        explicit
506    } else {
507        // Get profile to check default_token_type
508        let config_path = default_config_path().map_err(|e| e.to_string())?;
509        let profile = resolve_profile_full(&config_path, &profile_name)
510            .map_err(|e| format!("Failed to resolve profile '{}': {}", profile_name, e))?;
511
512        if let Some(default_type) = profile.default_token_type {
513            default_type
514        } else {
515            // Infer from token availability
516            let token_store = create_token_store().map_err(|e| e.to_string())?;
517            let user_token_key = format!("{}:{}:user", profile.team_id, profile.user_id);
518            if token_store.get(&user_token_key).is_ok() {
519                TokenType::User
520            } else {
521                TokenType::Bot
522            }
523        }
524    };
525
526    let endpoint = "https://slack.com/api/conversations.list";
527
528    debug::log_api_context(
529        debug_level,
530        Some(&profile_name),
531        token_store_backend,
532        resolved_token_type.as_str(),
533        "conversations.list",
534        endpoint,
535    );
536
537    let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
538    let mut response = commands::conv_list(&client, resolved_types, limit)
539        .await
540        .map_err(|e| e.to_string())?;
541
542    // Log error code if present
543    debug::log_error_code(
544        debug_level,
545        &serde_json::to_value(&response).unwrap_or_default(),
546    );
547
548    // Display error guidance if response contains a known error
549    crate::api::display_wrapper_error_guidance(&response);
550
551    // Apply filters
552    commands::apply_filters(&mut response, &filters);
553
554    // Apply sorting if specified
555    if let Some(key) = sort_key {
556        commands::sort_conversations(&mut response, key, sort_dir);
557    }
558
559    // Format output: non-JSON formats bypass raw/envelope logic
560    let output = if format != commands::OutputFormat::Json {
561        commands::format_response(&response, format)?
562    } else if raw {
563        serde_json::to_string_pretty(&response).unwrap()
564    } else {
565        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
566        let wrapped = wrap_with_envelope_and_token_type(
567            response_value,
568            "conversations.list",
569            "conv list",
570            Some(profile_name),
571            token_type,
572        )
573        .await?;
574        serde_json::to_string_pretty(&wrapped).unwrap()
575    };
576
577    println!("{}", output);
578    Ok(())
579}
580
581pub async fn run_conv_select(args: &[String]) -> Result<(), String> {
582    // Check for --help flag before API call
583    if has_flag(args, "--help") || has_flag(args, "-h") {
584        print_conv_usage(&args[0]);
585        return Ok(());
586    }
587
588    let types = get_option(args, "--types=");
589    let limit = get_option(args, "--limit=").and_then(|s| s.parse().ok());
590    let profile_name = resolve_profile_name(args);
591    let token_type = parse_token_type(args)?;
592    let filter_strings = get_all_options(args, "--filter=");
593
594    // Parse filters
595    let filters: Result<Vec<_>, _> = filter_strings
596        .iter()
597        .map(|s| commands::ConversationFilter::parse(s))
598        .collect();
599    let filters = filters.map_err(|e| e.to_string())?;
600
601    // Resolve types: default to public_channel,private_channel if not specified
602    let resolved_types = types.or(Some("public_channel,private_channel".to_string()));
603
604    let client = get_api_client_with_token_type(Some(profile_name), token_type).await?;
605    let mut response = commands::conv_list(&client, resolved_types, limit)
606        .await
607        .map_err(|e| e.to_string())?;
608
609    // Apply filters
610    commands::apply_filters(&mut response, &filters);
611
612    // Extract conversations and present selection
613    let items = commands::extract_conversations(&response);
614    let selector = commands::StdinSelector;
615    let channel_id = selector.select(&items)?;
616
617    println!("{}", channel_id);
618    Ok(())
619}
620
621pub async fn run_conv_search(args: &[String]) -> Result<(), String> {
622    // Check for --help flag before pattern extraction
623    if has_flag(args, "--help") || has_flag(args, "-h") {
624        print_conv_usage(&args[0]);
625        return Ok(());
626    }
627
628    // Extract the search pattern (first non-flag argument after "search")
629    let pattern = args
630        .get(3)
631        .filter(|arg| !arg.starts_with("--"))
632        .ok_or_else(|| "Search pattern is required".to_string())?
633        .clone();
634
635    let types = get_option(args, "--types=");
636    let limit = get_option(args, "--limit=").and_then(|s| s.parse().ok());
637    let profile_name = resolve_profile_name(args);
638    let token_type = parse_token_type(args)?;
639    let raw = should_output_raw(args);
640    let select = has_flag(args, "--select");
641
642    // Parse additional filters from --filter= flags
643    let filter_strings = get_all_options(args, "--filter=");
644
645    // Parse format option (default: json)
646    let format = if let Some(fmt_str) = get_option(args, "--format=") {
647        commands::OutputFormat::parse(&fmt_str)?
648    } else {
649        commands::OutputFormat::Json
650    };
651
652    // Validate --raw compatibility
653    if raw && format != commands::OutputFormat::Json {
654        return Err(format!(
655            "--raw is only valid with --format json, but got --format {}",
656            format
657        ));
658    }
659
660    // Parse sort options
661    let sort_key = if let Some(sort_str) = get_option(args, "--sort=") {
662        Some(commands::SortKey::parse(&sort_str)?)
663    } else {
664        None
665    };
666
667    let sort_dir = if let Some(dir_str) = get_option(args, "--sort-dir=") {
668        commands::SortDirection::parse(&dir_str)?
669    } else {
670        commands::SortDirection::default()
671    };
672
673    // Build filters: inject name:<pattern> filter + any additional filters
674    let mut filters: Vec<commands::ConversationFilter> =
675        vec![commands::ConversationFilter::Name(pattern)];
676
677    // Parse and add additional filters
678    for filter_str in filter_strings {
679        filters.push(commands::ConversationFilter::parse(&filter_str).map_err(|e| e.to_string())?);
680    }
681
682    // Resolve types: default to public_channel,private_channel if not specified
683    let resolved_types = types.or(Some("public_channel,private_channel".to_string()));
684
685    let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
686    let mut response = commands::conv_list(&client, resolved_types, limit)
687        .await
688        .map_err(|e| e.to_string())?;
689
690    // Apply filters
691    commands::apply_filters(&mut response, &filters);
692
693    // Apply sorting if specified
694    if let Some(key) = sort_key {
695        commands::sort_conversations(&mut response, key, sort_dir);
696    }
697
698    // If --select flag is present, use interactive selection
699    if select {
700        let items = commands::extract_conversations(&response);
701        let selector = commands::StdinSelector;
702        let channel_id = selector.select(&items)?;
703        println!("{}", channel_id);
704        return Ok(());
705    }
706
707    // Format output: non-JSON formats bypass raw/envelope logic
708    let output = if format != commands::OutputFormat::Json {
709        commands::format_response(&response, format)?
710    } else if raw {
711        serde_json::to_string_pretty(&response).unwrap()
712    } else {
713        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
714        let wrapped = wrap_with_envelope_and_token_type(
715            response_value,
716            "conversations.list",
717            "conv search",
718            Some(profile_name),
719            token_type,
720        )
721        .await?;
722        serde_json::to_string_pretty(&wrapped).unwrap()
723    };
724
725    println!("{}", output);
726    Ok(())
727}
728
729pub async fn run_conv_history(args: &[String]) -> Result<(), String> {
730    // Check for --help flag before API call
731    if has_flag(args, "--help") || has_flag(args, "-h") {
732        print_conv_usage(&args[0]);
733        return Ok(());
734    }
735
736    let interactive = has_flag(args, "--interactive");
737
738    let channel = if interactive {
739        // Use conv_select logic to get channel
740        let types = get_option(args, "--types=");
741        let profile_name_inner = resolve_profile_name(args);
742        let filter_strings = get_all_options(args, "--filter=");
743
744        // Parse filters
745        let filters: Result<Vec<_>, _> = filter_strings
746            .iter()
747            .map(|s| commands::ConversationFilter::parse(s))
748            .collect();
749        let filters = filters.map_err(|e| e.to_string())?;
750
751        // Resolve types: default to public_channel,private_channel if not specified
752        let resolved_types = types.or(Some("public_channel,private_channel".to_string()));
753
754        let token_type_inner = parse_token_type(args)?;
755        let client =
756            get_api_client_with_token_type(Some(profile_name_inner), token_type_inner).await?;
757        let mut response = commands::conv_list(&client, resolved_types, None)
758            .await
759            .map_err(|e| e.to_string())?;
760
761        // Apply filters
762        commands::apply_filters(&mut response, &filters);
763
764        // Extract conversations and present selection
765        let items = commands::extract_conversations(&response);
766        let selector = commands::StdinSelector;
767        selector.select(&items)?
768    } else {
769        if args.len() < 4 {
770            return Err("Channel argument required when --interactive is not used".to_string());
771        }
772        args[3].clone()
773    };
774
775    let limit = get_option(args, "--limit=").and_then(|s| s.parse().ok());
776    let oldest = get_option(args, "--oldest=");
777    let latest = get_option(args, "--latest=");
778    let profile_name = resolve_profile_name(args);
779    let token_type = parse_token_type(args)?;
780    let raw = should_output_raw(args);
781
782    // Get debug level from args
783    let debug_level = debug::get_debug_level(args);
784
785    // Log debug information if --debug or --trace flag is present
786    let token_store_backend = if std::env::var("SLACK_TOKEN").is_ok() {
787        "environment"
788    } else {
789        "file"
790    };
791
792    // Resolve actual token type for debug output
793    let resolved_token_type = if let Some(explicit) = token_type {
794        explicit
795    } else {
796        let config_path = default_config_path().map_err(|e| e.to_string())?;
797        let profile = resolve_profile_full(&config_path, &profile_name)
798            .map_err(|e| format!("Failed to resolve profile '{}': {}", profile_name, e))?;
799
800        if let Some(default_type) = profile.default_token_type {
801            default_type
802        } else {
803            let token_store = create_token_store().map_err(|e| e.to_string())?;
804            let user_token_key = format!("{}:{}:user", profile.team_id, profile.user_id);
805            if token_store.get(&user_token_key).is_ok() {
806                TokenType::User
807            } else {
808                TokenType::Bot
809            }
810        }
811    };
812
813    let endpoint = "https://slack.com/api/conversations.history";
814
815    debug::log_api_context(
816        debug_level,
817        Some(&profile_name),
818        token_store_backend,
819        resolved_token_type.as_str(),
820        "conversations.history",
821        endpoint,
822    );
823
824    let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
825    let response = commands::conv_history(&client, channel, limit, oldest, latest)
826        .await
827        .map_err(|e| e.to_string())?;
828
829    // Log error code if present
830    debug::log_error_code(
831        debug_level,
832        &serde_json::to_value(&response).unwrap_or_default(),
833    );
834
835    // Display error guidance if response contains a known error
836    crate::api::display_wrapper_error_guidance(&response);
837
838    // Output with or without envelope
839    let output = if raw {
840        serde_json::to_string_pretty(&response).unwrap()
841    } else {
842        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
843        let wrapped = wrap_with_envelope_and_token_type(
844            response_value,
845            "conversations.history",
846            "conv history",
847            Some(profile_name),
848            token_type,
849        )
850        .await?;
851        serde_json::to_string_pretty(&wrapped).unwrap()
852    };
853
854    println!("{}", output);
855    Ok(())
856}
857
858pub async fn run_thread_get(args: &[String]) -> Result<(), String> {
859    // Check for --help flag before API call
860    if has_flag(args, "--help") || has_flag(args, "-h") {
861        print_thread_usage(&args[0]);
862        return Ok(());
863    }
864
865    // Parse required arguments: channel and thread_ts
866    if args.len() < 5 {
867        return Err("Usage: slack-rs thread get <channel> <thread_ts> [--limit=N] [--inclusive] [--raw] [--profile=NAME] [--token-type=bot|user]".to_string());
868    }
869
870    let channel = args[3].clone();
871    let thread_ts = args[4].clone();
872    let limit = get_option(args, "--limit=").and_then(|s| s.parse().ok());
873    let inclusive = has_flag(args, "--inclusive");
874    let profile_name = resolve_profile_name(args);
875    let token_type = parse_token_type(args)?;
876    let raw = should_output_raw(args);
877
878    // Get debug level from args
879    let debug_level = debug::get_debug_level(args);
880
881    // Log debug information if --debug or --trace flag is present
882    let token_store_backend = if std::env::var("SLACK_TOKEN").is_ok() {
883        "environment"
884    } else {
885        "file"
886    };
887
888    // Resolve actual token type for debug output
889    let resolved_token_type = if let Some(explicit) = token_type {
890        explicit
891    } else {
892        let config_path = default_config_path().map_err(|e| e.to_string())?;
893        let profile = resolve_profile_full(&config_path, &profile_name)
894            .map_err(|e| format!("Failed to resolve profile '{}': {}", profile_name, e))?;
895
896        if let Some(default_type) = profile.default_token_type {
897            default_type
898        } else {
899            let token_store = create_token_store().map_err(|e| e.to_string())?;
900            let user_token_key = format!("{}:{}:user", profile.team_id, profile.user_id);
901            if token_store.get(&user_token_key).is_ok() {
902                TokenType::User
903            } else {
904                TokenType::Bot
905            }
906        }
907    };
908
909    let endpoint = "https://slack.com/api/conversations.replies";
910
911    debug::log_api_context(
912        debug_level,
913        Some(&profile_name),
914        token_store_backend,
915        resolved_token_type.as_str(),
916        "conversations.replies",
917        endpoint,
918    );
919
920    let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
921    let inclusive_opt = if inclusive { Some(true) } else { None };
922    let response = commands::thread_get(&client, channel, thread_ts, limit, inclusive_opt)
923        .await
924        .map_err(|e| e.to_string())?;
925
926    // Log error code if present
927    debug::log_error_code(
928        debug_level,
929        &serde_json::to_value(&response).unwrap_or_default(),
930    );
931
932    // Display error guidance if response contains a known error
933    crate::api::display_wrapper_error_guidance(&response);
934
935    // Output with or without envelope
936    let output = if raw {
937        serde_json::to_string_pretty(&response).unwrap()
938    } else {
939        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
940        let wrapped = wrap_with_envelope_and_token_type(
941            response_value,
942            "conversations.replies",
943            "thread get",
944            Some(profile_name),
945            token_type,
946        )
947        .await?;
948        serde_json::to_string_pretty(&wrapped).unwrap()
949    };
950
951    println!("{}", output);
952    Ok(())
953}
954
955pub async fn run_users_info(args: &[String]) -> Result<(), String> {
956    let user = args[3].clone();
957    let profile_name = resolve_profile_name(args);
958    let token_type = parse_token_type(args)?;
959    let raw = should_output_raw(args);
960
961    // Get debug level from args
962    let debug_level = debug::get_debug_level(args);
963
964    // Log debug information if --debug or --trace flag is present
965    let token_store_backend = if std::env::var("SLACK_TOKEN").is_ok() {
966        "environment"
967    } else {
968        "file"
969    };
970
971    // Resolve actual token type for debug output
972    let resolved_token_type = if let Some(explicit) = token_type {
973        explicit
974    } else {
975        let config_path = default_config_path().map_err(|e| e.to_string())?;
976        let profile = resolve_profile_full(&config_path, &profile_name)
977            .map_err(|e| format!("Failed to resolve profile '{}': {}", profile_name, e))?;
978
979        if let Some(default_type) = profile.default_token_type {
980            default_type
981        } else {
982            let token_store = create_token_store().map_err(|e| e.to_string())?;
983            let user_token_key = format!("{}:{}:user", profile.team_id, profile.user_id);
984            if token_store.get(&user_token_key).is_ok() {
985                TokenType::User
986            } else {
987                TokenType::Bot
988            }
989        }
990    };
991
992    let endpoint = "https://slack.com/api/users.info";
993
994    debug::log_api_context(
995        debug_level,
996        Some(&profile_name),
997        token_store_backend,
998        resolved_token_type.as_str(),
999        "users.info",
1000        endpoint,
1001    );
1002
1003    let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
1004    let response = commands::users_info(&client, user)
1005        .await
1006        .map_err(|e| e.to_string())?;
1007
1008    // Log error code if present
1009    debug::log_error_code(
1010        debug_level,
1011        &serde_json::to_value(&response).unwrap_or_default(),
1012    );
1013
1014    // Display error guidance if response contains a known error
1015    crate::api::display_wrapper_error_guidance(&response);
1016
1017    // Output with or without envelope
1018    let output = if raw {
1019        serde_json::to_string_pretty(&response).unwrap()
1020    } else {
1021        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
1022        let wrapped = wrap_with_envelope_and_token_type(
1023            response_value,
1024            "users.info",
1025            "users info",
1026            Some(profile_name),
1027            token_type,
1028        )
1029        .await?;
1030        serde_json::to_string_pretty(&wrapped).unwrap()
1031    };
1032
1033    println!("{}", output);
1034    Ok(())
1035}
1036
1037pub async fn run_users_cache_update(args: &[String]) -> Result<(), String> {
1038    let profile_name = resolve_profile_name(args);
1039    let force = has_flag(args, "--force");
1040    let token_type = parse_token_type(args)?;
1041
1042    let config_path = default_config_path().map_err(|e| e.to_string())?;
1043    let config = load_config(&config_path).map_err(|e| e.to_string())?;
1044
1045    let profile = config
1046        .get(&profile_name)
1047        .ok_or_else(|| format!("Profile '{}' not found", profile_name))?;
1048
1049    let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
1050
1051    commands::update_cache(&client, profile.team_id.clone(), force)
1052        .await
1053        .map_err(|e| e.to_string())?;
1054
1055    println!("Cache updated successfully for team {}", profile.team_id);
1056    Ok(())
1057}
1058
1059pub async fn run_users_resolve_mentions(args: &[String]) -> Result<(), String> {
1060    if args.len() < 4 {
1061        return Err(
1062            "Usage: users resolve-mentions <text> [--profile=NAME] [--format=FORMAT]".to_string(),
1063        );
1064    }
1065
1066    let text = args[3].clone();
1067    let profile_name = resolve_profile_name(args);
1068    let format_str = get_option(args, "--format=").unwrap_or_else(|| "display_name".to_string());
1069
1070    let format = format_str.parse::<commands::MentionFormat>().map_err(|_| {
1071        format!(
1072            "Invalid format: {}. Use display_name, real_name, or username",
1073            format_str
1074        )
1075    })?;
1076
1077    let config_path = default_config_path().map_err(|e| e.to_string())?;
1078    let config = load_config(&config_path).map_err(|e| e.to_string())?;
1079
1080    let profile = config
1081        .get(&profile_name)
1082        .ok_or_else(|| format!("Profile '{}' not found", profile_name))?;
1083
1084    let cache_path = commands::UsersCacheFile::default_path()?;
1085    let cache_file = commands::UsersCacheFile::load(&cache_path)?;
1086
1087    let workspace_cache = cache_file.get_workspace(&profile.team_id).ok_or_else(|| {
1088        format!(
1089            "No cache found for team {}. Run 'users cache-update' first.",
1090            profile.team_id
1091        )
1092    })?;
1093
1094    let result = commands::resolve_mentions(&text, workspace_cache, format);
1095    println!("{}", result);
1096    Ok(())
1097}
1098
1099/// Get team_id and user_id from profile
1100async fn get_team_and_user_ids_from_profile(
1101    profile_name: &str,
1102) -> Result<(String, String), String> {
1103    let config_path = default_config_path().map_err(|e| e.to_string())?;
1104    let profile = resolve_profile_full(&config_path, profile_name)
1105        .map_err(|e| format!("Failed to resolve profile '{}': {}", profile_name, e))?;
1106    Ok((profile.team_id, profile.user_id))
1107}
1108
1109pub async fn run_msg_post(args: &[String], non_interactive: bool) -> Result<(), String> {
1110    use crate::idempotency::{IdempotencyCheckResult, IdempotencyHandler};
1111
1112    if args.len() < 5 {
1113        return Err("Usage: msg post <channel> <text> [--thread-ts=TS] [--reply-broadcast] [--yes] [--profile=NAME] [--token-type=bot|user] [--idempotency-key=KEY]".to_string());
1114    }
1115
1116    let channel = args[3].clone();
1117    let text = args[4].clone();
1118    let thread_ts = get_option(args, "--thread-ts=");
1119    let reply_broadcast = has_flag(args, "--reply-broadcast");
1120    let yes = has_flag(args, "--yes");
1121    let profile_name = resolve_profile_name(args);
1122    let token_type = parse_token_type(args)?;
1123    let idempotency_key = get_option(args, "--idempotency-key=");
1124
1125    // Validate: --reply-broadcast requires --thread-ts
1126    if reply_broadcast && thread_ts.is_none() {
1127        return Err("Error: --reply-broadcast requires --thread-ts".to_string());
1128    }
1129
1130    let raw = should_output_raw(args);
1131    let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
1132
1133    // Check idempotency if key provided
1134    let (response_value, idempotency_status) = if let Some(key) = idempotency_key.clone() {
1135        let mut handler = IdempotencyHandler::new().map_err(|e| e.to_string())?;
1136
1137        // Build params for fingerprinting
1138        let mut params = serde_json::Map::new();
1139        params.insert("channel".to_string(), serde_json::json!(channel.clone()));
1140        params.insert("text".to_string(), serde_json::json!(text.clone()));
1141        if let Some(ref ts) = thread_ts {
1142            params.insert("thread_ts".to_string(), serde_json::json!(ts));
1143            if reply_broadcast {
1144                params.insert("reply_broadcast".to_string(), serde_json::json!(true));
1145            }
1146        }
1147
1148        // Get team_id and user_id from profile
1149        let (team_id, user_id) = get_team_and_user_ids_from_profile(&profile_name).await?;
1150
1151        match handler
1152            .check(
1153                Some(key.clone()),
1154                team_id.clone(),
1155                user_id.clone(),
1156                "chat.postMessage".to_string(),
1157                &params,
1158            )
1159            .map_err(|e| e.to_string())?
1160        {
1161            IdempotencyCheckResult::Replay {
1162                response, status, ..
1163            } => {
1164                // Return cached response
1165                (response, Some(status))
1166            }
1167            IdempotencyCheckResult::Execute {
1168                key: scoped_key,
1169                fingerprint,
1170            } => {
1171                // Execute and store
1172                let response = commands::msg_post(
1173                    &client,
1174                    channel,
1175                    text,
1176                    thread_ts,
1177                    reply_broadcast,
1178                    yes,
1179                    non_interactive,
1180                )
1181                .await
1182                .map_err(|e| e.to_string())?;
1183
1184                let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
1185
1186                // Store result
1187                handler
1188                    .store(scoped_key, fingerprint, response_value.clone())
1189                    .map_err(|e| e.to_string())?;
1190
1191                (
1192                    response_value,
1193                    Some(crate::idempotency::IdempotencyStatus::Executed),
1194                )
1195            }
1196            IdempotencyCheckResult::NoKey => unreachable!(),
1197        }
1198    } else {
1199        // No idempotency key - execute normally
1200        let response = commands::msg_post(
1201            &client,
1202            channel,
1203            text,
1204            thread_ts,
1205            reply_broadcast,
1206            yes,
1207            non_interactive,
1208        )
1209        .await
1210        .map_err(|e| e.to_string())?;
1211
1212        (
1213            serde_json::to_value(&response).map_err(|e| e.to_string())?,
1214            None,
1215        )
1216    };
1217
1218    // Display error guidance if response contains a known error
1219    if let Ok(api_response) =
1220        serde_json::from_value::<crate::api::ApiResponse>(response_value.clone())
1221    {
1222        crate::api::display_wrapper_error_guidance(&api_response);
1223    }
1224
1225    // Output with or without envelope
1226    let output = if raw {
1227        serde_json::to_string_pretty(&response_value).unwrap()
1228    } else {
1229        let mut wrapped = wrap_with_envelope_and_token_type(
1230            response_value,
1231            "chat.postMessage",
1232            "msg post",
1233            Some(profile_name),
1234            token_type,
1235        )
1236        .await?;
1237
1238        // Add idempotency metadata if key was provided
1239        if let (Some(key), Some(status)) = (idempotency_key, idempotency_status) {
1240            wrapped = wrapped.with_idempotency(
1241                key,
1242                match status {
1243                    crate::idempotency::IdempotencyStatus::Executed => "executed".to_string(),
1244                    crate::idempotency::IdempotencyStatus::Replayed => "replayed".to_string(),
1245                },
1246            );
1247        }
1248
1249        serde_json::to_string_pretty(&wrapped).unwrap()
1250    };
1251
1252    println!("{}", output);
1253    Ok(())
1254}
1255
1256pub async fn run_msg_update(args: &[String], non_interactive: bool) -> Result<(), String> {
1257    use crate::idempotency::{IdempotencyCheckResult, IdempotencyHandler};
1258
1259    if args.len() < 6 {
1260        return Err("Usage: msg update <channel> <ts> <text> [--yes] [--profile=NAME] [--token-type=bot|user] [--idempotency-key=KEY]".to_string());
1261    }
1262
1263    let channel = args[3].clone();
1264    let ts = args[4].clone();
1265    let text = args[5].clone();
1266    let yes = has_flag(args, "--yes");
1267    let profile_name = resolve_profile_name(args);
1268    let token_type = parse_token_type(args)?;
1269    let idempotency_key = get_option(args, "--idempotency-key=");
1270    let raw = should_output_raw(args);
1271
1272    let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
1273
1274    // Check idempotency if key provided
1275    let (response_value, idempotency_status) = if let Some(key) = idempotency_key.clone() {
1276        let mut handler = IdempotencyHandler::new().map_err(|e| e.to_string())?;
1277
1278        let mut params = serde_json::Map::new();
1279        params.insert("channel".to_string(), serde_json::json!(channel.clone()));
1280        params.insert("ts".to_string(), serde_json::json!(ts.clone()));
1281        params.insert("text".to_string(), serde_json::json!(text.clone()));
1282
1283        let (team_id, user_id) = get_team_and_user_ids_from_profile(&profile_name).await?;
1284
1285        match handler
1286            .check(
1287                Some(key.clone()),
1288                team_id,
1289                user_id,
1290                "chat.update".to_string(),
1291                &params,
1292            )
1293            .map_err(|e| e.to_string())?
1294        {
1295            IdempotencyCheckResult::Replay {
1296                response, status, ..
1297            } => (response, Some(status)),
1298            IdempotencyCheckResult::Execute {
1299                key: scoped_key,
1300                fingerprint,
1301            } => {
1302                let response =
1303                    commands::msg_update(&client, channel, ts, text, yes, non_interactive)
1304                        .await
1305                        .map_err(|e| e.to_string())?;
1306                let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
1307                handler
1308                    .store(scoped_key, fingerprint, response_value.clone())
1309                    .map_err(|e| e.to_string())?;
1310                (
1311                    response_value,
1312                    Some(crate::idempotency::IdempotencyStatus::Executed),
1313                )
1314            }
1315            IdempotencyCheckResult::NoKey => unreachable!(),
1316        }
1317    } else {
1318        let response = commands::msg_update(&client, channel, ts, text, yes, non_interactive)
1319            .await
1320            .map_err(|e| e.to_string())?;
1321        (
1322            serde_json::to_value(&response).map_err(|e| e.to_string())?,
1323            None,
1324        )
1325    };
1326
1327    if let Ok(api_response) =
1328        serde_json::from_value::<crate::api::ApiResponse>(response_value.clone())
1329    {
1330        crate::api::display_wrapper_error_guidance(&api_response);
1331    }
1332
1333    let output = if raw {
1334        serde_json::to_string_pretty(&response_value).unwrap()
1335    } else {
1336        let mut wrapped = wrap_with_envelope_and_token_type(
1337            response_value,
1338            "chat.update",
1339            "msg update",
1340            Some(profile_name),
1341            token_type,
1342        )
1343        .await?;
1344
1345        if let (Some(key), Some(status)) = (idempotency_key, idempotency_status) {
1346            wrapped = wrapped.with_idempotency(
1347                key,
1348                match status {
1349                    crate::idempotency::IdempotencyStatus::Executed => "executed".to_string(),
1350                    crate::idempotency::IdempotencyStatus::Replayed => "replayed".to_string(),
1351                },
1352            );
1353        }
1354
1355        serde_json::to_string_pretty(&wrapped).unwrap()
1356    };
1357
1358    println!("{}", output);
1359    Ok(())
1360}
1361
1362pub async fn run_msg_delete(args: &[String], non_interactive: bool) -> Result<(), String> {
1363    use crate::idempotency::{IdempotencyCheckResult, IdempotencyHandler};
1364
1365    if args.len() < 5 {
1366        return Err(
1367            "Usage: msg delete <channel> <ts> [--yes] [--profile=NAME] [--token-type=bot|user] [--idempotency-key=KEY]"
1368                .to_string(),
1369        );
1370    }
1371
1372    let channel = args[3].clone();
1373    let ts = args[4].clone();
1374    let yes = has_flag(args, "--yes");
1375    let profile_name = resolve_profile_name(args);
1376    let token_type = parse_token_type(args)?;
1377    let idempotency_key = get_option(args, "--idempotency-key=");
1378    let raw = should_output_raw(args);
1379
1380    let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
1381
1382    let (response_value, idempotency_status) = if let Some(key) = idempotency_key.clone() {
1383        let mut handler = IdempotencyHandler::new().map_err(|e| e.to_string())?;
1384        let mut params = serde_json::Map::new();
1385        params.insert("channel".to_string(), serde_json::json!(channel.clone()));
1386        params.insert("ts".to_string(), serde_json::json!(ts.clone()));
1387        let (team_id, user_id) = get_team_and_user_ids_from_profile(&profile_name).await?;
1388        match handler
1389            .check(
1390                Some(key.clone()),
1391                team_id,
1392                user_id,
1393                "chat.delete".to_string(),
1394                &params,
1395            )
1396            .map_err(|e| e.to_string())?
1397        {
1398            IdempotencyCheckResult::Replay {
1399                response, status, ..
1400            } => (response, Some(status)),
1401            IdempotencyCheckResult::Execute {
1402                key: scoped_key,
1403                fingerprint,
1404            } => {
1405                let response = commands::msg_delete(&client, channel, ts, yes, non_interactive)
1406                    .await
1407                    .map_err(|e| e.to_string())?;
1408                let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
1409                handler
1410                    .store(scoped_key, fingerprint, response_value.clone())
1411                    .map_err(|e| e.to_string())?;
1412                (
1413                    response_value,
1414                    Some(crate::idempotency::IdempotencyStatus::Executed),
1415                )
1416            }
1417            IdempotencyCheckResult::NoKey => unreachable!(),
1418        }
1419    } else {
1420        let response = commands::msg_delete(&client, channel, ts, yes, non_interactive)
1421            .await
1422            .map_err(|e| e.to_string())?;
1423        (
1424            serde_json::to_value(&response).map_err(|e| e.to_string())?,
1425            None,
1426        )
1427    };
1428
1429    if let Ok(api_response) =
1430        serde_json::from_value::<crate::api::ApiResponse>(response_value.clone())
1431    {
1432        crate::api::display_wrapper_error_guidance(&api_response);
1433    }
1434
1435    let output = if raw {
1436        serde_json::to_string_pretty(&response_value).unwrap()
1437    } else {
1438        let mut wrapped = wrap_with_envelope_and_token_type(
1439            response_value,
1440            "chat.delete",
1441            "msg delete",
1442            Some(profile_name),
1443            token_type,
1444        )
1445        .await?;
1446        if let (Some(key), Some(status)) = (idempotency_key, idempotency_status) {
1447            wrapped = wrapped.with_idempotency(
1448                key,
1449                match status {
1450                    crate::idempotency::IdempotencyStatus::Executed => "executed".to_string(),
1451                    crate::idempotency::IdempotencyStatus::Replayed => "replayed".to_string(),
1452                },
1453            );
1454        }
1455        serde_json::to_string_pretty(&wrapped).unwrap()
1456    };
1457
1458    println!("{}", output);
1459    Ok(())
1460}
1461
1462pub async fn run_react_add(args: &[String], non_interactive: bool) -> Result<(), String> {
1463    use crate::idempotency::{IdempotencyCheckResult, IdempotencyHandler};
1464
1465    if args.len() < 6 {
1466        return Err(
1467            "Usage: react add <channel> <ts> <emoji> [--yes] [--profile=NAME] [--token-type=bot|user] [--idempotency-key=KEY]"
1468                .to_string(),
1469        );
1470    }
1471
1472    let channel = args[3].clone();
1473    let ts = args[4].clone();
1474    let emoji = args[5].clone();
1475    let yes = has_flag(args, "--yes");
1476    let profile_name = resolve_profile_name(args);
1477    let token_type = parse_token_type(args)?;
1478    let idempotency_key = get_option(args, "--idempotency-key=");
1479    let raw = should_output_raw(args);
1480
1481    let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
1482
1483    let (response_value, idempotency_status) = if let Some(key) = idempotency_key.clone() {
1484        let mut handler = IdempotencyHandler::new().map_err(|e| e.to_string())?;
1485        let mut params = serde_json::Map::new();
1486        params.insert("channel".to_string(), serde_json::json!(channel.clone()));
1487        params.insert("timestamp".to_string(), serde_json::json!(ts.clone()));
1488        params.insert("name".to_string(), serde_json::json!(emoji.clone()));
1489        let (team_id, user_id) = get_team_and_user_ids_from_profile(&profile_name).await?;
1490        match handler
1491            .check(
1492                Some(key.clone()),
1493                team_id,
1494                user_id,
1495                "reactions.add".to_string(),
1496                &params,
1497            )
1498            .map_err(|e| e.to_string())?
1499        {
1500            IdempotencyCheckResult::Replay {
1501                response, status, ..
1502            } => (response, Some(status)),
1503            IdempotencyCheckResult::Execute {
1504                key: scoped_key,
1505                fingerprint,
1506            } => {
1507                let response =
1508                    commands::react_add(&client, channel, ts, emoji, yes, non_interactive)
1509                        .await
1510                        .map_err(|e| e.to_string())?;
1511                let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
1512                handler
1513                    .store(scoped_key, fingerprint, response_value.clone())
1514                    .map_err(|e| e.to_string())?;
1515                (
1516                    response_value,
1517                    Some(crate::idempotency::IdempotencyStatus::Executed),
1518                )
1519            }
1520            IdempotencyCheckResult::NoKey => unreachable!(),
1521        }
1522    } else {
1523        let response = commands::react_add(&client, channel, ts, emoji, yes, non_interactive)
1524            .await
1525            .map_err(|e| e.to_string())?;
1526        (
1527            serde_json::to_value(&response).map_err(|e| e.to_string())?,
1528            None,
1529        )
1530    };
1531
1532    if let Ok(api_response) =
1533        serde_json::from_value::<crate::api::ApiResponse>(response_value.clone())
1534    {
1535        crate::api::display_wrapper_error_guidance(&api_response);
1536    }
1537
1538    let output = if raw {
1539        serde_json::to_string_pretty(&response_value).unwrap()
1540    } else {
1541        let mut wrapped = wrap_with_envelope_and_token_type(
1542            response_value,
1543            "reactions.add",
1544            "react add",
1545            Some(profile_name),
1546            token_type,
1547        )
1548        .await?;
1549        if let (Some(key), Some(status)) = (idempotency_key, idempotency_status) {
1550            wrapped = wrapped.with_idempotency(
1551                key,
1552                match status {
1553                    crate::idempotency::IdempotencyStatus::Executed => "executed".to_string(),
1554                    crate::idempotency::IdempotencyStatus::Replayed => "replayed".to_string(),
1555                },
1556            );
1557        }
1558        serde_json::to_string_pretty(&wrapped).unwrap()
1559    };
1560
1561    println!("{}", output);
1562    Ok(())
1563}
1564
1565pub async fn run_react_remove(args: &[String], non_interactive: bool) -> Result<(), String> {
1566    use crate::idempotency::{IdempotencyCheckResult, IdempotencyHandler};
1567
1568    if args.len() < 6 {
1569        return Err(
1570            "Usage: react remove <channel> <ts> <emoji> [--yes] [--profile=NAME] [--token-type=bot|user] [--idempotency-key=KEY]".to_string(),
1571        );
1572    }
1573
1574    let channel = args[3].clone();
1575    let ts = args[4].clone();
1576    let emoji = args[5].clone();
1577    let yes = has_flag(args, "--yes");
1578    let profile_name = resolve_profile_name(args);
1579    let token_type = parse_token_type(args)?;
1580    let idempotency_key = get_option(args, "--idempotency-key=");
1581    let raw = should_output_raw(args);
1582
1583    let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
1584
1585    let (response_value, idempotency_status) = if let Some(key) = idempotency_key.clone() {
1586        let mut handler = IdempotencyHandler::new().map_err(|e| e.to_string())?;
1587        let mut params = serde_json::Map::new();
1588        params.insert("channel".to_string(), serde_json::json!(channel.clone()));
1589        params.insert("timestamp".to_string(), serde_json::json!(ts.clone()));
1590        params.insert("name".to_string(), serde_json::json!(emoji.clone()));
1591        let (team_id, user_id) = get_team_and_user_ids_from_profile(&profile_name).await?;
1592        match handler
1593            .check(
1594                Some(key.clone()),
1595                team_id,
1596                user_id,
1597                "reactions.remove".to_string(),
1598                &params,
1599            )
1600            .map_err(|e| e.to_string())?
1601        {
1602            IdempotencyCheckResult::Replay {
1603                response, status, ..
1604            } => (response, Some(status)),
1605            IdempotencyCheckResult::Execute {
1606                key: scoped_key,
1607                fingerprint,
1608            } => {
1609                let response =
1610                    commands::react_remove(&client, channel, ts, emoji, yes, non_interactive)
1611                        .await
1612                        .map_err(|e| e.to_string())?;
1613                let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
1614                handler
1615                    .store(scoped_key, fingerprint, response_value.clone())
1616                    .map_err(|e| e.to_string())?;
1617                (
1618                    response_value,
1619                    Some(crate::idempotency::IdempotencyStatus::Executed),
1620                )
1621            }
1622            IdempotencyCheckResult::NoKey => unreachable!(),
1623        }
1624    } else {
1625        let response = commands::react_remove(&client, channel, ts, emoji, yes, non_interactive)
1626            .await
1627            .map_err(|e| e.to_string())?;
1628        (
1629            serde_json::to_value(&response).map_err(|e| e.to_string())?,
1630            None,
1631        )
1632    };
1633
1634    if let Ok(api_response) =
1635        serde_json::from_value::<crate::api::ApiResponse>(response_value.clone())
1636    {
1637        crate::api::display_wrapper_error_guidance(&api_response);
1638    }
1639
1640    let output = if raw {
1641        serde_json::to_string_pretty(&response_value).unwrap()
1642    } else {
1643        let mut wrapped = wrap_with_envelope_and_token_type(
1644            response_value,
1645            "reactions.remove",
1646            "react remove",
1647            Some(profile_name),
1648            token_type,
1649        )
1650        .await?;
1651        if let (Some(key), Some(status)) = (idempotency_key, idempotency_status) {
1652            wrapped = wrapped.with_idempotency(
1653                key,
1654                match status {
1655                    crate::idempotency::IdempotencyStatus::Executed => "executed".to_string(),
1656                    crate::idempotency::IdempotencyStatus::Replayed => "replayed".to_string(),
1657                },
1658            );
1659        }
1660        serde_json::to_string_pretty(&wrapped).unwrap()
1661    };
1662
1663    println!("{}", output);
1664    Ok(())
1665}
1666
1667pub async fn run_file_upload(args: &[String], non_interactive: bool) -> Result<(), String> {
1668    use crate::idempotency::{IdempotencyCheckResult, IdempotencyHandler};
1669
1670    if args.len() < 4 {
1671        return Err(
1672            "Usage: file upload <path> [--channel=ID] [--channels=IDs] [--title=TITLE] [--comment=TEXT] [--yes] [--profile=NAME] [--token-type=bot|user] [--idempotency-key=KEY]"
1673                .to_string(),
1674        );
1675    }
1676
1677    let file_path = args[3].clone();
1678    let channels = get_option(args, "--channel=").or_else(|| get_option(args, "--channels="));
1679    let title = get_option(args, "--title=");
1680    let comment = get_option(args, "--comment=");
1681    let yes = has_flag(args, "--yes");
1682    let profile_name = resolve_profile_name(args);
1683    let token_type = parse_token_type(args)?;
1684    let idempotency_key = get_option(args, "--idempotency-key=");
1685    let raw = should_output_raw(args);
1686
1687    let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
1688
1689    let (response_value, idempotency_status) = if let Some(key) = idempotency_key.clone() {
1690        let mut handler = IdempotencyHandler::new().map_err(|e| e.to_string())?;
1691        let mut params = serde_json::Map::new();
1692        params.insert("filename".to_string(), serde_json::json!(file_path.clone()));
1693        if let Some(ref ch) = channels {
1694            params.insert("channels".to_string(), serde_json::json!(ch));
1695        }
1696        if let Some(ref t) = title {
1697            params.insert("title".to_string(), serde_json::json!(t));
1698        }
1699        if let Some(ref c) = comment {
1700            params.insert("comment".to_string(), serde_json::json!(c));
1701        }
1702        let (team_id, user_id) = get_team_and_user_ids_from_profile(&profile_name).await?;
1703        match handler
1704            .check(
1705                Some(key.clone()),
1706                team_id,
1707                user_id,
1708                "files.upload".to_string(),
1709                &params,
1710            )
1711            .map_err(|e| e.to_string())?
1712        {
1713            IdempotencyCheckResult::Replay {
1714                response, status, ..
1715            } => (response, Some(status)),
1716            IdempotencyCheckResult::Execute {
1717                key: scoped_key,
1718                fingerprint,
1719            } => {
1720                let response = commands::file_upload(
1721                    &client,
1722                    file_path,
1723                    channels,
1724                    title,
1725                    comment,
1726                    yes,
1727                    non_interactive,
1728                )
1729                .await
1730                .map_err(|e| e.to_string())?;
1731                let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
1732                handler
1733                    .store(scoped_key, fingerprint, response_value.clone())
1734                    .map_err(|e| e.to_string())?;
1735                (
1736                    response_value,
1737                    Some(crate::idempotency::IdempotencyStatus::Executed),
1738                )
1739            }
1740            IdempotencyCheckResult::NoKey => unreachable!(),
1741        }
1742    } else {
1743        let response = commands::file_upload(
1744            &client,
1745            file_path,
1746            channels,
1747            title,
1748            comment,
1749            yes,
1750            non_interactive,
1751        )
1752        .await
1753        .map_err(|e| e.to_string())?;
1754        (
1755            serde_json::to_value(&response).map_err(|e| e.to_string())?,
1756            None,
1757        )
1758    };
1759
1760    crate::api::display_json_error_guidance(&response_value);
1761
1762    let output = if raw {
1763        serde_json::to_string_pretty(&response_value).unwrap()
1764    } else {
1765        let mut wrapped = wrap_with_envelope_and_token_type(
1766            response_value,
1767            "files.upload",
1768            "file upload",
1769            Some(profile_name),
1770            token_type,
1771        )
1772        .await?;
1773        if let (Some(key), Some(status)) = (idempotency_key, idempotency_status) {
1774            wrapped = wrapped.with_idempotency(
1775                key,
1776                match status {
1777                    crate::idempotency::IdempotencyStatus::Executed => "executed".to_string(),
1778                    crate::idempotency::IdempotencyStatus::Replayed => "replayed".to_string(),
1779                },
1780            );
1781        }
1782        serde_json::to_string_pretty(&wrapped).unwrap()
1783    };
1784
1785    println!("{}", output);
1786    Ok(())
1787}
1788
1789pub async fn run_file_download(args: &[String]) -> Result<(), String> {
1790    if args.len() < 3 {
1791        return Err(
1792            "Usage: file download [<file_id>] [--url=URL] [--out=PATH] [--profile=NAME] [--token-type=bot|user]"
1793                .to_string(),
1794        );
1795    }
1796
1797    // Parse arguments
1798    let file_id = args.get(3).filter(|arg| !arg.starts_with("--")).cloned();
1799    let url = get_option(args, "--url=");
1800    let out = get_option(args, "--out=");
1801    let profile_name = resolve_profile_name(args);
1802    let token_type = parse_token_type(args)?;
1803    let raw = should_output_raw(args);
1804
1805    // Validate: at least one of file_id or url must be provided
1806    if file_id.is_none() && url.is_none() {
1807        return Err("Either <file_id> or --url must be provided".to_string());
1808    }
1809
1810    let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
1811    let response = commands::file_download(&client, file_id, url, out)
1812        .await
1813        .map_err(|e| e.to_string())?;
1814
1815    // For --out -, don't print any output (file bytes already written to stdout)
1816    if let Some(out_path) = response.get("output").and_then(|v| v.as_str()) {
1817        if out_path == "-" {
1818            return Ok(());
1819        }
1820    }
1821
1822    // Display error guidance if response contains a known error
1823    crate::api::display_json_error_guidance(&response);
1824
1825    // Output with or without envelope
1826    let output = if raw {
1827        serde_json::to_string_pretty(&response).unwrap()
1828    } else {
1829        let wrapped = wrap_with_envelope_and_token_type(
1830            response,
1831            "files.info + download",
1832            "file download",
1833            Some(profile_name),
1834            token_type,
1835        )
1836        .await?;
1837        serde_json::to_string_pretty(&wrapped).unwrap()
1838    };
1839
1840    println!("{}", output);
1841    Ok(())
1842}
1843
1844pub fn print_conv_usage(prog: &str) {
1845    println!("Conv command usage:");
1846    println!(
1847        "  {} conv list [--types=TYPE] [--include-private] [--all] [--limit=N] [--filter=KEY:VALUE]... [--format=FORMAT] [--sort=KEY] [--sort-dir=DIR] [--raw] [--profile=NAME] [--token-type=bot|user]",
1848        prog
1849    );
1850    println!("    List conversations with optional filtering and sorting");
1851    println!("    Options accept both --option=value and --option value formats");
1852    println!("    Default: Includes public and private channels (limit=1000, auto-paginated)");
1853    println!("    Type shortcuts (mutually exclusive with --types):");
1854    println!("      - --include-private: Include private channels (same as default now)");
1855    println!(
1856        "      - --all: Include all conversation types (public_channel,private_channel,im,mpim)"
1857    );
1858    println!("    Filters: name:<glob>, is_member:true|false, is_private:true|false");
1859    println!("      - name:<glob>: Filter by channel name (supports * and ? wildcards)");
1860    println!("      - is_member:true|false: Filter by membership status");
1861    println!("      - is_private:true|false: Filter by channel privacy");
1862    println!("    Formats: json (default), jsonl, table, tsv");
1863    println!("      - json: JSON format with envelope (use --raw for raw Slack API response)");
1864    println!("      - jsonl: JSON Lines format (one object per line)");
1865    println!("      - table: Human-readable table format");
1866    println!("      - tsv: Tab-separated values");
1867    println!("    Sort keys: name, created, num_members");
1868    println!("      - name: Sort by channel name");
1869    println!("      - created: Sort by creation timestamp");
1870    println!("      - num_members: Sort by member count");
1871    println!("    Sort direction: asc (default), desc");
1872    println!("    Note: --raw is only valid with --format json");
1873    println!();
1874    println!(
1875        "  {} conv search <pattern> [--select] [--types=TYPE] [--limit=N] [--filter=KEY:VALUE]... [--format=FORMAT] [--sort=KEY] [--sort-dir=DIR] [--raw] [--profile=NAME] [--token-type=bot|user]",
1876        prog
1877    );
1878    println!("    Search conversations by name pattern (applies name:<pattern> filter)");
1879    println!("    Default: Includes public and private channels (limit=1000, auto-paginated)");
1880    println!("    Options accept both --option=value and --option value formats");
1881    println!("    --select: Interactively select from results and output channel ID only");
1882    println!();
1883    println!(
1884        "  {} conv select [--types=TYPE] [--filter=KEY:VALUE]... [--profile=NAME]",
1885        prog
1886    );
1887    println!("    Interactively select a conversation and output its channel ID");
1888    println!("    Default: Includes public and private channels (limit=1000, auto-paginated)");
1889    println!("    Options accept both --option=value and --option value formats");
1890    println!();
1891    println!(
1892        "  {} conv history <channel> [--limit=N] [--oldest=TS] [--latest=TS] [--profile=NAME] [--token-type=bot|user]",
1893        prog
1894    );
1895    println!(
1896        "  {} conv history --interactive [--types=TYPE] [--filter=KEY:VALUE]... [--limit=N] [--profile=NAME]",
1897        prog
1898    );
1899    println!("    Select channel interactively before fetching history");
1900    println!("    Default: Includes public and private channels (limit=1000, auto-paginated)");
1901    println!("    Options accept both --option=value and --option value formats");
1902}
1903
1904pub fn print_thread_usage(prog: &str) {
1905    println!("Thread command usage:");
1906    println!(
1907        "  {} thread get <channel> <thread_ts> [--limit=N] [--inclusive] [--raw] [--profile=NAME] [--token-type=bot|user]",
1908        prog
1909    );
1910    println!("    Get thread messages (conversation replies) for a specific thread");
1911    println!("    Arguments:");
1912    println!("      <channel>    - Channel ID containing the thread");
1913    println!("      <thread_ts>  - Timestamp of the parent message (thread identifier)");
1914    println!("    Options:");
1915    println!("      --limit=N           - Number of messages per page (default: 100)");
1916    println!("      --inclusive         - Include the parent message in results");
1917    println!("      --raw               - Output raw Slack API response without envelope");
1918    println!("      --profile=NAME      - Profile to use (default: 'default')");
1919    println!("      --token-type=TYPE   - Token type to use (bot or user)");
1920    println!("    Note: Automatically follows pagination to retrieve all thread messages");
1921}
1922
1923pub fn print_users_usage(prog: &str) {
1924    println!("Users command usage:");
1925    println!(
1926        "  {} users info <user_id> [--profile=NAME] [--token-type=bot|user]",
1927        prog
1928    );
1929    println!(
1930        "  {} users cache-update [--profile=NAME] [--force] [--token-type=bot|user]",
1931        prog
1932    );
1933    println!("  {} users resolve-mentions <text> [--profile=NAME] [--format=display_name|real_name|username]", prog);
1934    println!("  Options accept both --option=value and --option value formats");
1935}
1936
1937pub fn print_msg_usage(prog: &str) {
1938    println!("Msg command usage:");
1939    println!(
1940        "  {} msg post <channel> <text> [--thread-ts=TS] [--reply-broadcast] [--idempotency-key=KEY] [--profile=NAME] [--token-type=bot|user]",
1941        prog
1942    );
1943    println!("    Requires SLACKCLI_ALLOW_WRITE=true environment variable");
1944    println!(
1945        "  {} msg update <channel> <ts> <text> [--yes] [--idempotency-key=KEY] [--profile=NAME] [--token-type=bot|user]",
1946        prog
1947    );
1948    println!("    Requires SLACKCLI_ALLOW_WRITE=true environment variable");
1949    println!(
1950        "  {} msg delete <channel> <ts> [--yes] [--idempotency-key=KEY] [--profile=NAME] [--token-type=bot|user]",
1951        prog
1952    );
1953    println!("    Requires SLACKCLI_ALLOW_WRITE=true environment variable");
1954    println!("  Options accept both --option=value and --option value formats");
1955    println!("  --idempotency-key: Prevent duplicate writes (replays stored result on retry)");
1956}
1957
1958pub fn print_react_usage(prog: &str) {
1959    println!("React command usage:");
1960    println!(
1961        "  {} react add <channel> <ts> <emoji> [--idempotency-key=KEY] [--profile=NAME] [--token-type=bot|user]",
1962        prog
1963    );
1964    println!("    Requires SLACKCLI_ALLOW_WRITE=true environment variable");
1965    println!(
1966        "  {} react remove <channel> <ts> <emoji> [--yes] [--idempotency-key=KEY] [--profile=NAME] [--token-type=bot|user]",
1967        prog
1968    );
1969    println!("    Requires SLACKCLI_ALLOW_WRITE=true environment variable");
1970    println!("  Options accept both --option=value and --option value formats");
1971    println!("  --idempotency-key: Prevent duplicate writes (replays stored result on retry)");
1972}
1973
1974pub fn print_file_usage(prog: &str) {
1975    println!("File command usage:");
1976    println!(
1977        "  {} file upload <path> [--channel=ID] [--channels=IDs] [--title=TITLE] [--comment=TEXT] [--idempotency-key=KEY] [--profile=NAME] [--token-type=bot|user]",
1978        prog
1979    );
1980    println!("    Upload a file using external upload method");
1981    println!("    Requires SLACKCLI_ALLOW_WRITE=true environment variable");
1982    println!(
1983        "  {} file download [<file_id>] [--url=URL] [--out=PATH] [--profile=NAME] [--token-type=bot|user]",
1984        prog
1985    );
1986    println!("    Download a file from Slack");
1987    println!("    Either <file_id> or --url must be provided");
1988    println!("    --out: Output path (omit for current directory, '-' for stdout, directory for auto-naming)");
1989    println!("  Options accept both --option=value and --option value formats");
1990    println!("  --idempotency-key: Prevent duplicate writes (replays stored result on retry, upload only)");
1991}
1992
1993#[cfg(test)]
1994mod tests {
1995    use super::*;
1996
1997    #[test]
1998    fn test_parse_token_type_equals_format() {
1999        let args = vec!["command".to_string(), "--token-type=user".to_string()];
2000        let result = parse_token_type(&args).unwrap();
2001        assert_eq!(result, Some(TokenType::User));
2002    }
2003
2004    #[test]
2005    fn test_parse_token_type_space_separated() {
2006        let args = vec![
2007            "command".to_string(),
2008            "--token-type".to_string(),
2009            "bot".to_string(),
2010        ];
2011        let result = parse_token_type(&args).unwrap();
2012        assert_eq!(result, Some(TokenType::Bot));
2013    }
2014
2015    #[test]
2016    fn test_parse_token_type_both_values() {
2017        // Test user with equals
2018        let args1 = vec!["--token-type=user".to_string()];
2019        assert_eq!(parse_token_type(&args1).unwrap(), Some(TokenType::User));
2020
2021        // Test bot with equals
2022        let args2 = vec!["--token-type=bot".to_string()];
2023        assert_eq!(parse_token_type(&args2).unwrap(), Some(TokenType::Bot));
2024
2025        // Test user with space
2026        let args3 = vec!["--token-type".to_string(), "user".to_string()];
2027        assert_eq!(parse_token_type(&args3).unwrap(), Some(TokenType::User));
2028
2029        // Test bot with space
2030        let args4 = vec!["--token-type".to_string(), "bot".to_string()];
2031        assert_eq!(parse_token_type(&args4).unwrap(), Some(TokenType::Bot));
2032    }
2033
2034    #[test]
2035    fn test_parse_token_type_missing() {
2036        let args = vec!["command".to_string()];
2037        let result = parse_token_type(&args).unwrap();
2038        assert_eq!(result, None);
2039    }
2040
2041    #[test]
2042    fn test_parse_token_type_missing_value() {
2043        let args = vec!["--token-type".to_string()];
2044        let result = parse_token_type(&args);
2045        assert!(result.is_err());
2046        assert_eq!(
2047            result.unwrap_err(),
2048            "--token-type requires a value (bot or user)"
2049        );
2050    }
2051
2052    #[test]
2053    fn test_parse_token_type_invalid_value() {
2054        let args = vec!["--token-type=invalid".to_string()];
2055        let result = parse_token_type(&args);
2056        assert!(result.is_err());
2057    }
2058
2059    // Mock token store for testing
2060    struct MockTokenStore {
2061        tokens: std::collections::HashMap<String, String>,
2062    }
2063
2064    impl MockTokenStore {
2065        fn new() -> Self {
2066            Self {
2067                tokens: std::collections::HashMap::new(),
2068            }
2069        }
2070
2071        fn with_token(mut self, key: &str, value: &str) -> Self {
2072            self.tokens.insert(key.to_string(), value.to_string());
2073            self
2074        }
2075    }
2076
2077    impl TokenStore for MockTokenStore {
2078        fn get(&self, key: &str) -> crate::profile::token_store::Result<String> {
2079            use crate::profile::token_store::TokenStoreError;
2080            self.tokens
2081                .get(key)
2082                .cloned()
2083                .ok_or_else(|| TokenStoreError::NotFound(key.to_string()))
2084        }
2085
2086        fn set(&self, _key: &str, _value: &str) -> crate::profile::token_store::Result<()> {
2087            unimplemented!("set not needed for tests")
2088        }
2089
2090        fn delete(&self, _key: &str) -> crate::profile::token_store::Result<()> {
2091            unimplemented!("delete not needed for tests")
2092        }
2093
2094        fn exists(&self, key: &str) -> bool {
2095            self.tokens.contains_key(key)
2096        }
2097    }
2098
2099    #[test]
2100    fn test_resolve_token_prefers_env() {
2101        // SLACK_TOKEN should be preferred over token store
2102        let store = MockTokenStore::new().with_token("T123:U123", "xoxb-store-token");
2103
2104        let result = resolve_token_for_wrapper(
2105            Some("xoxb-env-token".to_string()),
2106            &store,
2107            "T123:U123",
2108            None,
2109            false,
2110        );
2111
2112        assert!(result.is_ok());
2113        assert_eq!(result.unwrap(), "xoxb-env-token");
2114    }
2115
2116    #[test]
2117    fn test_resolve_token_uses_store() {
2118        // When SLACK_TOKEN is not set, use token store
2119        let store = MockTokenStore::new().with_token("T123:U123", "xoxb-store-token");
2120
2121        let result = resolve_token_for_wrapper(None, &store, "T123:U123", None, false);
2122
2123        assert!(result.is_ok());
2124        assert_eq!(result.unwrap(), "xoxb-store-token");
2125    }
2126
2127    #[test]
2128    fn test_resolve_token_explicit_request() {
2129        // When token type is explicitly requested, don't fallback
2130        let store = MockTokenStore::new().with_token("T123:U123", "xoxb-bot-token");
2131
2132        let result = resolve_token_for_wrapper(
2133            None,
2134            &store,
2135            "T123:U123:user",  // User token key
2136            Some("T123:U123"), // Bot token fallback
2137            true,              // Explicit request
2138        );
2139
2140        assert!(result.is_err());
2141        assert!(result.unwrap_err().contains("explicitly requested"));
2142    }
2143
2144    #[test]
2145    fn test_resolve_token_fallback_when_not_explicit() {
2146        // When token type is not explicitly requested, allow fallback
2147        let store = MockTokenStore::new().with_token("T123:U123", "xoxb-bot-token");
2148
2149        let result = resolve_token_for_wrapper(
2150            None,
2151            &store,
2152            "T123:U123:user",  // User token key (not found)
2153            Some("T123:U123"), // Bot token fallback
2154            false,             // Not explicit request
2155        );
2156
2157        assert!(result.is_ok());
2158        assert_eq!(result.unwrap(), "xoxb-bot-token");
2159    }
2160
2161    #[test]
2162    fn test_resolve_token_env_overrides_explicit() {
2163        // SLACK_TOKEN should override even explicit token type requests
2164        let store = MockTokenStore::new()
2165            .with_token("T123:U123", "xoxb-bot-token")
2166            .with_token("T123:U123:user", "xoxp-user-token");
2167
2168        let result = resolve_token_for_wrapper(
2169            Some("xoxb-env-token".to_string()),
2170            &store,
2171            "T123:U123:user",
2172            None,
2173            true, // Explicit request
2174        );
2175
2176        assert!(result.is_ok());
2177        assert_eq!(result.unwrap(), "xoxb-env-token");
2178    }
2179
2180    // Tests for get_option with space-separated format
2181    #[test]
2182    fn test_get_option_equals_format() {
2183        let args = vec!["cmd".to_string(), "--filter=is_private:true".to_string()];
2184        assert_eq!(
2185            get_option(&args, "--filter="),
2186            Some("is_private:true".to_string())
2187        );
2188    }
2189
2190    #[test]
2191    fn test_get_option_space_separated() {
2192        let args = vec![
2193            "cmd".to_string(),
2194            "--filter".to_string(),
2195            "is_private:true".to_string(),
2196        ];
2197        assert_eq!(
2198            get_option(&args, "--filter="),
2199            Some("is_private:true".to_string())
2200        );
2201    }
2202
2203    #[test]
2204    fn test_get_option_space_separated_rejects_dash_value() {
2205        // Value starting with '-' should not be treated as value
2206        let args = vec![
2207            "cmd".to_string(),
2208            "--filter".to_string(),
2209            "--other".to_string(),
2210        ];
2211        assert_eq!(get_option(&args, "--filter="), None);
2212    }
2213
2214    #[test]
2215    fn test_get_option_space_separated_missing_value() {
2216        let args = vec!["cmd".to_string(), "--filter".to_string()];
2217        assert_eq!(get_option(&args, "--filter="), None);
2218    }
2219
2220    #[test]
2221    fn test_get_option_prefers_equals_format() {
2222        // When both formats exist, equals format should be returned first
2223        let args = vec![
2224            "--filter=value1".to_string(),
2225            "--filter".to_string(),
2226            "value2".to_string(),
2227        ];
2228        assert_eq!(get_option(&args, "--filter="), Some("value1".to_string()));
2229    }
2230
2231    // Tests for get_all_options with mixed formats
2232    #[test]
2233    fn test_get_all_options_equals_format() {
2234        let args = vec![
2235            "cmd".to_string(),
2236            "--filter=is_private:true".to_string(),
2237            "--filter=is_member:true".to_string(),
2238        ];
2239        let result = get_all_options(&args, "--filter=");
2240        assert_eq!(result, vec!["is_private:true", "is_member:true"]);
2241    }
2242
2243    #[test]
2244    fn test_get_all_options_space_separated() {
2245        let args = vec![
2246            "cmd".to_string(),
2247            "--filter".to_string(),
2248            "is_private:true".to_string(),
2249            "--filter".to_string(),
2250            "is_member:true".to_string(),
2251        ];
2252        let result = get_all_options(&args, "--filter=");
2253        assert_eq!(result, vec!["is_private:true", "is_member:true"]);
2254    }
2255
2256    #[test]
2257    fn test_get_all_options_mixed_format() {
2258        let args = vec![
2259            "cmd".to_string(),
2260            "--filter=is_private:true".to_string(),
2261            "--filter".to_string(),
2262            "is_member:true".to_string(),
2263            "--filter=name:test".to_string(),
2264            "--filter".to_string(),
2265            "is_archived:false".to_string(),
2266        ];
2267        let result = get_all_options(&args, "--filter=");
2268        assert_eq!(
2269            result,
2270            vec![
2271                "is_private:true",
2272                "name:test",
2273                "is_member:true",
2274                "is_archived:false"
2275            ]
2276        );
2277    }
2278
2279    #[test]
2280    fn test_get_all_options_rejects_dash_values() {
2281        let args = vec![
2282            "cmd".to_string(),
2283            "--filter=value1".to_string(),
2284            "--filter".to_string(),
2285            "--other".to_string(), // Should be ignored
2286            "--filter".to_string(),
2287            "value2".to_string(),
2288        ];
2289        let result = get_all_options(&args, "--filter=");
2290        assert_eq!(result, vec!["value1", "value2"]);
2291    }
2292
2293    #[test]
2294    fn test_get_all_options_space_separated_at_end() {
2295        // --filter at the end without value should be ignored
2296        let args = vec![
2297            "cmd".to_string(),
2298            "--filter=value1".to_string(),
2299            "--filter".to_string(),
2300        ];
2301        let result = get_all_options(&args, "--filter=");
2302        assert_eq!(result, vec!["value1"]);
2303    }
2304
2305    // Integration tests for conv commands with space-separated options
2306    #[test]
2307    fn test_conv_list_filter_space_separated() {
2308        // Test that filter parsing works with space-separated format
2309        let args = vec![
2310            "slack".to_string(),
2311            "conv".to_string(),
2312            "list".to_string(),
2313            "--filter".to_string(),
2314            "is_private:true".to_string(),
2315        ];
2316        let filters = get_all_options(&args, "--filter=");
2317        assert_eq!(filters.len(), 1);
2318        assert_eq!(filters[0], "is_private:true");
2319    }
2320
2321    #[test]
2322    fn test_conv_list_multiple_filters_mixed() {
2323        let args = vec![
2324            "slack".to_string(),
2325            "conv".to_string(),
2326            "list".to_string(),
2327            "--filter=is_private:true".to_string(),
2328            "--filter".to_string(),
2329            "is_member:true".to_string(),
2330        ];
2331        let filters = get_all_options(&args, "--filter=");
2332        assert_eq!(filters.len(), 2);
2333        assert_eq!(filters[0], "is_private:true");
2334        assert_eq!(filters[1], "is_member:true");
2335    }
2336
2337    #[test]
2338    fn test_conv_search_options_space_separated() {
2339        let args = vec![
2340            "slack".to_string(),
2341            "conv".to_string(),
2342            "search".to_string(),
2343            "pattern".to_string(),
2344            "--format".to_string(),
2345            "table".to_string(),
2346            "--sort".to_string(),
2347            "name".to_string(),
2348        ];
2349        assert_eq!(get_option(&args, "--format="), Some("table".to_string()));
2350        assert_eq!(get_option(&args, "--sort="), Some("name".to_string()));
2351    }
2352
2353    #[test]
2354    fn test_search_command_options_space_separated() {
2355        let args = vec![
2356            "slack".to_string(),
2357            "search".to_string(),
2358            "query".to_string(),
2359            "--count".to_string(),
2360            "10".to_string(),
2361            "--sort".to_string(),
2362            "timestamp".to_string(),
2363        ];
2364        assert_eq!(get_option(&args, "--count="), Some("10".to_string()));
2365        assert_eq!(get_option(&args, "--sort="), Some("timestamp".to_string()));
2366    }
2367
2368    // Tests for resolve_profile_name function
2369    #[test]
2370    fn test_resolve_profile_name_with_equals_format() {
2371        let args = vec![
2372            "slack".to_string(),
2373            "api".to_string(),
2374            "call".to_string(),
2375            "--profile=myprofile".to_string(),
2376            "test.method".to_string(),
2377        ];
2378        assert_eq!(resolve_profile_name(&args), "myprofile");
2379    }
2380
2381    #[test]
2382    fn test_resolve_profile_name_with_space_format() {
2383        let args = vec![
2384            "slack".to_string(),
2385            "api".to_string(),
2386            "call".to_string(),
2387            "--profile".to_string(),
2388            "myprofile".to_string(),
2389            "test.method".to_string(),
2390        ];
2391        assert_eq!(resolve_profile_name(&args), "myprofile");
2392    }
2393
2394    #[test]
2395    fn test_resolve_profile_name_at_beginning() {
2396        let args = vec![
2397            "slack".to_string(),
2398            "--profile=myprofile".to_string(),
2399            "api".to_string(),
2400            "call".to_string(),
2401            "test.method".to_string(),
2402        ];
2403        assert_eq!(resolve_profile_name(&args), "myprofile");
2404    }
2405
2406    #[test]
2407    fn test_resolve_profile_name_at_end() {
2408        let args = vec![
2409            "slack".to_string(),
2410            "api".to_string(),
2411            "call".to_string(),
2412            "test.method".to_string(),
2413            "--profile=myprofile".to_string(),
2414        ];
2415        assert_eq!(resolve_profile_name(&args), "myprofile");
2416    }
2417
2418    #[test]
2419    #[serial_test::serial]
2420    fn test_resolve_profile_name_env_fallback() {
2421        // Set environment variable
2422        std::env::set_var("SLACK_PROFILE", "envprofile");
2423
2424        let args = vec!["slack".to_string(), "api".to_string(), "call".to_string()];
2425        assert_eq!(resolve_profile_name(&args), "envprofile");
2426
2427        // Clean up
2428        std::env::remove_var("SLACK_PROFILE");
2429    }
2430
2431    #[test]
2432    #[serial_test::serial]
2433    fn test_resolve_profile_name_default_fallback() {
2434        // Ensure SLACK_PROFILE is not set
2435        std::env::remove_var("SLACK_PROFILE");
2436
2437        let args = vec!["slack".to_string(), "api".to_string(), "call".to_string()];
2438        assert_eq!(resolve_profile_name(&args), "default");
2439    }
2440
2441    #[test]
2442    #[serial_test::serial]
2443    fn test_resolve_profile_name_flag_overrides_env() {
2444        // Set environment variable
2445        std::env::set_var("SLACK_PROFILE", "envprofile");
2446
2447        let args = vec![
2448            "slack".to_string(),
2449            "api".to_string(),
2450            "--profile=flagprofile".to_string(),
2451            "call".to_string(),
2452        ];
2453        assert_eq!(resolve_profile_name(&args), "flagprofile");
2454
2455        // Clean up
2456        std::env::remove_var("SLACK_PROFILE");
2457    }
2458
2459    #[test]
2460    #[serial_test::serial]
2461    fn test_resolve_profile_name_priority_all_sources() {
2462        // Set environment variable
2463        std::env::set_var("SLACK_PROFILE", "envprofile");
2464
2465        // Test that --profile flag takes highest priority
2466        let args = vec![
2467            "--profile".to_string(),
2468            "flagprofile".to_string(),
2469            "slack".to_string(),
2470            "api".to_string(),
2471            "call".to_string(),
2472        ];
2473        assert_eq!(resolve_profile_name(&args), "flagprofile");
2474
2475        // Clean up
2476        std::env::remove_var("SLACK_PROFILE");
2477    }
2478
2479    #[test]
2480    fn test_resolve_profile_name_mixed_formats() {
2481        // Test that equals format is found even with space format present
2482        let args = vec![
2483            "slack".to_string(),
2484            "--profile=profile1".to_string(),
2485            "api".to_string(),
2486            "--profile".to_string(),
2487            "profile2".to_string(),
2488            "call".to_string(),
2489        ];
2490        // Should return profile1 as equals format is checked first
2491        assert_eq!(resolve_profile_name(&args), "profile1");
2492    }
2493
2494    #[test]
2495    fn test_conv_list_include_private_flag() {
2496        let args = vec![
2497            "slack".to_string(),
2498            "conv".to_string(),
2499            "list".to_string(),
2500            "--include-private".to_string(),
2501        ];
2502        assert!(has_flag(&args, "--include-private"));
2503        assert!(!has_flag(&args, "--all"));
2504    }
2505
2506    #[test]
2507    fn test_conv_list_all_flag() {
2508        let args = vec![
2509            "slack".to_string(),
2510            "conv".to_string(),
2511            "list".to_string(),
2512            "--all".to_string(),
2513        ];
2514        assert!(!has_flag(&args, "--include-private"));
2515        assert!(has_flag(&args, "--all"));
2516    }
2517
2518    #[test]
2519    fn test_conv_list_types_exclude_private_all() {
2520        // This test verifies the flag detection logic
2521        // The actual exclusivity check happens in run_conv_list
2522        let args_with_types = vec![
2523            "slack".to_string(),
2524            "conv".to_string(),
2525            "list".to_string(),
2526            "--types=public_channel".to_string(),
2527        ];
2528        assert_eq!(
2529            get_option(&args_with_types, "--types="),
2530            Some("public_channel".to_string())
2531        );
2532
2533        let args_with_private = vec![
2534            "slack".to_string(),
2535            "conv".to_string(),
2536            "list".to_string(),
2537            "--types=public_channel".to_string(),
2538            "--include-private".to_string(),
2539        ];
2540        assert_eq!(
2541            get_option(&args_with_private, "--types="),
2542            Some("public_channel".to_string())
2543        );
2544        assert!(has_flag(&args_with_private, "--include-private"));
2545    }
2546
2547    #[test]
2548    fn test_conv_list_types_resolution_logic() {
2549        // Test types resolution without flags
2550        let args_no_flags = vec!["slack".to_string(), "conv".to_string(), "list".to_string()];
2551        let types = get_option(&args_no_flags, "--types=");
2552        let include_private = has_flag(&args_no_flags, "--include-private");
2553        let all = has_flag(&args_no_flags, "--all");
2554        assert!(types.is_none());
2555        assert!(!include_private);
2556        assert!(!all);
2557
2558        // Test with --include-private
2559        let args_private = vec![
2560            "slack".to_string(),
2561            "conv".to_string(),
2562            "list".to_string(),
2563            "--include-private".to_string(),
2564        ];
2565        let types = get_option(&args_private, "--types=");
2566        let include_private = has_flag(&args_private, "--include-private");
2567        let all = has_flag(&args_private, "--all");
2568        assert!(types.is_none());
2569        assert!(include_private);
2570        assert!(!all);
2571
2572        // Test with --all
2573        let args_all = vec![
2574            "slack".to_string(),
2575            "conv".to_string(),
2576            "list".to_string(),
2577            "--all".to_string(),
2578        ];
2579        let types = get_option(&args_all, "--types=");
2580        let include_private = has_flag(&args_all, "--include-private");
2581        let all = has_flag(&args_all, "--all");
2582        assert!(types.is_none());
2583        assert!(!include_private);
2584        assert!(all);
2585
2586        // Test mutual exclusion: --types with --include-private
2587        let args_conflict1 = vec![
2588            "slack".to_string(),
2589            "conv".to_string(),
2590            "list".to_string(),
2591            "--types=public_channel".to_string(),
2592            "--include-private".to_string(),
2593        ];
2594        let types = get_option(&args_conflict1, "--types=");
2595        let include_private = has_flag(&args_conflict1, "--include-private");
2596        assert!(types.is_some());
2597        assert!(include_private);
2598        // This should trigger error in run_conv_list
2599
2600        // Test mutual exclusion: --types with --all
2601        let args_conflict2 = vec![
2602            "slack".to_string(),
2603            "conv".to_string(),
2604            "list".to_string(),
2605            "--types=public_channel".to_string(),
2606            "--all".to_string(),
2607        ];
2608        let types = get_option(&args_conflict2, "--types=");
2609        let all = has_flag(&args_conflict2, "--all");
2610        assert!(types.is_some());
2611        assert!(all);
2612        // This should trigger error in run_conv_list
2613    }
2614}