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