Skip to main content

context7_cli/
storage.rs

1/// XDG storage backend for API keys.
2///
3/// Implements the four-layer key loading hierarchy:
4/// 1. `CONTEXT7_API_KEYS` runtime environment variable (highest priority)
5/// 2. XDG config file `~/.config/context7/config.toml`
6/// 3. `.env` file in the current working directory
7/// 4. `CONTEXT7_API_KEYS` compile-time environment variable (lowest priority)
8use anyhow::{bail, Context, Result};
9use chrono::Utc;
10use directories::ProjectDirs;
11use serde::{Deserialize, Serialize};
12use std::path::PathBuf;
13
14use crate::errors::ErroContext7;
15use crate::i18n::{t, Mensagem};
16
17// ─── STRUCTS DE CONFIGURAÇÃO XDG ────────────────────────────────────────────
18
19/// Represents a stored API key entry in the XDG configuration file.
20///
21/// Field names use English (`value`, `added_at`) to mirror the external TOML format.
22#[derive(Debug, Serialize, Deserialize, Clone)]
23pub struct ChaveArmazenada {
24    /// The API key value.
25    pub value: String,
26    /// RFC 3339 timestamp when the key was added.
27    pub added_at: String,
28}
29
30/// Represents the structured TOML configuration file.
31///
32/// Field names use English (`schema_version`, `keys`) to mirror the external TOML format.
33#[derive(Debug, Serialize, Deserialize, Default)]
34pub struct ConfigArquivo {
35    /// Configuration schema version (currently 1).
36    pub schema_version: u32,
37    /// List of stored API keys.
38    #[serde(default)]
39    pub keys: Vec<ChaveArmazenada>,
40}
41
42// ─── FUNÇÕES DE PERMISSÕES DE ARQUIVO ───────────────────────────────────────
43
44/// Sets 600 permissions (owner read/write only) on Unix systems.
45///
46/// On non-Unix systems this is a no-op. Centralises the chmod 600 used by
47/// [`escrever_config_xdg`] and [`escrever_config_arquivo`].
48pub fn aplicar_permissoes_600(caminho: &std::path::Path) -> Result<()> {
49    #[cfg(unix)]
50    {
51        use std::os::unix::fs::PermissionsExt;
52        let mut perms = std::fs::metadata(caminho)
53            .with_context(|| format!("Falha ao ler metadados de: {}", caminho.display()))?
54            .permissions();
55        perms.set_mode(0o600);
56        std::fs::set_permissions(caminho, perms)
57            .with_context(|| format!("Falha ao definir permissões em: {}", caminho.display()))?;
58    }
59    #[cfg(not(unix))]
60    let _ = caminho;
61    Ok(())
62}
63
64// ─── FUNÇÕES DE DESCOBERTA DE CAMINHOS XDG ──────────────────────────────────
65
66/// Resolves a base path override from the `CONTEXT7_HOME` environment variable.
67///
68/// Returns `Some(PathBuf)` if `CONTEXT7_HOME` is set, non-empty, and contains no
69/// path-traversal components (`..`). Returns `None` otherwise, allowing callers
70/// to fall back to XDG/ProjectDirs defaults.
71fn resolver_home_override() -> Option<PathBuf> {
72    let home = std::env::var("CONTEXT7_HOME").ok()?;
73    if home.is_empty() {
74        return None;
75    }
76    let base = PathBuf::from(&home);
77    // Rejeitar path traversal para evitar escape do diretório de configuração
78    if base
79        .components()
80        .any(|c| c == std::path::Component::ParentDir)
81    {
82        return None;
83    }
84    Some(base)
85}
86
87/// Discovers the XDG configuration path for the `config.toml` file.
88///
89/// Checks `CONTEXT7_HOME` first: if set and valid, returns
90/// `{CONTEXT7_HOME}/context7/config.toml`. Falls back to
91/// `ProjectDirs::from("", "", "context7")` → `config_dir()`.
92/// Returns `None` if neither source provides a path.
93pub fn descobrir_caminho_config() -> Option<PathBuf> {
94    if let Some(base) = resolver_home_override() {
95        return Some(base.join("context7").join("config.toml"));
96    }
97    ProjectDirs::from("", "", "context7").map(|dirs| dirs.config_dir().join("config.toml"))
98}
99
100/// Discovers the XDG path for storing log files.
101///
102/// Checks `CONTEXT7_HOME` first: if set and valid, returns
103/// `{CONTEXT7_HOME}/context7/logs`. Falls back to `state_dir()` on Linux
104/// (XDG_STATE_HOME) with fallback to `data_local_dir()`.
105/// Returns `None` if neither source provides a path.
106pub fn descobrir_caminho_logs_xdg() -> Option<PathBuf> {
107    if let Some(base) = resolver_home_override() {
108        return Some(base.join("context7").join("logs"));
109    }
110    ProjectDirs::from("", "", "context7").map(|dirs| {
111        // state_dir() disponível apenas em Linux/XDG; fallback cross-platform
112        #[cfg(target_os = "linux")]
113        {
114            dirs.state_dir()
115                .unwrap_or_else(|| dirs.data_local_dir())
116                .to_path_buf()
117        }
118        #[cfg(not(target_os = "linux"))]
119        {
120            dirs.data_local_dir().to_path_buf()
121        }
122    })
123}
124
125// ─── FUNÇÕES DE CARREGAMENTO DE CHAVES (HIERARQUIA) ─────────────────────────
126
127/// Layer 1: reads keys from the `CONTEXT7_API_KEYS` runtime environment variable.
128///
129/// Accepts multiple comma-separated keys:
130/// `CONTEXT7_API_KEYS=ctx7sk-a,ctx7sk-b,ctx7sk-c`
131/// Whitespace around each key is trimmed automatically.
132/// Returns `None` if the variable is not set or is empty.
133pub fn ler_env_var_chave() -> Option<Vec<String>> {
134    std::env::var("CONTEXT7_API_KEYS")
135        .ok()
136        .map(|valor| {
137            valor
138                .split(',')
139                .map(|s| s.trim().to_string())
140                .filter(|s| !s.is_empty())
141                .collect::<Vec<_>>()
142        })
143        .filter(|v| !v.is_empty())
144}
145
146/// Layer 2: reads keys from the XDG configuration file (`config.toml`).
147///
148/// Returns `None` if the file does not exist or the XDG path is unavailable.
149/// Returns `Err` if the file exists but contains invalid TOML.
150pub fn ler_config_xdg() -> Result<Option<Vec<String>>> {
151    let caminho = match descobrir_caminho_config() {
152        Some(p) => p,
153        None => return Ok(None),
154    };
155
156    if !caminho.exists() {
157        return Ok(None);
158    }
159
160    let conteudo = std::fs::read_to_string(&caminho)
161        .with_context(|| format!("Falha ao ler configuração XDG em: {}", caminho.display()))?;
162
163    let config: ConfigArquivo = toml::from_str(&conteudo)
164        .with_context(|| format!("TOML inválido em: {}", caminho.display()))?;
165
166    let chaves: Vec<String> = config
167        .keys
168        .into_iter()
169        .map(|c| c.value)
170        .filter(|v| !v.is_empty())
171        .collect();
172
173    if chaves.is_empty() {
174        Ok(None)
175    } else {
176        Ok(Some(chaves))
177    }
178}
179
180/// Layer 3: reads keys from the `.env` file in the current working directory.
181///
182/// Re-uses [`extrair_chaves_env`] which is pure and testable.
183/// Returns `None` if the `.env` file does not exist or has no valid keys.
184pub fn ler_env_cwd() -> Option<Vec<String>> {
185    let caminho = std::env::current_dir().ok().map(|d| d.join(".env"))?;
186
187    if !caminho.exists() {
188        return None;
189    }
190
191    std::fs::read_to_string(&caminho)
192        .ok()
193        .and_then(|conteudo| extrair_chaves_env(&conteudo).ok())
194}
195
196/// Layer 4: reads keys embedded at compile time via `option_env!("CONTEXT7_API_KEYS")`.
197///
198/// Allows embedding keys in the binary at build time:
199/// `CONTEXT7_API_KEYS=ctx7sk-a cargo build --release`
200///
201/// **Security warning**: compile-time keys are visible to anyone who inspects
202/// the binary (e.g. `strings context7 | grep ctx7sk-`). Use only in controlled
203/// pipelines where access to the binary artefact is restricted.
204///
205/// Returns `None` if the variable was not defined at compile time.
206pub fn ler_env_compile_time() -> Option<Vec<String>> {
207    option_env!("CONTEXT7_API_KEYS").map(|valor| {
208        valor
209            .split(',')
210            .map(|s| s.trim().to_string())
211            .filter(|s| !s.is_empty())
212            .collect()
213    })
214}
215
216/// Loads API keys using the four-layer precedence hierarchy:
217///
218/// 1. `CONTEXT7_API_KEYS` runtime env var (highest priority)
219/// 2. XDG config `~/.config/context7/config.toml`
220/// 3. `.env` file in the current working directory
221/// 4. `CONTEXT7_API_KEYS` compile-time env var (lowest priority)
222///
223/// Returns an error only if NO layer provides valid keys.
224pub fn carregar_chaves_api() -> Result<Vec<String>> {
225    use tracing::{info, warn};
226
227    // Camada 1: env var runtime
228    if let Some(chaves) = ler_env_var_chave() {
229        info!("Chaves carregadas via variável de ambiente CONTEXT7_API_KEYS");
230        return Ok(chaves);
231    }
232
233    // Camada 2: config XDG
234    match ler_config_xdg() {
235        Ok(Some(chaves)) => {
236            info!("Chaves carregadas via configuração XDG");
237            return Ok(chaves);
238        }
239        Ok(None) => {}
240        Err(e) => {
241            warn!("Falha ao ler configuração XDG (continuando): {}", e);
242        }
243    }
244
245    // Camada 3: .env no CWD
246    if let Some(chaves) = ler_env_cwd() {
247        info!(
248            "Iniciando context7 com {} chaves de API disponíveis",
249            chaves.len()
250        );
251        return Ok(chaves);
252    }
253
254    // Camada 4: compile-time
255    if let Some(chaves) = ler_env_compile_time() {
256        info!("Chaves carregadas via compile-time CONTEXT7_API_KEYS");
257        return Ok(chaves);
258    }
259
260    bail!(t(Mensagem::NenhumaChaveConfigurada))
261}
262
263// ─── FUNÇÕES DE ESCRITA DE CONFIG ───────────────────────────────────────────
264
265/// Writes (or updates) the XDG configuration file with the provided key.
266///
267/// Creates parent directories if necessary.
268/// On Unix systems, sets 600 permissions via [`aplicar_permissoes_600`].
269pub fn escrever_config_xdg(nova_chave: &str) -> Result<PathBuf> {
270    let caminho = descobrir_caminho_config()
271        .context("Sistema não suporta diretórios XDG — impossível salvar configuração")?;
272
273    // Criar diretórios pai se não existirem
274    if let Some(pai) = caminho.parent() {
275        std::fs::create_dir_all(pai)
276            .with_context(|| format!("Falha ao criar diretório: {}", pai.display()))?;
277    }
278
279    // Ler config existente ou criar nova
280    let mut config = if caminho.exists() {
281        let conteudo = std::fs::read_to_string(&caminho)
282            .with_context(|| format!("Falha ao ler config existente: {}", caminho.display()))?;
283        toml::from_str::<ConfigArquivo>(&conteudo)
284            .with_context(|| format!("TOML inválido em: {}", caminho.display()))?
285    } else {
286        ConfigArquivo {
287            schema_version: 1,
288            keys: Vec::new(),
289        }
290    };
291
292    // Adicionar nova chave se ainda não existir
293    let ja_existe = config.keys.iter().any(|c| c.value == nova_chave);
294    if !ja_existe {
295        config.keys.push(ChaveArmazenada {
296            value: nova_chave.to_string(),
297            added_at: Utc::now().to_rfc3339(),
298        });
299    }
300
301    // Serializar e escrever
302    let toml_str =
303        toml::to_string_pretty(&config).context("Falha ao serializar configuração para TOML")?;
304    std::fs::write(&caminho, &toml_str)
305        .with_context(|| format!("Falha ao escrever config em: {}", caminho.display()))?;
306
307    aplicar_permissoes_600(&caminho)?;
308
309    Ok(caminho)
310}
311
312/// Reads the XDG configuration file and returns the full [`ConfigArquivo`].
313///
314/// Used by operations that need the complete structure (list, remove, export).
315/// Returns `Ok(None)` if the file does not exist or the XDG path is unavailable.
316pub fn ler_config_xdg_raw() -> Result<Option<ConfigArquivo>> {
317    let caminho = match descobrir_caminho_config() {
318        Some(p) => p,
319        None => return Ok(None),
320    };
321
322    if !caminho.exists() {
323        return Ok(None);
324    }
325
326    let conteudo = std::fs::read_to_string(&caminho)
327        .with_context(|| format!("Falha ao ler configuração XDG em: {}", caminho.display()))?;
328
329    let config: ConfigArquivo = toml::from_str(&conteudo)
330        .with_context(|| format!("TOML inválido em: {}", caminho.display()))?;
331
332    Ok(Some(config))
333}
334
335/// Writes a complete [`ConfigArquivo`] to the XDG configuration file.
336///
337/// Creates parent directories if necessary.
338/// On Unix systems, sets 600 permissions.
339pub fn escrever_config_arquivo(config: &ConfigArquivo) -> Result<PathBuf> {
340    let caminho = descobrir_caminho_config()
341        .context("Sistema não suporta diretórios XDG — impossível salvar configuração")?;
342
343    if let Some(pai) = caminho.parent() {
344        std::fs::create_dir_all(pai)
345            .with_context(|| format!("Falha ao criar diretório: {}", pai.display()))?;
346    }
347
348    let toml_str =
349        toml::to_string_pretty(config).context("Falha ao serializar configuração para TOML")?;
350    std::fs::write(&caminho, &toml_str)
351        .with_context(|| format!("Falha ao escrever config em: {}", caminho.display()))?;
352
353    aplicar_permissoes_600(&caminho)?;
354
355    Ok(caminho)
356}
357
358// ─── FUNÇÕES AUXILIARES ─────────────────────────────────────────────────────
359
360/// Masks an API key showing only the first 12 and last 4 characters.
361///
362/// Example: `ctx7sk-abc123...xyz9`
363///
364/// If the key is too short (≤ 16 Unicode characters), returns `***` for protection.
365/// Uses `chars()` for UTF-8 safety — avoids panics from byte-indexing multibyte characters.
366pub fn mascarar_chave(chave: &str) -> String {
367    let n_chars = chave.chars().count();
368    let inicio = 12;
369    let fim = 4;
370    if n_chars <= inicio + fim {
371        return "***".to_string();
372    }
373    let prefixo: String = chave.chars().take(inicio).collect();
374    let sufixo: String = chave
375        .chars()
376        .rev()
377        .take(fim)
378        .collect::<String>()
379        .chars()
380        .rev()
381        .collect();
382    format!("{}...{}", prefixo, sufixo)
383}
384
385/// Extracts `CONTEXT7_API=` keys from `.env` file content in memory.
386///
387/// Ignores comments (lines starting with `#`) and blank lines.
388/// Removes surrounding double and single quotes from values.
389/// Pure function — accepts `&str`, no I/O, facilitates unit testing.
390pub fn extrair_chaves_env(conteudo: &str) -> Result<Vec<String>> {
391    let chaves: Vec<String> = conteudo
392        .lines()
393        .filter_map(|linha| {
394            // Remove comentários inline (tudo após #)
395            let linha_sem_comentario = linha.split('#').next().unwrap_or("").trim();
396            linha_sem_comentario
397                .strip_prefix("CONTEXT7_API=")
398                .map(|valor| {
399                    // Remove aspas simples ou duplas ao redor do valor
400                    valor
401                        .trim()
402                        .trim_matches('"')
403                        .trim_matches('\'')
404                        .to_string()
405                })
406                .filter(|v| !v.is_empty())
407        })
408        .collect();
409
410    if chaves.is_empty() {
411        bail!(t(Mensagem::NenhumaChaveContext7NoArquivo));
412    }
413
414    Ok(chaves)
415}
416
417/// Asks for interactive confirmation before clearing all keys.
418///
419/// Returns `true` if the user confirms with `s` or `S`.
420pub fn confirmar_clear() -> Result<bool> {
421    use std::io::Write;
422    print!("Tem certeza que deseja remover TODAS as chaves? [s/N] ");
423    std::io::stdout()
424        .flush()
425        .context("Falha ao limpar buffer de saída")?;
426
427    let mut entrada = String::new();
428    std::io::stdin()
429        .read_line(&mut entrada)
430        .context("Falha ao ler confirmação do usuário")?;
431
432    Ok(matches!(
433        entrada.trim().to_lowercase().as_str(),
434        "s" | "sim" | "y" | "yes"
435    ))
436}
437
438// ─── OPERAÇÕES DO SUBCOMANDO KEYS ───────────────────────────────────────────
439
440/// Adds a new key to the XDG storage.
441///
442/// If the key already exists, prints a warning and returns without modifying the config.
443/// Re-uses [`escrever_config_xdg`] which implements deduplication and chmod 600.
444pub fn cmd_keys_add(chave: &str) -> Result<()> {
445    // Verificar duplicata antes de escrever — para exibir aviso claro ao usuário
446    if let Some(config) = ler_config_xdg_raw()? {
447        if config.keys.iter().any(|c| c.value == chave) {
448            crate::output::exibir_chave_ja_existia();
449            return Ok(());
450        }
451    }
452    let caminho = escrever_config_xdg(chave)?;
453    crate::output::exibir_chave_adicionada(&caminho);
454    Ok(())
455}
456
457/// Lists all stored keys with their 1-based indices and masked values.
458pub fn cmd_keys_list() -> Result<()> {
459    match ler_config_xdg_raw()? {
460        None => crate::output::exibir_nenhuma_chave(),
461        Some(config) if config.keys.is_empty() => crate::output::exibir_nenhuma_chave(),
462        Some(config) => crate::output::exibir_chaves_mascaradas(&config.keys, mascarar_chave),
463    }
464    Ok(())
465}
466
467/// Removes a key by its 1-based index.
468pub fn cmd_keys_remove(indice: usize) -> Result<()> {
469    let mut config = match ler_config_xdg_raw()? {
470        None => {
471            crate::output::exibir_nenhuma_chave_para_remover();
472            bail!(ErroContext7::OperacaoKeysFalhou);
473        }
474        Some(c) if c.keys.is_empty() => {
475            crate::output::exibir_nenhuma_chave_para_remover();
476            bail!(ErroContext7::OperacaoKeysFalhou);
477        }
478        Some(c) => c,
479    };
480
481    if indice == 0 || indice > config.keys.len() {
482        crate::output::exibir_indice_invalido(indice, config.keys.len());
483        bail!(ErroContext7::OperacaoKeysFalhou);
484    }
485
486    let removida = config.keys.remove(indice - 1);
487    escrever_config_arquivo(&config)?;
488    crate::output::exibir_chave_removida(&mascarar_chave(&removida.value));
489    Ok(())
490}
491
492/// Removes all stored keys. Asks for confirmation unless `--yes` is passed.
493pub fn cmd_keys_clear(sim: bool) -> Result<()> {
494    if !sim && !confirmar_clear()? {
495        crate::output::exibir_operacao_cancelada();
496        return Ok(());
497    }
498
499    let config = ConfigArquivo {
500        schema_version: 1,
501        keys: Vec::new(),
502    };
503    escrever_config_arquivo(&config)?;
504    crate::output::exibir_chaves_removidas();
505    Ok(())
506}
507
508/// Displays the path of the XDG configuration file.
509pub fn cmd_keys_path() -> Result<()> {
510    match descobrir_caminho_config() {
511        Some(caminho) => println!("{}", caminho.display()),
512        None => crate::output::exibir_xdg_nao_suportado(),
513    }
514    Ok(())
515}
516
517/// Imports keys from a `.env` file, reading `CONTEXT7_API=` entries.
518///
519/// Re-uses [`extrair_chaves_env`] and [`escrever_config_xdg`] for each key.
520pub fn cmd_keys_import(arquivo: &std::path::Path) -> Result<()> {
521    let conteudo = std::fs::read_to_string(arquivo)
522        .with_context(|| format!("Falha ao ler arquivo: {}", arquivo.display()))?;
523
524    let chaves = extrair_chaves_env(&conteudo).with_context(|| {
525        format!(
526            "Nenhuma chave CONTEXT7_API= encontrada em: {}",
527            arquivo.display()
528        )
529    })?;
530
531    let total = chaves.len();
532    let mut importadas = 0usize;
533
534    for chave in &chaves {
535        escrever_config_xdg(chave)?;
536        importadas += 1;
537    }
538
539    crate::output::exibir_importacao_concluida(importadas, total);
540    Ok(())
541}
542
543/// Exports all keys to stdout in `CONTEXT7_API=<value>` format, one per line.
544///
545/// Compatible with `.env` files — useful for scripts and pipes.
546pub fn cmd_keys_export() -> Result<()> {
547    match ler_config_xdg_raw()? {
548        None => {}
549        Some(config) if config.keys.is_empty() => {}
550        Some(config) => {
551            for chave in &config.keys {
552                println!("CONTEXT7_API={}", chave.value);
553            }
554        }
555    }
556    Ok(())
557}
558
559// ─── TESTES ─────────────────────────────────────────────────────────────────
560
561#[cfg(test)]
562mod testes {
563    use super::*;
564
565    // ── Função auxiliar de teste ──────────────────────────────────────────────
566
567    /// Lê o conteúdo de um arquivo TOML do caminho e retorna `ConfigArquivo`.
568    fn ler_config_toml_do_caminho(caminho: &std::path::Path) -> Result<ConfigArquivo> {
569        let conteudo = std::fs::read_to_string(caminho)
570            .with_context(|| format!("Falha ao ler: {}", caminho.display()))?;
571        toml::from_str(&conteudo)
572            .with_context(|| format!("TOML inválido em: {}", caminho.display()))
573    }
574
575    // ── Parsing do .env ───────────────────────────────────────────────────────
576
577    #[test]
578    fn testa_parsing_env_com_multiplas_chaves_iguais() {
579        let mut conteudo = String::new();
580        for i in 0..17 {
581            conteudo.push_str(&format!("CONTEXT7_API=ctx7sk-chave-{:02}\n", i));
582        }
583        let chaves = extrair_chaves_env(&conteudo).expect("Deve extrair 17 chaves sem erro");
584        assert_eq!(chaves.len(), 17, "Deve retornar exatamente 17 chaves");
585        for (i, chave) in chaves.iter().enumerate() {
586            assert_eq!(
587                chave,
588                &format!("ctx7sk-chave-{:02}", i),
589                "Chave {} deve ter o valor correto",
590                i
591            );
592        }
593    }
594
595    #[test]
596    fn testa_parsing_env_ignora_comentarios_e_linhas_vazias() {
597        let conteudo = "# Este é um comentário\n\
598                        CONTEXT7_API=ctx7sk-chave-valida-01\n\
599                        \n\
600                        # Outro comentário\n\
601                        CONTEXT7_API=ctx7sk-chave-valida-02\n\
602                        \n";
603        let chaves = extrair_chaves_env(conteudo).expect("Deve extrair chaves sem erro");
604        assert_eq!(chaves.len(), 2, "Deve ignorar comentários e linhas vazias");
605        assert_eq!(chaves[0], "ctx7sk-chave-valida-01");
606        assert_eq!(chaves[1], "ctx7sk-chave-valida-02");
607    }
608
609    #[test]
610    fn testa_parsing_env_remove_aspas_duplas() {
611        let conteudo = "CONTEXT7_API=\"ctx7sk-abc-com-aspas\"\n";
612        let chaves = extrair_chaves_env(conteudo).expect("Deve extrair chave sem erro");
613        assert_eq!(chaves.len(), 1);
614        assert_eq!(
615            chaves[0], "ctx7sk-abc-com-aspas",
616            "Deve remover aspas duplas"
617        );
618    }
619
620    #[test]
621    fn testa_parsing_env_remove_aspas_simples() {
622        let conteudo = "CONTEXT7_API='ctx7sk-abc-aspas-simples'\n";
623        let chaves = extrair_chaves_env(conteudo).expect("Deve extrair chave sem erro");
624        assert_eq!(chaves.len(), 1);
625        assert_eq!(
626            chaves[0], "ctx7sk-abc-aspas-simples",
627            "Deve remover aspas simples"
628        );
629    }
630
631    #[test]
632    fn testa_parsing_env_erro_quando_nenhuma_chave() {
633        let conteudo = "# Apenas comentários\n\
634                        OUTRA_VAR=valor\n\
635                        \n";
636        let resultado = extrair_chaves_env(conteudo);
637        assert!(
638            resultado.is_err(),
639            "Deve retornar Err quando não há chaves CONTEXT7_API"
640        );
641        let mensagem_erro = resultado.unwrap_err().to_string();
642        assert!(
643            mensagem_erro.contains("chave")
644                || mensagem_erro.contains("CONTEXT7_API")
645                || mensagem_erro.contains("key")
646                || mensagem_erro.contains("API"),
647            "Mensagem de erro deve mencionar CONTEXT7_API, chave, key ou API, obteve: {}",
648            mensagem_erro
649        );
650    }
651
652    #[test]
653    fn testa_parsing_env_ignora_chaves_vazias() {
654        let conteudo = "CONTEXT7_API=\n\
655                        CONTEXT7_API=ctx7sk-valida\n";
656        let chaves = extrair_chaves_env(conteudo).expect("Deve extrair chave sem erro");
657        assert_eq!(
658            chaves.len(),
659            1,
660            "Deve ignorar entradas CONTEXT7_API sem valor"
661        );
662        assert_eq!(chaves[0], "ctx7sk-valida");
663    }
664
665    #[test]
666    fn testa_parsing_env_ignora_comentario_inline() {
667        let conteudo = "CONTEXT7_API=ctx7sk-valida # comentário aqui\n";
668        let chaves = extrair_chaves_env(conteudo).expect("Deve extrair chave sem erro");
669        assert_eq!(chaves.len(), 1);
670        assert_eq!(chaves[0], "ctx7sk-valida");
671    }
672
673    // ── mascarar_chave ────────────────────────────────────────────────────────
674
675    #[test]
676    fn testa_mascarar_chave_com_valor_longo_exibe_prefixo_e_sufixo() {
677        let chave = "ctx7sk-abc123-def456-ghi789";
678        assert_eq!(chave.len(), 27, "Pré-condição: chave deve ter 27 chars");
679        let mascarada = mascarar_chave(chave);
680        assert!(
681            mascarada.starts_with("ctx7sk-abc12"),
682            "Deve iniciar com os primeiros 12 chars, obteve: {}",
683            mascarada
684        );
685        assert!(
686            mascarada.ends_with("i789"),
687            "Deve terminar com os últimos 4 chars, obteve: {}",
688            mascarada
689        );
690        assert!(
691            mascarada.contains("..."),
692            "Deve conter '...' entre prefixo e sufixo, obteve: {}",
693            mascarada
694        );
695    }
696
697    #[test]
698    fn testa_mascarar_chave_curta_retorna_asteriscos() {
699        let chave_exatamente_16 = "ctx7sk-abcdef012";
700        assert_eq!(
701            chave_exatamente_16.len(),
702            16,
703            "Pré-condição: chave deve ter 16 chars"
704        );
705        let mascarada = mascarar_chave(chave_exatamente_16);
706        assert_eq!(
707            mascarada, "***",
708            "Chave de 16 chars deve retornar '***', obteve: {}",
709            mascarada
710        );
711    }
712
713    #[test]
714    fn testa_mascarar_chave_vazia_retorna_asteriscos() {
715        let mascarada = mascarar_chave("");
716        assert_eq!(
717            mascarada, "***",
718            "Chave vazia deve retornar '***', obteve: {}",
719            mascarada
720        );
721    }
722
723    #[test]
724    fn testa_mascarar_chave_de_exatamente_17_chars_mascara_corretamente() {
725        let chave = "ctx7sk-abcdef0123"; // 17 chars
726        assert_eq!(chave.len(), 17, "Pré-condição: chave deve ter 17 chars");
727        let mascarada = mascarar_chave(chave);
728        assert!(
729            mascarada.contains("..."),
730            "Chave de 17 chars deve ser mascarada, obteve: {}",
731            mascarada
732        );
733        assert_eq!(
734            &mascarada[..12],
735            &chave[..12],
736            "Prefixo de 12 chars deve ser preservado"
737        );
738        assert!(
739            mascarada.ends_with(&chave[chave.len() - 4..]),
740            "Sufixo de 4 chars deve ser preservado"
741        );
742    }
743
744    // ── ler_env_var_chave ─────────────────────────────────────────────────────
745
746    #[test]
747    #[serial_test::serial]
748    fn testa_ler_env_var_chave_retorna_some_quando_setada() {
749        // SAFETY: testes serializados via #[serial_test::serial] garantem ausência de
750        // concorrência. Necessário para compatibilidade com Rust 2024 edition.
751        unsafe {
752            std::env::set_var("CONTEXT7_API_KEYS", "ctx7sk-chave-teste-01");
753        }
754        let resultado = ler_env_var_chave();
755        unsafe {
756            std::env::remove_var("CONTEXT7_API_KEYS");
757        }
758
759        let chaves = resultado.expect("Deve retornar Some com chave válida");
760        assert_eq!(chaves.len(), 1, "Deve retornar exatamente 1 chave");
761        assert_eq!(chaves[0], "ctx7sk-chave-teste-01");
762    }
763
764    #[test]
765    #[serial_test::serial]
766    fn testa_ler_env_var_chave_aceita_multiplas_separadas_por_virgula() {
767        // SAFETY: idem
768        unsafe {
769            std::env::set_var(
770                "CONTEXT7_API_KEYS",
771                "ctx7sk-chave-a, ctx7sk-chave-b , ctx7sk-chave-c",
772            );
773        }
774        let resultado = ler_env_var_chave();
775        unsafe {
776            std::env::remove_var("CONTEXT7_API_KEYS");
777        }
778
779        let chaves = resultado.expect("Deve retornar Some com múltiplas chaves");
780        assert_eq!(chaves.len(), 3, "Deve retornar 3 chaves");
781        assert_eq!(chaves[0], "ctx7sk-chave-a");
782        assert_eq!(chaves[1], "ctx7sk-chave-b");
783        assert_eq!(chaves[2], "ctx7sk-chave-c");
784    }
785
786    #[test]
787    #[serial_test::serial]
788    fn testa_ler_env_var_chave_retorna_none_quando_vazia() {
789        // SAFETY: idem
790        unsafe {
791            std::env::set_var("CONTEXT7_API_KEYS", "");
792        }
793        let resultado = ler_env_var_chave();
794        unsafe {
795            std::env::remove_var("CONTEXT7_API_KEYS");
796        }
797
798        assert!(
799            resultado.is_none(),
800            "Deve retornar None quando env var está vazia"
801        );
802    }
803
804    #[test]
805    #[serial_test::serial]
806    fn testa_ler_env_var_chave_retorna_none_quando_apenas_whitespace() {
807        // SAFETY: idem
808        unsafe {
809            std::env::set_var("CONTEXT7_API_KEYS", "   ,  ,  ");
810        }
811        let resultado = ler_env_var_chave();
812        unsafe {
813            std::env::remove_var("CONTEXT7_API_KEYS");
814        }
815
816        assert!(
817            resultado.is_none(),
818            "Deve retornar None quando env var contém apenas whitespace/vírgulas"
819        );
820    }
821
822    #[test]
823    #[serial_test::serial]
824    fn testa_ler_env_var_chave_retorna_none_quando_ausente() {
825        // SAFETY: idem
826        unsafe {
827            std::env::remove_var("CONTEXT7_API_KEYS");
828        }
829        let resultado = ler_env_var_chave();
830
831        assert!(
832            resultado.is_none(),
833            "Deve retornar None quando env var não existe"
834        );
835    }
836
837    // ── path traversal via CONTEXT7_HOME ──────────────────────────────────
838
839    #[test]
840    #[serial_test::serial]
841    fn testa_context7_home_rejeita_path_traversal() {
842        let casos = ["../../../etc", "..", "/tmp/../etc"];
843        for caso in &casos {
844            // SAFETY: manipulação de env var em contexto serial de teste.
845            unsafe {
846                std::env::set_var("CONTEXT7_HOME", caso);
847            }
848            let resultado = descobrir_caminho_config();
849            unsafe {
850                std::env::remove_var("CONTEXT7_HOME");
851            }
852
853            // Deve cair no fallback XDG — o resultado NÃO deve conter ".."
854            if let Some(caminho) = resultado {
855                let s = caminho.to_string_lossy();
856                assert!(
857                    !s.contains(".."),
858                    "Path traversal '{caso}' não deve resultar em caminho com '..': {s}"
859                );
860            }
861            // None também é aceitável (ProjectDirs ausente no ambiente de CI)
862        }
863    }
864
865    // ── ler_config_xdg via CONTEXT7_HOME ───────────────────────────────────
866
867    #[test]
868    #[serial_test::serial]
869    fn testa_ler_config_xdg_arquivo_inexistente_retorna_none() {
870        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
871        // SAFETY: idem
872        unsafe {
873            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
874        }
875        let resultado = ler_config_xdg();
876        unsafe {
877            std::env::remove_var("CONTEXT7_HOME");
878        }
879
880        let valor = resultado.expect("Deve retornar Ok quando arquivo não existe");
881        assert!(
882            valor.is_none(),
883            "Deve retornar None quando config.toml não existe"
884        );
885    }
886
887    #[test]
888    #[serial_test::serial]
889    fn testa_ler_config_xdg_le_toml_valido_com_multiplas_chaves() {
890        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
891        let dir_context7 = dir_temp.path().join("context7");
892        std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
893
894        let toml_conteudo = r#"schema_version = 1
895
896[[keys]]
897value = "ctx7sk-chave-xdg-01"
898added_at = "2026-01-01T00:00:00+00:00"
899
900[[keys]]
901value = "ctx7sk-chave-xdg-02"
902added_at = "2026-01-02T00:00:00+00:00"
903"#;
904        std::fs::write(dir_context7.join("config.toml"), toml_conteudo)
905            .expect("Deve escrever config.toml");
906
907        // SAFETY: idem
908        unsafe {
909            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
910        }
911        let resultado = ler_config_xdg();
912        unsafe {
913            std::env::remove_var("CONTEXT7_HOME");
914        }
915
916        let chaves = resultado
917            .expect("Deve retornar Ok")
918            .expect("Deve retornar Some com chaves");
919        assert_eq!(chaves.len(), 2, "Deve retornar 2 chaves");
920        assert_eq!(chaves[0], "ctx7sk-chave-xdg-01");
921        assert_eq!(chaves[1], "ctx7sk-chave-xdg-02");
922    }
923
924    #[test]
925    #[serial_test::serial]
926    fn testa_ler_config_xdg_retorna_err_quando_toml_invalido() {
927        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
928        let dir_context7 = dir_temp.path().join("context7");
929        std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
930
931        std::fs::write(
932            dir_context7.join("config.toml"),
933            "schema_version = INVALIDO\n[[[malformado",
934        )
935        .expect("Deve escrever TOML inválido");
936
937        // SAFETY: idem
938        unsafe {
939            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
940        }
941        let resultado = ler_config_xdg();
942        unsafe {
943            std::env::remove_var("CONTEXT7_HOME");
944        }
945
946        assert!(
947            resultado.is_err(),
948            "Deve retornar Err quando TOML está malformado"
949        );
950    }
951
952    #[test]
953    #[serial_test::serial]
954    fn testa_ler_config_xdg_preserva_ordem_das_chaves() {
955        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
956        let dir_context7 = dir_temp.path().join("context7");
957        std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
958
959        let toml_conteudo = r#"schema_version = 1
960
961[[keys]]
962value = "ctx7sk-primeira"
963added_at = "2026-01-01T00:00:00+00:00"
964
965[[keys]]
966value = "ctx7sk-segunda"
967added_at = "2026-01-02T00:00:00+00:00"
968
969[[keys]]
970value = "ctx7sk-terceira"
971added_at = "2026-01-03T00:00:00+00:00"
972"#;
973        std::fs::write(dir_context7.join("config.toml"), toml_conteudo)
974            .expect("Deve escrever config.toml");
975
976        // SAFETY: idem
977        unsafe {
978            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
979        }
980        let resultado = ler_config_xdg();
981        unsafe {
982            std::env::remove_var("CONTEXT7_HOME");
983        }
984
985        let chaves = resultado
986            .expect("Deve retornar Ok")
987            .expect("Deve retornar Some");
988        assert_eq!(chaves[0], "ctx7sk-primeira");
989        assert_eq!(chaves[1], "ctx7sk-segunda");
990        assert_eq!(chaves[2], "ctx7sk-terceira");
991    }
992
993    #[test]
994    #[serial_test::serial]
995    fn testa_ler_config_xdg_keys_vazio_retorna_none() {
996        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
997        let dir_context7 = dir_temp.path().join("context7");
998        std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
999
1000        let toml_sem_chaves = "schema_version = 1\n";
1001        std::fs::write(dir_context7.join("config.toml"), toml_sem_chaves)
1002            .expect("Deve escrever config.toml sem keys");
1003
1004        // SAFETY: idem
1005        unsafe {
1006            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1007        }
1008        let resultado = ler_config_xdg();
1009        unsafe {
1010            std::env::remove_var("CONTEXT7_HOME");
1011        }
1012
1013        let valor = resultado.expect("Deve retornar Ok");
1014        assert!(
1015            valor.is_none(),
1016            "Deve retornar None quando config.toml existe mas keys está vazio"
1017        );
1018    }
1019
1020    // ── escrever_config_xdg ───────────────────────────────────────────────────
1021
1022    #[test]
1023    #[serial_test::serial]
1024    fn testa_escrever_config_xdg_roundtrip_serializa_e_deserializa() {
1025        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1026        // SAFETY: idem
1027        unsafe {
1028            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1029        }
1030
1031        let caminho =
1032            escrever_config_xdg("ctx7sk-roundtrip-01").expect("Deve escrever config sem erro");
1033
1034        let config_lido = ler_config_toml_do_caminho(&caminho)
1035            .expect("Deve ler TOML escrito por escrever_config_xdg");
1036
1037        unsafe {
1038            std::env::remove_var("CONTEXT7_HOME");
1039        }
1040
1041        assert_eq!(config_lido.schema_version, 1, "schema_version deve ser 1");
1042        assert_eq!(config_lido.keys.len(), 1, "Deve conter 1 chave");
1043        assert_eq!(
1044            config_lido.keys[0].value, "ctx7sk-roundtrip-01",
1045            "Valor da chave deve ser preservado"
1046        );
1047        assert!(
1048            !config_lido.keys[0].added_at.is_empty(),
1049            "added_at não deve ser vazio"
1050        );
1051    }
1052
1053    #[test]
1054    #[serial_test::serial]
1055    fn testa_escrever_config_xdg_cria_diretorios_pai_se_nao_existirem() {
1056        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1057        let xdg_novo = dir_temp.path().join("xdg_inexistente");
1058        // SAFETY: idem
1059        unsafe {
1060            std::env::set_var("CONTEXT7_HOME", &xdg_novo);
1061        }
1062
1063        let resultado = escrever_config_xdg("ctx7sk-mkdir-teste");
1064        unsafe {
1065            std::env::remove_var("CONTEXT7_HOME");
1066        }
1067
1068        let caminho = resultado.expect("Deve criar diretório pai e escrever config");
1069        assert!(
1070            caminho.exists(),
1071            "Arquivo de config deve existir após escrita"
1072        );
1073    }
1074
1075    #[test]
1076    #[serial_test::serial]
1077    fn testa_escrever_config_xdg_nao_duplica_chave_ja_existente() {
1078        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1079        // SAFETY: idem
1080        unsafe {
1081            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1082        }
1083
1084        escrever_config_xdg("ctx7sk-unica").expect("Primeira escrita deve funcionar");
1085        escrever_config_xdg("ctx7sk-unica").expect("Segunda escrita não deve falhar");
1086
1087        let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1088        let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1089
1090        unsafe {
1091            std::env::remove_var("CONTEXT7_HOME");
1092        }
1093
1094        assert_eq!(
1095            config.keys.len(),
1096            1,
1097            "Não deve duplicar chave já existente — deve ter apenas 1"
1098        );
1099    }
1100
1101    #[test]
1102    #[serial_test::serial]
1103    fn testa_escrever_config_xdg_acumula_chaves_distintas() {
1104        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1105        // SAFETY: idem
1106        unsafe {
1107            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1108        }
1109
1110        escrever_config_xdg("ctx7sk-chave-a").expect("Primeira escrita deve funcionar");
1111        escrever_config_xdg("ctx7sk-chave-b").expect("Segunda escrita deve funcionar");
1112        escrever_config_xdg("ctx7sk-chave-c").expect("Terceira escrita deve funcionar");
1113
1114        let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1115        let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1116
1117        unsafe {
1118            std::env::remove_var("CONTEXT7_HOME");
1119        }
1120
1121        assert_eq!(config.keys.len(), 3, "Deve acumular 3 chaves distintas");
1122        let valores: Vec<&str> = config.keys.iter().map(|c| c.value.as_str()).collect();
1123        assert!(valores.contains(&"ctx7sk-chave-a"));
1124        assert!(valores.contains(&"ctx7sk-chave-b"));
1125        assert!(valores.contains(&"ctx7sk-chave-c"));
1126    }
1127
1128    #[test]
1129    #[cfg(unix)]
1130    #[serial_test::serial]
1131    fn testa_escrever_config_xdg_aplica_permissoes_600_em_unix() {
1132        use std::os::unix::fs::PermissionsExt;
1133
1134        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1135        // SAFETY: idem
1136        unsafe {
1137            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1138        }
1139
1140        let caminho =
1141            escrever_config_xdg("ctx7sk-perm-600").expect("Deve escrever config sem erro");
1142        unsafe {
1143            std::env::remove_var("CONTEXT7_HOME");
1144        }
1145
1146        let metadados = std::fs::metadata(&caminho).expect("Deve obter metadados do arquivo");
1147        let modo = metadados.permissions().mode() & 0o777;
1148
1149        assert_eq!(modo, 0o600, "Permissões devem ser 600, obteve: {:o}", modo);
1150    }
1151
1152    // ── Serde TOML roundtrip ──────────────────────────────────────────────────
1153
1154    #[test]
1155    fn testa_config_arquivo_roundtrip_serde_preserva_todos_campos() {
1156        let config_original = ConfigArquivo {
1157            schema_version: 1,
1158            keys: vec![
1159                ChaveArmazenada {
1160                    value: "ctx7sk-serde-01".to_string(),
1161                    added_at: "2026-01-01T12:00:00+00:00".to_string(),
1162                },
1163                ChaveArmazenada {
1164                    value: "ctx7sk-serde-02".to_string(),
1165                    added_at: "2026-01-02T12:00:00+00:00".to_string(),
1166                },
1167            ],
1168        };
1169
1170        let toml_str = toml::to_string_pretty(&config_original)
1171            .expect("Deve serializar ConfigArquivo para TOML");
1172        let config_deserializado: ConfigArquivo =
1173            toml::from_str(&toml_str).expect("Deve deserializar TOML de volta para ConfigArquivo");
1174
1175        assert_eq!(
1176            config_deserializado.schema_version, config_original.schema_version,
1177            "schema_version deve ser preservado no roundtrip"
1178        );
1179        assert_eq!(
1180            config_deserializado.keys.len(),
1181            config_original.keys.len(),
1182            "Número de chaves deve ser preservado"
1183        );
1184        assert_eq!(
1185            config_deserializado.keys[0].value, config_original.keys[0].value,
1186            "Valor da primeira chave deve ser preservado"
1187        );
1188        assert_eq!(
1189            config_deserializado.keys[0].added_at, config_original.keys[0].added_at,
1190            "added_at da primeira chave deve ser preservado"
1191        );
1192    }
1193
1194    #[test]
1195    fn testa_config_arquivo_schema_version_sempre_presente_na_serializacao() {
1196        let config = ConfigArquivo {
1197            schema_version: 1,
1198            keys: Vec::new(),
1199        };
1200
1201        let toml_str = toml::to_string_pretty(&config).expect("Deve serializar para TOML");
1202
1203        assert!(
1204            toml_str.contains("schema_version"),
1205            "schema_version deve estar presente na serialização TOML"
1206        );
1207        assert!(toml_str.contains('1'), "Valor 1 deve estar presente");
1208    }
1209
1210    #[test]
1211    fn testa_config_arquivo_keys_vazio_aceito_na_deserializacao() {
1212        let toml_str = "schema_version = 1\n";
1213        let config: ConfigArquivo =
1214            toml::from_str(toml_str).expect("Deve deserializar com keys ausente (default vazio)");
1215
1216        assert_eq!(config.schema_version, 1);
1217        assert!(
1218            config.keys.is_empty(),
1219            "keys deve ser vazio quando não presente no TOML"
1220        );
1221    }
1222
1223    #[test]
1224    fn testa_chave_armazenada_preserva_added_at_como_string_utc() {
1225        let timestamp = "2026-04-08T20:00:00+00:00";
1226        let chave = ChaveArmazenada {
1227            value: "ctx7sk-timestamp".to_string(),
1228            added_at: timestamp.to_string(),
1229        };
1230
1231        let toml_str = toml::to_string_pretty(&chave).expect("Deve serializar ChaveArmazenada");
1232        let chave_de_volta: ChaveArmazenada =
1233            toml::from_str(&toml_str).expect("Deve deserializar ChaveArmazenada");
1234
1235        assert_eq!(
1236            chave_de_volta.added_at, timestamp,
1237            "Timestamp added_at deve ser preservado exatamente"
1238        );
1239    }
1240
1241    // ── carregar_chaves_api (precedência) ─────────────────────────────────────
1242
1243    #[test]
1244    #[serial_test::serial]
1245    fn testa_carregar_chaves_api_env_var_tem_prioridade_sobre_xdg() {
1246        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1247        let dir_context7 = dir_temp.path().join("context7");
1248        std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
1249
1250        let toml_xdg = r#"schema_version = 1
1251[[keys]]
1252value = "ctx7sk-xdg-deve-ser-ignorada"
1253added_at = "2026-01-01T00:00:00+00:00"
1254"#;
1255        std::fs::write(dir_context7.join("config.toml"), toml_xdg)
1256            .expect("Deve escrever config XDG");
1257
1258        // SAFETY: idem
1259        unsafe {
1260            std::env::set_var("CONTEXT7_API_KEYS", "ctx7sk-env-var-prioritaria");
1261            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1262        }
1263
1264        let resultado = carregar_chaves_api();
1265
1266        unsafe {
1267            std::env::remove_var("CONTEXT7_API_KEYS");
1268            std::env::remove_var("CONTEXT7_HOME");
1269        }
1270
1271        let chaves = resultado.expect("Deve carregar chaves via env var");
1272        assert_eq!(chaves.len(), 1);
1273        assert_eq!(
1274            chaves[0], "ctx7sk-env-var-prioritaria",
1275            "Env var deve ter prioridade sobre XDG"
1276        );
1277    }
1278
1279    #[test]
1280    #[serial_test::serial]
1281    fn testa_carregar_chaves_api_xdg_usado_quando_env_var_ausente() {
1282        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1283        let dir_context7 = dir_temp.path().join("context7");
1284        std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
1285
1286        let toml_xdg = r#"schema_version = 1
1287[[keys]]
1288value = "ctx7sk-via-xdg"
1289added_at = "2026-01-01T00:00:00+00:00"
1290"#;
1291        std::fs::write(dir_context7.join("config.toml"), toml_xdg)
1292            .expect("Deve escrever config XDG");
1293
1294        // SAFETY: idem
1295        unsafe {
1296            std::env::remove_var("CONTEXT7_API_KEYS");
1297            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1298        }
1299
1300        let resultado = carregar_chaves_api();
1301
1302        unsafe {
1303            std::env::remove_var("CONTEXT7_HOME");
1304        }
1305
1306        let chaves = resultado.expect("Deve carregar chaves via XDG");
1307        assert_eq!(chaves.len(), 1);
1308        assert_eq!(chaves[0], "ctx7sk-via-xdg");
1309    }
1310
1311    #[test]
1312    #[serial_test::serial]
1313    fn testa_carregar_chaves_api_retorna_err_quando_nada_disponivel() {
1314        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1315        let dir_xdg_vazio = dir_temp.path().join("xdg_vazio");
1316        std::fs::create_dir_all(&dir_xdg_vazio).expect("Deve criar diretório XDG vazio");
1317
1318        let dir_sem_env = dir_temp.path().join("sem_env");
1319        std::fs::create_dir_all(&dir_sem_env).expect("Deve criar diretório sem .env");
1320
1321        // SAFETY: idem
1322        unsafe {
1323            std::env::remove_var("CONTEXT7_API_KEYS");
1324            std::env::set_var("CONTEXT7_HOME", &dir_xdg_vazio);
1325        }
1326        let cwd_original = std::env::current_dir().expect("Deve obter CWD atual");
1327        std::env::set_current_dir(&dir_sem_env).expect("Deve mudar CWD");
1328
1329        let resultado = carregar_chaves_api();
1330
1331        std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1332        unsafe {
1333            std::env::remove_var("CONTEXT7_HOME");
1334        }
1335
1336        assert!(
1337            resultado.is_err(),
1338            "Deve retornar Err quando nenhuma camada fornecer chaves"
1339        );
1340    }
1341
1342    #[test]
1343    #[serial_test::serial]
1344    fn testa_ler_env_cwd_le_env_com_multiplas_chaves_context7_api() {
1345        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1346        let conteudo_env = "CONTEXT7_API=ctx7sk-cwd-01\nCONTEXT7_API=ctx7sk-cwd-02\n";
1347        std::fs::write(dir_temp.path().join(".env"), conteudo_env)
1348            .expect("Deve escrever .env temporário");
1349
1350        let cwd_original = std::env::current_dir().expect("Deve obter CWD");
1351        std::env::set_current_dir(dir_temp.path()).expect("Deve mudar CWD para temp");
1352
1353        let resultado = ler_env_cwd();
1354
1355        std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1356
1357        let chaves = resultado.expect("Deve retornar Some com chaves do .env CWD");
1358        assert_eq!(chaves.len(), 2, "Deve ler 2 chaves do .env");
1359        assert_eq!(chaves[0], "ctx7sk-cwd-01");
1360        assert_eq!(chaves[1], "ctx7sk-cwd-02");
1361    }
1362
1363    #[test]
1364    #[serial_test::serial]
1365    fn testa_ler_env_cwd_retorna_none_quando_env_ausente() {
1366        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1367
1368        let cwd_original = std::env::current_dir().expect("Deve obter CWD");
1369        std::env::set_current_dir(dir_temp.path()).expect("Deve mudar CWD para temp sem .env");
1370
1371        let resultado = ler_env_cwd();
1372
1373        std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1374
1375        assert!(
1376            resultado.is_none(),
1377            "Deve retornar None quando não há .env no CWD"
1378        );
1379    }
1380
1381    #[test]
1382    fn testa_descobrir_caminho_logs_xdg_retorna_algum_caminho_valido() {
1383        let resultado = descobrir_caminho_logs_xdg();
1384
1385        if let Some(caminho) = resultado {
1386            let caminho_str = caminho.to_string_lossy();
1387            assert!(
1388                caminho_str.contains("context7"),
1389                "Caminho de logs XDG deve conter 'context7', obteve: {}",
1390                caminho_str
1391            );
1392        }
1393    }
1394
1395    #[test]
1396    #[serial_test::serial]
1397    fn testa_carregar_chaves_api_env_cwd_usado_quando_env_var_e_xdg_ausentes() {
1398        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1399        let dir_xdg_sem_config = dir_temp.path().join("xdg_sem_config");
1400        std::fs::create_dir_all(&dir_xdg_sem_config).expect("Deve criar diretório XDG vazio");
1401
1402        let dir_cwd = dir_temp.path().join("cwd_com_env");
1403        std::fs::create_dir_all(&dir_cwd).expect("Deve criar CWD temporário");
1404        std::fs::write(dir_cwd.join(".env"), "CONTEXT7_API=ctx7sk-cwd-camada-3\n")
1405            .expect("Deve escrever .env no CWD");
1406
1407        // SAFETY: idem
1408        unsafe {
1409            std::env::remove_var("CONTEXT7_API_KEYS");
1410            std::env::set_var("CONTEXT7_HOME", &dir_xdg_sem_config);
1411        }
1412        let cwd_original = std::env::current_dir().expect("Deve obter CWD");
1413        std::env::set_current_dir(&dir_cwd).expect("Deve mudar CWD");
1414
1415        let resultado = carregar_chaves_api();
1416
1417        std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1418        unsafe {
1419            std::env::remove_var("CONTEXT7_HOME");
1420        }
1421
1422        let chaves = resultado.expect("Deve carregar chaves via .env CWD");
1423        assert_eq!(chaves.len(), 1);
1424        assert_eq!(chaves[0], "ctx7sk-cwd-camada-3");
1425    }
1426
1427    #[test]
1428    #[serial_test::serial]
1429    fn testa_carregar_chaves_api_faz_fallback_quando_xdg_invalido() {
1430        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1431        let dir_context7 = dir_temp.path().join("context7");
1432        std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
1433
1434        std::fs::write(dir_context7.join("config.toml"), "[[[invalido")
1435            .expect("Deve escrever TOML inválido");
1436
1437        let dir_cwd = dir_temp.path().join("cwd_fallback");
1438        std::fs::create_dir_all(&dir_cwd).expect("Deve criar CWD com .env");
1439        std::fs::write(dir_cwd.join(".env"), "CONTEXT7_API=ctx7sk-fallback-cwd\n")
1440            .expect("Deve escrever .env no CWD");
1441
1442        // SAFETY: idem
1443        unsafe {
1444            std::env::remove_var("CONTEXT7_API_KEYS");
1445            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1446        }
1447        let cwd_original = std::env::current_dir().expect("Deve obter CWD");
1448        std::env::set_current_dir(&dir_cwd).expect("Deve mudar CWD");
1449
1450        let resultado = carregar_chaves_api();
1451
1452        std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1453        unsafe {
1454            std::env::remove_var("CONTEXT7_HOME");
1455        }
1456
1457        let chaves = resultado.expect("Deve carregar chaves via fallback .env CWD");
1458        assert_eq!(chaves.len(), 1);
1459        assert_eq!(chaves[0], "ctx7sk-fallback-cwd");
1460    }
1461
1462    // ── cmd_keys_add ─────────────────────────────────────────────────────────
1463
1464    #[test]
1465    #[serial_test::serial]
1466    fn testa_cmd_keys_add_cria_config_quando_nao_existe() {
1467        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1468        // SAFETY: idem
1469        unsafe {
1470            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1471        }
1472
1473        let resultado = cmd_keys_add("ctx7sk-nova-chave-add-test");
1474
1475        unsafe {
1476            std::env::remove_var("CONTEXT7_HOME");
1477        }
1478
1479        resultado.expect("cmd_keys_add deve funcionar em config vazio");
1480
1481        let caminho = dir_temp.path().join("context7").join("config.toml");
1482        assert!(
1483            caminho.exists(),
1484            "config.toml deve existir após cmd_keys_add"
1485        );
1486
1487        let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config criado");
1488        assert_eq!(config.keys.len(), 1, "Config deve ter 1 chave");
1489        assert_eq!(config.keys[0].value, "ctx7sk-nova-chave-add-test");
1490    }
1491
1492    #[test]
1493    #[serial_test::serial]
1494    fn testa_cmd_keys_add_acumula_em_config_existente() {
1495        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1496        // SAFETY: idem
1497        unsafe {
1498            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1499        }
1500
1501        cmd_keys_add("ctx7sk-chave-um").expect("Primeira adição deve funcionar");
1502        cmd_keys_add("ctx7sk-chave-dois").expect("Segunda adição deve funcionar");
1503
1504        let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1505        let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1506
1507        unsafe {
1508            std::env::remove_var("CONTEXT7_HOME");
1509        }
1510
1511        assert_eq!(config.keys.len(), 2, "Deve acumular 2 chaves");
1512        assert_eq!(config.keys[0].value, "ctx7sk-chave-um");
1513        assert_eq!(config.keys[1].value, "ctx7sk-chave-dois");
1514    }
1515
1516    #[test]
1517    #[serial_test::serial]
1518    fn testa_cmd_keys_add_nao_duplica_chave_existente() {
1519        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1520        // SAFETY: idem
1521        unsafe {
1522            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1523        }
1524
1525        cmd_keys_add("ctx7sk-unica-dedup").expect("Primeira adição deve funcionar");
1526        cmd_keys_add("ctx7sk-unica-dedup").expect("Segunda adição da mesma chave não deve falhar");
1527
1528        let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1529        let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1530
1531        unsafe {
1532            std::env::remove_var("CONTEXT7_HOME");
1533        }
1534
1535        assert_eq!(config.keys.len(), 1, "Não deve duplicar chave já existente");
1536    }
1537
1538    #[test]
1539    #[cfg(unix)]
1540    #[serial_test::serial]
1541    fn testa_cmd_keys_add_aplica_permissoes_600_em_unix() {
1542        use std::os::unix::fs::PermissionsExt;
1543
1544        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1545        // SAFETY: idem
1546        unsafe {
1547            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1548        }
1549
1550        cmd_keys_add("ctx7sk-perm-600-keys-add").expect("Deve adicionar chave sem erro");
1551
1552        let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1553        unsafe {
1554            std::env::remove_var("CONTEXT7_HOME");
1555        }
1556
1557        let metadados = std::fs::metadata(&caminho).expect("Deve obter metadados");
1558        let modo = metadados.permissions().mode() & 0o777;
1559        assert_eq!(
1560            modo, 0o600,
1561            "Permissões devem ser 600 após cmd_keys_add, obteve: {:o}",
1562            modo
1563        );
1564    }
1565
1566    // ── cmd_keys_remove ───────────────────────────────────────────────────────
1567
1568    #[test]
1569    #[serial_test::serial]
1570    fn testa_cmd_keys_remove_indice_1_de_config_com_3_chaves() {
1571        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1572        // SAFETY: idem
1573        unsafe {
1574            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1575        }
1576
1577        escrever_config_xdg("ctx7sk-rem-alpha").expect("Deve escrever chave 1");
1578        escrever_config_xdg("ctx7sk-rem-beta").expect("Deve escrever chave 2");
1579        escrever_config_xdg("ctx7sk-rem-gamma").expect("Deve escrever chave 3");
1580
1581        cmd_keys_remove(1).expect("Remove índice 1 deve funcionar");
1582
1583        let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1584        let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1585
1586        unsafe {
1587            std::env::remove_var("CONTEXT7_HOME");
1588        }
1589
1590        assert_eq!(config.keys.len(), 2, "Devem sobrar 2 chaves após remoção");
1591        assert_eq!(config.keys[0].value, "ctx7sk-rem-beta");
1592        assert_eq!(config.keys[1].value, "ctx7sk-rem-gamma");
1593    }
1594
1595    #[test]
1596    #[serial_test::serial]
1597    fn testa_cmd_keys_remove_indice_2_de_config_com_3_chaves() {
1598        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1599        // SAFETY: idem
1600        unsafe {
1601            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1602        }
1603
1604        escrever_config_xdg("ctx7sk-mid-alpha").expect("Deve escrever chave 1");
1605        escrever_config_xdg("ctx7sk-mid-beta").expect("Deve escrever chave 2");
1606        escrever_config_xdg("ctx7sk-mid-gamma").expect("Deve escrever chave 3");
1607
1608        cmd_keys_remove(2).expect("Remove índice 2 deve funcionar");
1609
1610        let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1611        let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1612
1613        unsafe {
1614            std::env::remove_var("CONTEXT7_HOME");
1615        }
1616
1617        assert_eq!(
1618            config.keys.len(),
1619            2,
1620            "Devem sobrar 2 chaves após remoção da do meio"
1621        );
1622        assert_eq!(config.keys[0].value, "ctx7sk-mid-alpha");
1623        assert_eq!(config.keys[1].value, "ctx7sk-mid-gamma");
1624    }
1625
1626    #[test]
1627    #[serial_test::serial]
1628    fn testa_cmd_keys_remove_indice_zero_retorna_err_com_mensagem() {
1629        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1630        // SAFETY: idem
1631        unsafe {
1632            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1633        }
1634
1635        escrever_config_xdg("ctx7sk-idx-zero-test").expect("Deve escrever chave");
1636
1637        let resultado = cmd_keys_remove(0);
1638
1639        unsafe {
1640            std::env::remove_var("CONTEXT7_HOME");
1641        }
1642
1643        assert!(
1644            resultado.is_err(),
1645            "Índice 0 inválido deve retornar Err (exit code 1), obteve: {:?}",
1646            resultado
1647        );
1648    }
1649
1650    #[test]
1651    #[serial_test::serial]
1652    fn testa_cmd_keys_remove_indice_maior_que_len_retorna_err_com_mensagem() {
1653        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1654        // SAFETY: idem
1655        unsafe {
1656            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1657        }
1658
1659        escrever_config_xdg("ctx7sk-overflow-test").expect("Deve escrever chave");
1660
1661        let resultado = cmd_keys_remove(99);
1662
1663        unsafe {
1664            std::env::remove_var("CONTEXT7_HOME");
1665        }
1666
1667        assert!(
1668            resultado.is_err(),
1669            "Índice fora do range deve retornar Err (exit code 1), obteve: {:?}",
1670            resultado
1671        );
1672    }
1673
1674    #[test]
1675    #[serial_test::serial]
1676    fn testa_cmd_keys_remove_em_config_vazio_retorna_err() {
1677        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1678        // SAFETY: idem
1679        unsafe {
1680            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1681        }
1682
1683        let resultado = cmd_keys_remove(1);
1684
1685        unsafe {
1686            std::env::remove_var("CONTEXT7_HOME");
1687        }
1688
1689        assert!(
1690            resultado.is_err(),
1691            "Remover de config vazio deve retornar Err (exit code 1), obteve: {:?}",
1692            resultado
1693        );
1694    }
1695
1696    // ── cmd_keys_clear ────────────────────────────────────────────────────────
1697
1698    #[test]
1699    #[serial_test::serial]
1700    fn testa_cmd_keys_clear_com_yes_true_limpa_todas_as_chaves() {
1701        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1702        // SAFETY: idem
1703        unsafe {
1704            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1705        }
1706
1707        escrever_config_xdg("ctx7sk-clear-alpha").expect("Deve escrever chave 1");
1708        escrever_config_xdg("ctx7sk-clear-beta").expect("Deve escrever chave 2");
1709
1710        let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1711        let antes = ler_config_toml_do_caminho(&caminho).expect("Deve ler config antes");
1712        assert_eq!(antes.keys.len(), 2, "Pré-condição: 2 chaves antes do clear");
1713
1714        cmd_keys_clear(true).expect("clear com yes=true deve funcionar");
1715
1716        let depois = ler_config_toml_do_caminho(&caminho).expect("Deve ler config depois");
1717
1718        unsafe {
1719            std::env::remove_var("CONTEXT7_HOME");
1720        }
1721
1722        assert!(
1723            depois.keys.is_empty(),
1724            "Após clear com yes=true, chaves devem estar vazias"
1725        );
1726    }
1727
1728    #[test]
1729    #[serial_test::serial]
1730    fn testa_cmd_keys_clear_com_yes_true_em_config_inexistente_funciona() {
1731        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1732        // SAFETY: idem
1733        unsafe {
1734            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1735        }
1736
1737        let resultado = cmd_keys_clear(true);
1738
1739        unsafe {
1740            std::env::remove_var("CONTEXT7_HOME");
1741        }
1742
1743        assert!(
1744            resultado.is_ok(),
1745            "clear em config inexistente deve retornar Ok (idempotente), obteve: {:?}",
1746            resultado
1747        );
1748    }
1749
1750    // ── cmd_keys_import ───────────────────────────────────────────────────────
1751
1752    #[test]
1753    #[serial_test::serial]
1754    fn testa_cmd_keys_import_env_valido_com_multiplas_chaves() {
1755        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1756        let arquivo_env = dir_temp.path().join("chaves.env");
1757        std::fs::write(
1758            &arquivo_env,
1759            "CONTEXT7_API=ctx7sk-import-alpha\nCONTEXT7_API=ctx7sk-import-beta\n",
1760        )
1761        .expect("Deve escrever .env de teste");
1762
1763        // SAFETY: idem
1764        unsafe {
1765            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1766        }
1767
1768        let resultado = cmd_keys_import(&arquivo_env);
1769
1770        let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1771        let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config após import");
1772
1773        unsafe {
1774            std::env::remove_var("CONTEXT7_HOME");
1775        }
1776
1777        resultado.expect("import de .env válido deve funcionar");
1778        assert_eq!(config.keys.len(), 2, "Deve ter importado 2 chaves");
1779
1780        let valores: Vec<&str> = config.keys.iter().map(|c| c.value.as_str()).collect();
1781        assert!(valores.contains(&"ctx7sk-import-alpha"));
1782        assert!(valores.contains(&"ctx7sk-import-beta"));
1783    }
1784
1785    #[test]
1786    #[serial_test::serial]
1787    fn testa_cmd_keys_import_env_sem_chaves_retorna_err() {
1788        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1789        let arquivo_env = dir_temp.path().join("vazio.env");
1790        std::fs::write(&arquivo_env, "# apenas comentario\nOUTRA_VAR=valor\n")
1791            .expect("Deve escrever .env sem chaves");
1792
1793        // SAFETY: idem
1794        unsafe {
1795            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1796        }
1797
1798        let resultado = cmd_keys_import(&arquivo_env);
1799
1800        unsafe {
1801            std::env::remove_var("CONTEXT7_HOME");
1802        }
1803
1804        assert!(
1805            resultado.is_err(),
1806            "Import de .env sem chaves CONTEXT7_API deve retornar Err"
1807        );
1808    }
1809
1810    #[test]
1811    #[serial_test::serial]
1812    fn testa_cmd_keys_import_arquivo_inexistente_retorna_err() {
1813        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1814        let arquivo_inexistente = dir_temp.path().join("nao_existe.env");
1815
1816        // SAFETY: idem
1817        unsafe {
1818            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1819        }
1820
1821        let resultado = cmd_keys_import(&arquivo_inexistente);
1822
1823        unsafe {
1824            std::env::remove_var("CONTEXT7_HOME");
1825        }
1826
1827        assert!(
1828            resultado.is_err(),
1829            "Import de arquivo inexistente deve retornar Err"
1830        );
1831    }
1832
1833    #[test]
1834    #[serial_test::serial]
1835    fn testa_cmd_keys_import_roundtrip_add_depois_list() {
1836        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1837        let arquivo_env = dir_temp.path().join("roundtrip.env");
1838        std::fs::write(
1839            &arquivo_env,
1840            "CONTEXT7_API=ctx7sk-rtrip-01\nCONTEXT7_API=ctx7sk-rtrip-02\n",
1841        )
1842        .expect("Deve escrever .env de roundtrip");
1843
1844        // SAFETY: idem
1845        unsafe {
1846            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1847        }
1848
1849        cmd_keys_import(&arquivo_env).expect("Import deve funcionar");
1850
1851        let config = ler_config_xdg_raw()
1852            .expect("Deve retornar Ok")
1853            .expect("Deve retornar Some após import");
1854
1855        unsafe {
1856            std::env::remove_var("CONTEXT7_HOME");
1857        }
1858
1859        assert_eq!(
1860            config.keys.len(),
1861            2,
1862            "Roundtrip: deve ter 2 chaves após import"
1863        );
1864        assert_eq!(config.keys[0].value, "ctx7sk-rtrip-01");
1865        assert_eq!(config.keys[1].value, "ctx7sk-rtrip-02");
1866    }
1867
1868    // ── cmd_keys_export ───────────────────────────────────────────────────────
1869
1870    #[test]
1871    #[serial_test::serial]
1872    fn testa_cmd_keys_export_em_config_vazio_retorna_ok() {
1873        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1874        // SAFETY: idem
1875        unsafe {
1876            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1877        }
1878
1879        let resultado = cmd_keys_export();
1880
1881        unsafe {
1882            std::env::remove_var("CONTEXT7_HOME");
1883        }
1884
1885        assert!(
1886            resultado.is_ok(),
1887            "Export de config vazio deve retornar Ok, obteve: {:?}",
1888            resultado
1889        );
1890    }
1891
1892    #[test]
1893    #[serial_test::serial]
1894    fn testa_cmd_keys_export_retorna_ok_com_chaves_existentes() {
1895        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1896        // SAFETY: idem
1897        unsafe {
1898            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1899        }
1900
1901        escrever_config_xdg("ctx7sk-export-um").expect("Deve escrever chave 1");
1902        escrever_config_xdg("ctx7sk-export-dois").expect("Deve escrever chave 2");
1903
1904        let resultado = cmd_keys_export();
1905
1906        unsafe {
1907            std::env::remove_var("CONTEXT7_HOME");
1908        }
1909
1910        assert!(
1911            resultado.is_ok(),
1912            "Export com chaves existentes deve retornar Ok, obteve: {:?}",
1913            resultado
1914        );
1915    }
1916
1917    // ── ler_config_xdg_raw ────────────────────────────────────────────────────
1918
1919    #[test]
1920    #[serial_test::serial]
1921    fn testa_ler_config_xdg_raw_retorna_none_sem_arquivo() {
1922        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1923        // SAFETY: idem
1924        unsafe {
1925            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1926        }
1927
1928        let resultado = ler_config_xdg_raw();
1929
1930        unsafe {
1931            std::env::remove_var("CONTEXT7_HOME");
1932        }
1933
1934        let valor = resultado.expect("Deve retornar Ok");
1935        assert!(
1936            valor.is_none(),
1937            "Deve retornar None quando config.toml não existe"
1938        );
1939    }
1940
1941    #[test]
1942    #[serial_test::serial]
1943    fn testa_ler_config_xdg_raw_retorna_config_com_chaves() {
1944        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1945        let dir_context7 = dir_temp.path().join("context7");
1946        std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
1947
1948        let toml = r#"schema_version = 1
1949
1950[[keys]]
1951value = "ctx7sk-raw-01"
1952added_at = "2026-04-08T00:00:00+00:00"
1953
1954[[keys]]
1955value = "ctx7sk-raw-02"
1956added_at = "2026-04-08T00:01:00+00:00"
1957"#;
1958        std::fs::write(dir_context7.join("config.toml"), toml).expect("Deve escrever config.toml");
1959
1960        // SAFETY: idem
1961        unsafe {
1962            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1963        }
1964
1965        let resultado = ler_config_xdg_raw();
1966
1967        unsafe {
1968            std::env::remove_var("CONTEXT7_HOME");
1969        }
1970
1971        let config = resultado
1972            .expect("Deve retornar Ok")
1973            .expect("Deve retornar Some com config");
1974        assert_eq!(config.keys.len(), 2);
1975        assert_eq!(config.keys[0].value, "ctx7sk-raw-01");
1976        assert_eq!(config.keys[1].value, "ctx7sk-raw-02");
1977    }
1978
1979    #[test]
1980    #[serial_test::serial]
1981    fn testa_cmd_keys_path_retorna_ok() {
1982        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1983        // SAFETY: idem
1984        unsafe {
1985            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
1986        }
1987
1988        let resultado = cmd_keys_path();
1989
1990        unsafe {
1991            std::env::remove_var("CONTEXT7_HOME");
1992        }
1993
1994        resultado.expect("cmd_keys_path deve retornar Ok");
1995    }
1996
1997    #[test]
1998    #[serial_test::serial]
1999    fn testa_descobrir_caminho_config_termina_com_config_toml() {
2000        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
2001        // SAFETY: idem
2002        unsafe {
2003            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
2004        }
2005
2006        let caminho = descobrir_caminho_config();
2007
2008        unsafe {
2009            std::env::remove_var("CONTEXT7_HOME");
2010        }
2011
2012        let caminho = caminho.expect("Deve retornar caminho XDG válido");
2013        assert!(
2014            caminho.to_string_lossy().ends_with("config.toml"),
2015            "Caminho deve terminar com config.toml, obteve: {}",
2016            caminho.display()
2017        );
2018        assert!(
2019            caminho.to_string_lossy().contains("context7"),
2020            "Caminho deve conter 'context7', obteve: {}",
2021            caminho.display()
2022        );
2023    }
2024
2025    // ── fluxo completo ────────────────────────────────────────────────────────
2026
2027    #[test]
2028    #[serial_test::serial]
2029    fn testa_fluxo_completo_add_list_remove_clear() {
2030        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
2031        // SAFETY: idem
2032        unsafe {
2033            std::env::set_var("CONTEXT7_HOME", dir_temp.path());
2034        }
2035
2036        cmd_keys_add("ctx7sk-fluxo-01").expect("Add 1 deve funcionar");
2037        cmd_keys_add("ctx7sk-fluxo-02").expect("Add 2 deve funcionar");
2038        cmd_keys_add("ctx7sk-fluxo-03").expect("Add 3 deve funcionar");
2039
2040        let config_antes = ler_config_xdg_raw()
2041            .expect("Ok")
2042            .expect("Some com 3 chaves");
2043        assert_eq!(config_antes.keys.len(), 3, "Deve ter 3 chaves após 3 adds");
2044
2045        cmd_keys_remove(2).expect("Remove índice 2 deve funcionar");
2046
2047        let config_pos_remove = ler_config_xdg_raw()
2048            .expect("Ok")
2049            .expect("Some com 2 chaves");
2050        assert_eq!(
2051            config_pos_remove.keys.len(),
2052            2,
2053            "Deve ter 2 chaves após remove"
2054        );
2055        assert_eq!(config_pos_remove.keys[0].value, "ctx7sk-fluxo-01");
2056        assert_eq!(config_pos_remove.keys[1].value, "ctx7sk-fluxo-03");
2057
2058        cmd_keys_clear(true).expect("Clear com yes=true deve funcionar");
2059
2060        let caminho = descobrir_caminho_config().expect("Deve ter caminho");
2061        let config_final = ler_config_toml_do_caminho(&caminho).expect("Deve ler config final");
2062
2063        unsafe {
2064            std::env::remove_var("CONTEXT7_HOME");
2065        }
2066
2067        assert!(
2068            config_final.keys.is_empty(),
2069            "Após clear, chaves devem estar vazias"
2070        );
2071    }
2072
2073    // ── CONTEXT7_HOME override direto ─────────────────────────────────────────
2074
2075    #[test]
2076    #[serial_test::serial]
2077    fn testa_context7_home_override_config_path() {
2078        let tmp = tempfile::TempDir::new().expect("Deve criar tempdir");
2079        // SAFETY: testes serializados via #[serial_test::serial] garantem ausência de
2080        // concorrência. Necessário para compatibilidade com Rust 2024 edition.
2081        unsafe {
2082            std::env::set_var("CONTEXT7_HOME", tmp.path());
2083        }
2084
2085        let caminho = descobrir_caminho_config();
2086
2087        unsafe {
2088            std::env::remove_var("CONTEXT7_HOME");
2089        }
2090
2091        let caminho = caminho.expect("Deve retornar Some quando CONTEXT7_HOME está definido");
2092        let esperado = tmp.path().join("context7").join("config.toml");
2093        assert_eq!(
2094            caminho, esperado,
2095            "CONTEXT7_HOME deve definir caminho como {{CONTEXT7_HOME}}/context7/config.toml"
2096        );
2097    }
2098
2099    #[test]
2100    #[serial_test::serial]
2101    fn testa_context7_home_override_logs_path() {
2102        let tmp = tempfile::TempDir::new().expect("Deve criar tempdir");
2103        // SAFETY: idem
2104        unsafe {
2105            std::env::set_var("CONTEXT7_HOME", tmp.path());
2106        }
2107
2108        let caminho = descobrir_caminho_logs_xdg();
2109
2110        unsafe {
2111            std::env::remove_var("CONTEXT7_HOME");
2112        }
2113
2114        let caminho = caminho.expect("Deve retornar Some quando CONTEXT7_HOME está definido");
2115        let esperado = tmp.path().join("context7").join("logs");
2116        assert_eq!(
2117            caminho, esperado,
2118            "CONTEXT7_HOME deve definir logs como {{CONTEXT7_HOME}}/context7/logs"
2119        );
2120    }
2121
2122    #[test]
2123    #[serial_test::serial]
2124    fn testa_context7_home_vazio_cai_em_projectdirs() {
2125        let tmp = tempfile::TempDir::new().expect("Deve criar tempdir");
2126        // SAFETY: idem
2127        unsafe {
2128            std::env::set_var("CONTEXT7_HOME", "");
2129        }
2130
2131        let caminho = descobrir_caminho_config();
2132
2133        unsafe {
2134            std::env::remove_var("CONTEXT7_HOME");
2135        }
2136
2137        // Quando CONTEXT7_HOME é vazio, cai em ProjectDirs — path NÃO deve ser dentro do tempdir
2138        if let Some(c) = caminho {
2139            let tmp_str = tmp.path().to_string_lossy();
2140            assert!(
2141                !c.to_string_lossy().starts_with(tmp_str.as_ref()),
2142                "CONTEXT7_HOME vazio não deve usar o tempdir: {}",
2143                c.display()
2144            );
2145        }
2146        // Se ProjectDirs retornar None (CI sem home), também é aceitável
2147    }
2148}