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