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