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