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")
603                || mensagem_erro.contains("CONTEXT7_API")
604                || mensagem_erro.contains("key")
605                || mensagem_erro.contains("API"),
606            "Mensagem de erro deve mencionar CONTEXT7_API, chave, key ou API, obteve: {}",
607            mensagem_erro
608        );
609    }
610
611    #[test]
612    fn testa_parsing_env_ignora_chaves_vazias() {
613        let conteudo = "CONTEXT7_API=\n\
614                        CONTEXT7_API=ctx7sk-valida\n";
615        let chaves = extrair_chaves_env(conteudo).expect("Deve extrair chave sem erro");
616        assert_eq!(
617            chaves.len(),
618            1,
619            "Deve ignorar entradas CONTEXT7_API sem valor"
620        );
621        assert_eq!(chaves[0], "ctx7sk-valida");
622    }
623
624    #[test]
625    fn testa_parsing_env_ignora_comentario_inline() {
626        let conteudo = "CONTEXT7_API=ctx7sk-valida # comentário aqui\n";
627        let chaves = extrair_chaves_env(conteudo).expect("Deve extrair chave sem erro");
628        assert_eq!(chaves.len(), 1);
629        assert_eq!(chaves[0], "ctx7sk-valida");
630    }
631
632    // ── mascarar_chave ────────────────────────────────────────────────────────
633
634    #[test]
635    fn testa_mascarar_chave_com_valor_longo_exibe_prefixo_e_sufixo() {
636        let chave = "ctx7sk-abc123-def456-ghi789";
637        assert_eq!(chave.len(), 27, "Pré-condição: chave deve ter 27 chars");
638        let mascarada = mascarar_chave(chave);
639        assert!(
640            mascarada.starts_with("ctx7sk-abc12"),
641            "Deve iniciar com os primeiros 12 chars, obteve: {}",
642            mascarada
643        );
644        assert!(
645            mascarada.ends_with("i789"),
646            "Deve terminar com os últimos 4 chars, obteve: {}",
647            mascarada
648        );
649        assert!(
650            mascarada.contains("..."),
651            "Deve conter '...' entre prefixo e sufixo, obteve: {}",
652            mascarada
653        );
654    }
655
656    #[test]
657    fn testa_mascarar_chave_curta_retorna_asteriscos() {
658        let chave_exatamente_16 = "ctx7sk-abcdef012";
659        assert_eq!(
660            chave_exatamente_16.len(),
661            16,
662            "Pré-condição: chave deve ter 16 chars"
663        );
664        let mascarada = mascarar_chave(chave_exatamente_16);
665        assert_eq!(
666            mascarada, "***",
667            "Chave de 16 chars deve retornar '***', obteve: {}",
668            mascarada
669        );
670    }
671
672    #[test]
673    fn testa_mascarar_chave_vazia_retorna_asteriscos() {
674        let mascarada = mascarar_chave("");
675        assert_eq!(
676            mascarada, "***",
677            "Chave vazia deve retornar '***', obteve: {}",
678            mascarada
679        );
680    }
681
682    #[test]
683    fn testa_mascarar_chave_de_exatamente_17_chars_mascara_corretamente() {
684        let chave = "ctx7sk-abcdef0123"; // 17 chars
685        assert_eq!(chave.len(), 17, "Pré-condição: chave deve ter 17 chars");
686        let mascarada = mascarar_chave(chave);
687        assert!(
688            mascarada.contains("..."),
689            "Chave de 17 chars deve ser mascarada, obteve: {}",
690            mascarada
691        );
692        assert_eq!(
693            &mascarada[..12],
694            &chave[..12],
695            "Prefixo de 12 chars deve ser preservado"
696        );
697        assert!(
698            mascarada.ends_with(&chave[chave.len() - 4..]),
699            "Sufixo de 4 chars deve ser preservado"
700        );
701    }
702
703    // ── ler_env_var_chave ─────────────────────────────────────────────────────
704
705    #[test]
706    #[serial_test::serial]
707    fn testa_ler_env_var_chave_retorna_some_quando_setada() {
708        // SAFETY: testes serializados via #[serial_test::serial] garantem ausência de
709        // concorrência. Necessário para compatibilidade com Rust 2024 edition.
710        unsafe {
711            std::env::set_var("CONTEXT7_API_KEYS", "ctx7sk-chave-teste-01");
712        }
713        let resultado = ler_env_var_chave();
714        unsafe {
715            std::env::remove_var("CONTEXT7_API_KEYS");
716        }
717
718        let chaves = resultado.expect("Deve retornar Some com chave válida");
719        assert_eq!(chaves.len(), 1, "Deve retornar exatamente 1 chave");
720        assert_eq!(chaves[0], "ctx7sk-chave-teste-01");
721    }
722
723    #[test]
724    #[serial_test::serial]
725    fn testa_ler_env_var_chave_aceita_multiplas_separadas_por_virgula() {
726        // SAFETY: idem
727        unsafe {
728            std::env::set_var(
729                "CONTEXT7_API_KEYS",
730                "ctx7sk-chave-a, ctx7sk-chave-b , ctx7sk-chave-c",
731            );
732        }
733        let resultado = ler_env_var_chave();
734        unsafe {
735            std::env::remove_var("CONTEXT7_API_KEYS");
736        }
737
738        let chaves = resultado.expect("Deve retornar Some com múltiplas chaves");
739        assert_eq!(chaves.len(), 3, "Deve retornar 3 chaves");
740        assert_eq!(chaves[0], "ctx7sk-chave-a");
741        assert_eq!(chaves[1], "ctx7sk-chave-b");
742        assert_eq!(chaves[2], "ctx7sk-chave-c");
743    }
744
745    #[test]
746    #[serial_test::serial]
747    fn testa_ler_env_var_chave_retorna_none_quando_vazia() {
748        // SAFETY: idem
749        unsafe {
750            std::env::set_var("CONTEXT7_API_KEYS", "");
751        }
752        let resultado = ler_env_var_chave();
753        unsafe {
754            std::env::remove_var("CONTEXT7_API_KEYS");
755        }
756
757        assert!(
758            resultado.is_none(),
759            "Deve retornar None quando env var está vazia"
760        );
761    }
762
763    #[test]
764    #[serial_test::serial]
765    fn testa_ler_env_var_chave_retorna_none_quando_apenas_whitespace() {
766        // SAFETY: idem
767        unsafe {
768            std::env::set_var("CONTEXT7_API_KEYS", "   ,  ,  ");
769        }
770        let resultado = ler_env_var_chave();
771        unsafe {
772            std::env::remove_var("CONTEXT7_API_KEYS");
773        }
774
775        assert!(
776            resultado.is_none(),
777            "Deve retornar None quando env var contém apenas whitespace/vírgulas"
778        );
779    }
780
781    #[test]
782    #[serial_test::serial]
783    fn testa_ler_env_var_chave_retorna_none_quando_ausente() {
784        // SAFETY: idem
785        unsafe {
786            std::env::remove_var("CONTEXT7_API_KEYS");
787        }
788        let resultado = ler_env_var_chave();
789
790        assert!(
791            resultado.is_none(),
792            "Deve retornar None quando env var não existe"
793        );
794    }
795
796    // ── ler_config_xdg via XDG_CONFIG_HOME ───────────────────────────────────
797
798    #[test]
799    #[serial_test::serial]
800    fn testa_ler_config_xdg_arquivo_inexistente_retorna_none() {
801        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
802        // SAFETY: idem
803        unsafe {
804            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
805        }
806        let resultado = ler_config_xdg();
807        unsafe {
808            std::env::remove_var("XDG_CONFIG_HOME");
809        }
810
811        let valor = resultado.expect("Deve retornar Ok quando arquivo não existe");
812        assert!(
813            valor.is_none(),
814            "Deve retornar None quando config.toml não existe"
815        );
816    }
817
818    #[test]
819    #[serial_test::serial]
820    fn testa_ler_config_xdg_le_toml_valido_com_multiplas_chaves() {
821        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
822        let dir_context7 = dir_temp.path().join("context7");
823        std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
824
825        let toml_conteudo = r#"schema_version = 1
826
827[[keys]]
828value = "ctx7sk-chave-xdg-01"
829added_at = "2026-01-01T00:00:00+00:00"
830
831[[keys]]
832value = "ctx7sk-chave-xdg-02"
833added_at = "2026-01-02T00:00:00+00:00"
834"#;
835        std::fs::write(dir_context7.join("config.toml"), toml_conteudo)
836            .expect("Deve escrever config.toml");
837
838        // SAFETY: idem
839        unsafe {
840            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
841        }
842        let resultado = ler_config_xdg();
843        unsafe {
844            std::env::remove_var("XDG_CONFIG_HOME");
845        }
846
847        let chaves = resultado
848            .expect("Deve retornar Ok")
849            .expect("Deve retornar Some com chaves");
850        assert_eq!(chaves.len(), 2, "Deve retornar 2 chaves");
851        assert_eq!(chaves[0], "ctx7sk-chave-xdg-01");
852        assert_eq!(chaves[1], "ctx7sk-chave-xdg-02");
853    }
854
855    #[test]
856    #[serial_test::serial]
857    fn testa_ler_config_xdg_retorna_err_quando_toml_invalido() {
858        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
859        let dir_context7 = dir_temp.path().join("context7");
860        std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
861
862        std::fs::write(
863            dir_context7.join("config.toml"),
864            "schema_version = INVALIDO\n[[[malformado",
865        )
866        .expect("Deve escrever TOML inválido");
867
868        // SAFETY: idem
869        unsafe {
870            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
871        }
872        let resultado = ler_config_xdg();
873        unsafe {
874            std::env::remove_var("XDG_CONFIG_HOME");
875        }
876
877        assert!(
878            resultado.is_err(),
879            "Deve retornar Err quando TOML está malformado"
880        );
881    }
882
883    #[test]
884    #[serial_test::serial]
885    fn testa_ler_config_xdg_preserva_ordem_das_chaves() {
886        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
887        let dir_context7 = dir_temp.path().join("context7");
888        std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
889
890        let toml_conteudo = r#"schema_version = 1
891
892[[keys]]
893value = "ctx7sk-primeira"
894added_at = "2026-01-01T00:00:00+00:00"
895
896[[keys]]
897value = "ctx7sk-segunda"
898added_at = "2026-01-02T00:00:00+00:00"
899
900[[keys]]
901value = "ctx7sk-terceira"
902added_at = "2026-01-03T00:00:00+00:00"
903"#;
904        std::fs::write(dir_context7.join("config.toml"), toml_conteudo)
905            .expect("Deve escrever config.toml");
906
907        // SAFETY: idem
908        unsafe {
909            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
910        }
911        let resultado = ler_config_xdg();
912        unsafe {
913            std::env::remove_var("XDG_CONFIG_HOME");
914        }
915
916        let chaves = resultado
917            .expect("Deve retornar Ok")
918            .expect("Deve retornar Some");
919        assert_eq!(chaves[0], "ctx7sk-primeira");
920        assert_eq!(chaves[1], "ctx7sk-segunda");
921        assert_eq!(chaves[2], "ctx7sk-terceira");
922    }
923
924    #[test]
925    #[serial_test::serial]
926    fn testa_ler_config_xdg_keys_vazio_retorna_none() {
927        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
928        let dir_context7 = dir_temp.path().join("context7");
929        std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
930
931        let toml_sem_chaves = "schema_version = 1\n";
932        std::fs::write(dir_context7.join("config.toml"), toml_sem_chaves)
933            .expect("Deve escrever config.toml sem keys");
934
935        // SAFETY: idem
936        unsafe {
937            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
938        }
939        let resultado = ler_config_xdg();
940        unsafe {
941            std::env::remove_var("XDG_CONFIG_HOME");
942        }
943
944        let valor = resultado.expect("Deve retornar Ok");
945        assert!(
946            valor.is_none(),
947            "Deve retornar None quando config.toml existe mas keys está vazio"
948        );
949    }
950
951    // ── escrever_config_xdg ───────────────────────────────────────────────────
952
953    #[test]
954    #[serial_test::serial]
955    fn testa_escrever_config_xdg_roundtrip_serializa_e_deserializa() {
956        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
957        // SAFETY: idem
958        unsafe {
959            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
960        }
961
962        let caminho =
963            escrever_config_xdg("ctx7sk-roundtrip-01").expect("Deve escrever config sem erro");
964
965        let config_lido = ler_config_toml_do_caminho(&caminho)
966            .expect("Deve ler TOML escrito por escrever_config_xdg");
967
968        unsafe {
969            std::env::remove_var("XDG_CONFIG_HOME");
970        }
971
972        assert_eq!(config_lido.schema_version, 1, "schema_version deve ser 1");
973        assert_eq!(config_lido.keys.len(), 1, "Deve conter 1 chave");
974        assert_eq!(
975            config_lido.keys[0].value, "ctx7sk-roundtrip-01",
976            "Valor da chave deve ser preservado"
977        );
978        assert!(
979            !config_lido.keys[0].added_at.is_empty(),
980            "added_at não deve ser vazio"
981        );
982    }
983
984    #[test]
985    #[serial_test::serial]
986    fn testa_escrever_config_xdg_cria_diretorios_pai_se_nao_existirem() {
987        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
988        let xdg_novo = dir_temp.path().join("xdg_inexistente");
989        // SAFETY: idem
990        unsafe {
991            std::env::set_var("XDG_CONFIG_HOME", &xdg_novo);
992        }
993
994        let resultado = escrever_config_xdg("ctx7sk-mkdir-teste");
995        unsafe {
996            std::env::remove_var("XDG_CONFIG_HOME");
997        }
998
999        let caminho = resultado.expect("Deve criar diretório pai e escrever config");
1000        assert!(
1001            caminho.exists(),
1002            "Arquivo de config deve existir após escrita"
1003        );
1004    }
1005
1006    #[test]
1007    #[serial_test::serial]
1008    fn testa_escrever_config_xdg_nao_duplica_chave_ja_existente() {
1009        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1010        // SAFETY: idem
1011        unsafe {
1012            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1013        }
1014
1015        escrever_config_xdg("ctx7sk-unica").expect("Primeira escrita deve funcionar");
1016        escrever_config_xdg("ctx7sk-unica").expect("Segunda escrita não deve falhar");
1017
1018        let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1019        let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1020
1021        unsafe {
1022            std::env::remove_var("XDG_CONFIG_HOME");
1023        }
1024
1025        assert_eq!(
1026            config.keys.len(),
1027            1,
1028            "Não deve duplicar chave já existente — deve ter apenas 1"
1029        );
1030    }
1031
1032    #[test]
1033    #[serial_test::serial]
1034    fn testa_escrever_config_xdg_acumula_chaves_distintas() {
1035        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1036        // SAFETY: idem
1037        unsafe {
1038            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1039        }
1040
1041        escrever_config_xdg("ctx7sk-chave-a").expect("Primeira escrita deve funcionar");
1042        escrever_config_xdg("ctx7sk-chave-b").expect("Segunda escrita deve funcionar");
1043        escrever_config_xdg("ctx7sk-chave-c").expect("Terceira escrita deve funcionar");
1044
1045        let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1046        let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1047
1048        unsafe {
1049            std::env::remove_var("XDG_CONFIG_HOME");
1050        }
1051
1052        assert_eq!(config.keys.len(), 3, "Deve acumular 3 chaves distintas");
1053        let valores: Vec<&str> = config.keys.iter().map(|c| c.value.as_str()).collect();
1054        assert!(valores.contains(&"ctx7sk-chave-a"));
1055        assert!(valores.contains(&"ctx7sk-chave-b"));
1056        assert!(valores.contains(&"ctx7sk-chave-c"));
1057    }
1058
1059    #[test]
1060    #[cfg(unix)]
1061    #[serial_test::serial]
1062    fn testa_escrever_config_xdg_aplica_permissoes_600_em_unix() {
1063        use std::os::unix::fs::PermissionsExt;
1064
1065        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1066        // SAFETY: idem
1067        unsafe {
1068            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1069        }
1070
1071        let caminho =
1072            escrever_config_xdg("ctx7sk-perm-600").expect("Deve escrever config sem erro");
1073        unsafe {
1074            std::env::remove_var("XDG_CONFIG_HOME");
1075        }
1076
1077        let metadados = std::fs::metadata(&caminho).expect("Deve obter metadados do arquivo");
1078        let modo = metadados.permissions().mode() & 0o777;
1079
1080        assert_eq!(modo, 0o600, "Permissões devem ser 600, obteve: {:o}", modo);
1081    }
1082
1083    // ── Serde TOML roundtrip ──────────────────────────────────────────────────
1084
1085    #[test]
1086    fn testa_config_arquivo_roundtrip_serde_preserva_todos_campos() {
1087        let config_original = ConfigArquivo {
1088            schema_version: 1,
1089            keys: vec![
1090                ChaveArmazenada {
1091                    value: "ctx7sk-serde-01".to_string(),
1092                    added_at: "2026-01-01T12:00:00+00:00".to_string(),
1093                },
1094                ChaveArmazenada {
1095                    value: "ctx7sk-serde-02".to_string(),
1096                    added_at: "2026-01-02T12:00:00+00:00".to_string(),
1097                },
1098            ],
1099        };
1100
1101        let toml_str = toml::to_string_pretty(&config_original)
1102            .expect("Deve serializar ConfigArquivo para TOML");
1103        let config_deserializado: ConfigArquivo =
1104            toml::from_str(&toml_str).expect("Deve deserializar TOML de volta para ConfigArquivo");
1105
1106        assert_eq!(
1107            config_deserializado.schema_version, config_original.schema_version,
1108            "schema_version deve ser preservado no roundtrip"
1109        );
1110        assert_eq!(
1111            config_deserializado.keys.len(),
1112            config_original.keys.len(),
1113            "Número de chaves deve ser preservado"
1114        );
1115        assert_eq!(
1116            config_deserializado.keys[0].value, config_original.keys[0].value,
1117            "Valor da primeira chave deve ser preservado"
1118        );
1119        assert_eq!(
1120            config_deserializado.keys[0].added_at, config_original.keys[0].added_at,
1121            "added_at da primeira chave deve ser preservado"
1122        );
1123    }
1124
1125    #[test]
1126    fn testa_config_arquivo_schema_version_sempre_presente_na_serializacao() {
1127        let config = ConfigArquivo {
1128            schema_version: 1,
1129            keys: Vec::new(),
1130        };
1131
1132        let toml_str = toml::to_string_pretty(&config).expect("Deve serializar para TOML");
1133
1134        assert!(
1135            toml_str.contains("schema_version"),
1136            "schema_version deve estar presente na serialização TOML"
1137        );
1138        assert!(toml_str.contains('1'), "Valor 1 deve estar presente");
1139    }
1140
1141    #[test]
1142    fn testa_config_arquivo_keys_vazio_aceito_na_deserializacao() {
1143        let toml_str = "schema_version = 1\n";
1144        let config: ConfigArquivo =
1145            toml::from_str(toml_str).expect("Deve deserializar com keys ausente (default vazio)");
1146
1147        assert_eq!(config.schema_version, 1);
1148        assert!(
1149            config.keys.is_empty(),
1150            "keys deve ser vazio quando não presente no TOML"
1151        );
1152    }
1153
1154    #[test]
1155    fn testa_chave_armazenada_preserva_added_at_como_string_utc() {
1156        let timestamp = "2026-04-08T20:00:00+00:00";
1157        let chave = ChaveArmazenada {
1158            value: "ctx7sk-timestamp".to_string(),
1159            added_at: timestamp.to_string(),
1160        };
1161
1162        let toml_str = toml::to_string_pretty(&chave).expect("Deve serializar ChaveArmazenada");
1163        let chave_de_volta: ChaveArmazenada =
1164            toml::from_str(&toml_str).expect("Deve deserializar ChaveArmazenada");
1165
1166        assert_eq!(
1167            chave_de_volta.added_at, timestamp,
1168            "Timestamp added_at deve ser preservado exatamente"
1169        );
1170    }
1171
1172    // ── carregar_chaves_api (precedência) ─────────────────────────────────────
1173
1174    #[test]
1175    #[serial_test::serial]
1176    fn testa_carregar_chaves_api_env_var_tem_prioridade_sobre_xdg() {
1177        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1178        let dir_context7 = dir_temp.path().join("context7");
1179        std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
1180
1181        let toml_xdg = r#"schema_version = 1
1182[[keys]]
1183value = "ctx7sk-xdg-deve-ser-ignorada"
1184added_at = "2026-01-01T00:00:00+00:00"
1185"#;
1186        std::fs::write(dir_context7.join("config.toml"), toml_xdg)
1187            .expect("Deve escrever config XDG");
1188
1189        // SAFETY: idem
1190        unsafe {
1191            std::env::set_var("CONTEXT7_API_KEYS", "ctx7sk-env-var-prioritaria");
1192            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1193        }
1194
1195        let resultado = carregar_chaves_api();
1196
1197        unsafe {
1198            std::env::remove_var("CONTEXT7_API_KEYS");
1199            std::env::remove_var("XDG_CONFIG_HOME");
1200        }
1201
1202        let chaves = resultado.expect("Deve carregar chaves via env var");
1203        assert_eq!(chaves.len(), 1);
1204        assert_eq!(
1205            chaves[0], "ctx7sk-env-var-prioritaria",
1206            "Env var deve ter prioridade sobre XDG"
1207        );
1208    }
1209
1210    #[test]
1211    #[serial_test::serial]
1212    fn testa_carregar_chaves_api_xdg_usado_quando_env_var_ausente() {
1213        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1214        let dir_context7 = dir_temp.path().join("context7");
1215        std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
1216
1217        let toml_xdg = r#"schema_version = 1
1218[[keys]]
1219value = "ctx7sk-via-xdg"
1220added_at = "2026-01-01T00:00:00+00:00"
1221"#;
1222        std::fs::write(dir_context7.join("config.toml"), toml_xdg)
1223            .expect("Deve escrever config XDG");
1224
1225        // SAFETY: idem
1226        unsafe {
1227            std::env::remove_var("CONTEXT7_API_KEYS");
1228            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1229        }
1230
1231        let resultado = carregar_chaves_api();
1232
1233        unsafe {
1234            std::env::remove_var("XDG_CONFIG_HOME");
1235        }
1236
1237        let chaves = resultado.expect("Deve carregar chaves via XDG");
1238        assert_eq!(chaves.len(), 1);
1239        assert_eq!(chaves[0], "ctx7sk-via-xdg");
1240    }
1241
1242    #[test]
1243    #[serial_test::serial]
1244    fn testa_carregar_chaves_api_retorna_err_quando_nada_disponivel() {
1245        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1246        let dir_xdg_vazio = dir_temp.path().join("xdg_vazio");
1247        std::fs::create_dir_all(&dir_xdg_vazio).expect("Deve criar diretório XDG vazio");
1248
1249        let dir_sem_env = dir_temp.path().join("sem_env");
1250        std::fs::create_dir_all(&dir_sem_env).expect("Deve criar diretório sem .env");
1251
1252        // SAFETY: idem
1253        unsafe {
1254            std::env::remove_var("CONTEXT7_API_KEYS");
1255            std::env::set_var("XDG_CONFIG_HOME", &dir_xdg_vazio);
1256        }
1257        let cwd_original = std::env::current_dir().expect("Deve obter CWD atual");
1258        std::env::set_current_dir(&dir_sem_env).expect("Deve mudar CWD");
1259
1260        let resultado = carregar_chaves_api();
1261
1262        std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1263        unsafe {
1264            std::env::remove_var("XDG_CONFIG_HOME");
1265        }
1266
1267        assert!(
1268            resultado.is_err(),
1269            "Deve retornar Err quando nenhuma camada fornecer chaves"
1270        );
1271    }
1272
1273    #[test]
1274    #[serial_test::serial]
1275    fn testa_ler_env_cwd_le_env_com_multiplas_chaves_context7_api() {
1276        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1277        let conteudo_env = "CONTEXT7_API=ctx7sk-cwd-01\nCONTEXT7_API=ctx7sk-cwd-02\n";
1278        std::fs::write(dir_temp.path().join(".env"), conteudo_env)
1279            .expect("Deve escrever .env temporário");
1280
1281        let cwd_original = std::env::current_dir().expect("Deve obter CWD");
1282        std::env::set_current_dir(dir_temp.path()).expect("Deve mudar CWD para temp");
1283
1284        let resultado = ler_env_cwd();
1285
1286        std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1287
1288        let chaves = resultado.expect("Deve retornar Some com chaves do .env CWD");
1289        assert_eq!(chaves.len(), 2, "Deve ler 2 chaves do .env");
1290        assert_eq!(chaves[0], "ctx7sk-cwd-01");
1291        assert_eq!(chaves[1], "ctx7sk-cwd-02");
1292    }
1293
1294    #[test]
1295    #[serial_test::serial]
1296    fn testa_ler_env_cwd_retorna_none_quando_env_ausente() {
1297        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1298
1299        let cwd_original = std::env::current_dir().expect("Deve obter CWD");
1300        std::env::set_current_dir(dir_temp.path()).expect("Deve mudar CWD para temp sem .env");
1301
1302        let resultado = ler_env_cwd();
1303
1304        std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1305
1306        assert!(
1307            resultado.is_none(),
1308            "Deve retornar None quando não há .env no CWD"
1309        );
1310    }
1311
1312    #[test]
1313    fn testa_descobrir_caminho_logs_xdg_retorna_algum_caminho_valido() {
1314        let resultado = descobrir_caminho_logs_xdg();
1315
1316        if let Some(caminho) = resultado {
1317            let caminho_str = caminho.to_string_lossy();
1318            assert!(
1319                caminho_str.contains("context7"),
1320                "Caminho de logs XDG deve conter 'context7', obteve: {}",
1321                caminho_str
1322            );
1323        }
1324    }
1325
1326    #[test]
1327    #[serial_test::serial]
1328    fn testa_carregar_chaves_api_env_cwd_usado_quando_env_var_e_xdg_ausentes() {
1329        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1330        let dir_xdg_sem_config = dir_temp.path().join("xdg_sem_config");
1331        std::fs::create_dir_all(&dir_xdg_sem_config).expect("Deve criar diretório XDG vazio");
1332
1333        let dir_cwd = dir_temp.path().join("cwd_com_env");
1334        std::fs::create_dir_all(&dir_cwd).expect("Deve criar CWD temporário");
1335        std::fs::write(dir_cwd.join(".env"), "CONTEXT7_API=ctx7sk-cwd-camada-3\n")
1336            .expect("Deve escrever .env no CWD");
1337
1338        // SAFETY: idem
1339        unsafe {
1340            std::env::remove_var("CONTEXT7_API_KEYS");
1341            std::env::set_var("XDG_CONFIG_HOME", &dir_xdg_sem_config);
1342        }
1343        let cwd_original = std::env::current_dir().expect("Deve obter CWD");
1344        std::env::set_current_dir(&dir_cwd).expect("Deve mudar CWD");
1345
1346        let resultado = carregar_chaves_api();
1347
1348        std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1349        unsafe {
1350            std::env::remove_var("XDG_CONFIG_HOME");
1351        }
1352
1353        let chaves = resultado.expect("Deve carregar chaves via .env CWD");
1354        assert_eq!(chaves.len(), 1);
1355        assert_eq!(chaves[0], "ctx7sk-cwd-camada-3");
1356    }
1357
1358    #[test]
1359    #[serial_test::serial]
1360    fn testa_carregar_chaves_api_faz_fallback_quando_xdg_invalido() {
1361        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1362        let dir_context7 = dir_temp.path().join("context7");
1363        std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
1364
1365        std::fs::write(dir_context7.join("config.toml"), "[[[invalido")
1366            .expect("Deve escrever TOML inválido");
1367
1368        let dir_cwd = dir_temp.path().join("cwd_fallback");
1369        std::fs::create_dir_all(&dir_cwd).expect("Deve criar CWD com .env");
1370        std::fs::write(dir_cwd.join(".env"), "CONTEXT7_API=ctx7sk-fallback-cwd\n")
1371            .expect("Deve escrever .env no CWD");
1372
1373        // SAFETY: idem
1374        unsafe {
1375            std::env::remove_var("CONTEXT7_API_KEYS");
1376            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1377        }
1378        let cwd_original = std::env::current_dir().expect("Deve obter CWD");
1379        std::env::set_current_dir(&dir_cwd).expect("Deve mudar CWD");
1380
1381        let resultado = carregar_chaves_api();
1382
1383        std::env::set_current_dir(&cwd_original).expect("Deve restaurar CWD");
1384        unsafe {
1385            std::env::remove_var("XDG_CONFIG_HOME");
1386        }
1387
1388        let chaves = resultado.expect("Deve carregar chaves via fallback .env CWD");
1389        assert_eq!(chaves.len(), 1);
1390        assert_eq!(chaves[0], "ctx7sk-fallback-cwd");
1391    }
1392
1393    // ── cmd_keys_add ─────────────────────────────────────────────────────────
1394
1395    #[test]
1396    #[serial_test::serial]
1397    fn testa_cmd_keys_add_cria_config_quando_nao_existe() {
1398        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1399        // SAFETY: idem
1400        unsafe {
1401            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1402        }
1403
1404        let resultado = cmd_keys_add("ctx7sk-nova-chave-add-test");
1405
1406        unsafe {
1407            std::env::remove_var("XDG_CONFIG_HOME");
1408        }
1409
1410        resultado.expect("cmd_keys_add deve funcionar em config vazio");
1411
1412        let caminho = dir_temp.path().join("context7").join("config.toml");
1413        assert!(
1414            caminho.exists(),
1415            "config.toml deve existir após cmd_keys_add"
1416        );
1417
1418        let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config criado");
1419        assert_eq!(config.keys.len(), 1, "Config deve ter 1 chave");
1420        assert_eq!(config.keys[0].value, "ctx7sk-nova-chave-add-test");
1421    }
1422
1423    #[test]
1424    #[serial_test::serial]
1425    fn testa_cmd_keys_add_acumula_em_config_existente() {
1426        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1427        // SAFETY: idem
1428        unsafe {
1429            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1430        }
1431
1432        cmd_keys_add("ctx7sk-chave-um").expect("Primeira adição deve funcionar");
1433        cmd_keys_add("ctx7sk-chave-dois").expect("Segunda adição deve funcionar");
1434
1435        let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1436        let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1437
1438        unsafe {
1439            std::env::remove_var("XDG_CONFIG_HOME");
1440        }
1441
1442        assert_eq!(config.keys.len(), 2, "Deve acumular 2 chaves");
1443        assert_eq!(config.keys[0].value, "ctx7sk-chave-um");
1444        assert_eq!(config.keys[1].value, "ctx7sk-chave-dois");
1445    }
1446
1447    #[test]
1448    #[serial_test::serial]
1449    fn testa_cmd_keys_add_nao_duplica_chave_existente() {
1450        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1451        // SAFETY: idem
1452        unsafe {
1453            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1454        }
1455
1456        cmd_keys_add("ctx7sk-unica-dedup").expect("Primeira adição deve funcionar");
1457        cmd_keys_add("ctx7sk-unica-dedup").expect("Segunda adição da mesma chave não deve falhar");
1458
1459        let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1460        let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1461
1462        unsafe {
1463            std::env::remove_var("XDG_CONFIG_HOME");
1464        }
1465
1466        assert_eq!(config.keys.len(), 1, "Não deve duplicar chave já existente");
1467    }
1468
1469    #[test]
1470    #[cfg(unix)]
1471    #[serial_test::serial]
1472    fn testa_cmd_keys_add_aplica_permissoes_600_em_unix() {
1473        use std::os::unix::fs::PermissionsExt;
1474
1475        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1476        // SAFETY: idem
1477        unsafe {
1478            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1479        }
1480
1481        cmd_keys_add("ctx7sk-perm-600-keys-add").expect("Deve adicionar chave sem erro");
1482
1483        let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1484        unsafe {
1485            std::env::remove_var("XDG_CONFIG_HOME");
1486        }
1487
1488        let metadados = std::fs::metadata(&caminho).expect("Deve obter metadados");
1489        let modo = metadados.permissions().mode() & 0o777;
1490        assert_eq!(
1491            modo, 0o600,
1492            "Permissões devem ser 600 após cmd_keys_add, obteve: {:o}",
1493            modo
1494        );
1495    }
1496
1497    // ── cmd_keys_remove ───────────────────────────────────────────────────────
1498
1499    #[test]
1500    #[serial_test::serial]
1501    fn testa_cmd_keys_remove_indice_1_de_config_com_3_chaves() {
1502        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1503        // SAFETY: idem
1504        unsafe {
1505            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1506        }
1507
1508        escrever_config_xdg("ctx7sk-rem-alpha").expect("Deve escrever chave 1");
1509        escrever_config_xdg("ctx7sk-rem-beta").expect("Deve escrever chave 2");
1510        escrever_config_xdg("ctx7sk-rem-gamma").expect("Deve escrever chave 3");
1511
1512        cmd_keys_remove(1).expect("Remove índice 1 deve funcionar");
1513
1514        let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1515        let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1516
1517        unsafe {
1518            std::env::remove_var("XDG_CONFIG_HOME");
1519        }
1520
1521        assert_eq!(config.keys.len(), 2, "Devem sobrar 2 chaves após remoção");
1522        assert_eq!(config.keys[0].value, "ctx7sk-rem-beta");
1523        assert_eq!(config.keys[1].value, "ctx7sk-rem-gamma");
1524    }
1525
1526    #[test]
1527    #[serial_test::serial]
1528    fn testa_cmd_keys_remove_indice_2_de_config_com_3_chaves() {
1529        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1530        // SAFETY: idem
1531        unsafe {
1532            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1533        }
1534
1535        escrever_config_xdg("ctx7sk-mid-alpha").expect("Deve escrever chave 1");
1536        escrever_config_xdg("ctx7sk-mid-beta").expect("Deve escrever chave 2");
1537        escrever_config_xdg("ctx7sk-mid-gamma").expect("Deve escrever chave 3");
1538
1539        cmd_keys_remove(2).expect("Remove índice 2 deve funcionar");
1540
1541        let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1542        let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config");
1543
1544        unsafe {
1545            std::env::remove_var("XDG_CONFIG_HOME");
1546        }
1547
1548        assert_eq!(
1549            config.keys.len(),
1550            2,
1551            "Devem sobrar 2 chaves após remoção da do meio"
1552        );
1553        assert_eq!(config.keys[0].value, "ctx7sk-mid-alpha");
1554        assert_eq!(config.keys[1].value, "ctx7sk-mid-gamma");
1555    }
1556
1557    #[test]
1558    #[serial_test::serial]
1559    fn testa_cmd_keys_remove_indice_zero_retorna_ok_com_mensagem() {
1560        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1561        // SAFETY: idem
1562        unsafe {
1563            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1564        }
1565
1566        escrever_config_xdg("ctx7sk-idx-zero-test").expect("Deve escrever chave");
1567
1568        let resultado = cmd_keys_remove(0);
1569
1570        unsafe {
1571            std::env::remove_var("XDG_CONFIG_HOME");
1572        }
1573
1574        assert!(
1575            resultado.is_ok(),
1576            "Índice 0 inválido deve retornar Ok (não Err), obteve: {:?}",
1577            resultado
1578        );
1579    }
1580
1581    #[test]
1582    #[serial_test::serial]
1583    fn testa_cmd_keys_remove_indice_maior_que_len_retorna_ok_com_mensagem() {
1584        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1585        // SAFETY: idem
1586        unsafe {
1587            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1588        }
1589
1590        escrever_config_xdg("ctx7sk-overflow-test").expect("Deve escrever chave");
1591
1592        let resultado = cmd_keys_remove(99);
1593
1594        unsafe {
1595            std::env::remove_var("XDG_CONFIG_HOME");
1596        }
1597
1598        assert!(
1599            resultado.is_ok(),
1600            "Índice fora do range deve retornar Ok (não Err), obteve: {:?}",
1601            resultado
1602        );
1603    }
1604
1605    #[test]
1606    #[serial_test::serial]
1607    fn testa_cmd_keys_remove_em_config_vazio_retorna_ok() {
1608        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1609        // SAFETY: idem
1610        unsafe {
1611            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1612        }
1613
1614        let resultado = cmd_keys_remove(1);
1615
1616        unsafe {
1617            std::env::remove_var("XDG_CONFIG_HOME");
1618        }
1619
1620        assert!(
1621            resultado.is_ok(),
1622            "Remover de config vazio deve retornar Ok, obteve: {:?}",
1623            resultado
1624        );
1625    }
1626
1627    // ── cmd_keys_clear ────────────────────────────────────────────────────────
1628
1629    #[test]
1630    #[serial_test::serial]
1631    fn testa_cmd_keys_clear_com_yes_true_limpa_todas_as_chaves() {
1632        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1633        // SAFETY: idem
1634        unsafe {
1635            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1636        }
1637
1638        escrever_config_xdg("ctx7sk-clear-alpha").expect("Deve escrever chave 1");
1639        escrever_config_xdg("ctx7sk-clear-beta").expect("Deve escrever chave 2");
1640
1641        let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1642        let antes = ler_config_toml_do_caminho(&caminho).expect("Deve ler config antes");
1643        assert_eq!(antes.keys.len(), 2, "Pré-condição: 2 chaves antes do clear");
1644
1645        cmd_keys_clear(true).expect("clear com yes=true deve funcionar");
1646
1647        let depois = ler_config_toml_do_caminho(&caminho).expect("Deve ler config depois");
1648
1649        unsafe {
1650            std::env::remove_var("XDG_CONFIG_HOME");
1651        }
1652
1653        assert!(
1654            depois.keys.is_empty(),
1655            "Após clear com yes=true, chaves devem estar vazias"
1656        );
1657    }
1658
1659    #[test]
1660    #[serial_test::serial]
1661    fn testa_cmd_keys_clear_com_yes_true_em_config_inexistente_funciona() {
1662        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1663        // SAFETY: idem
1664        unsafe {
1665            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1666        }
1667
1668        let resultado = cmd_keys_clear(true);
1669
1670        unsafe {
1671            std::env::remove_var("XDG_CONFIG_HOME");
1672        }
1673
1674        assert!(
1675            resultado.is_ok(),
1676            "clear em config inexistente deve retornar Ok (idempotente), obteve: {:?}",
1677            resultado
1678        );
1679    }
1680
1681    // ── cmd_keys_import ───────────────────────────────────────────────────────
1682
1683    #[test]
1684    #[serial_test::serial]
1685    fn testa_cmd_keys_import_env_valido_com_multiplas_chaves() {
1686        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1687        let arquivo_env = dir_temp.path().join("chaves.env");
1688        std::fs::write(
1689            &arquivo_env,
1690            "CONTEXT7_API=ctx7sk-import-alpha\nCONTEXT7_API=ctx7sk-import-beta\n",
1691        )
1692        .expect("Deve escrever .env de teste");
1693
1694        // SAFETY: idem
1695        unsafe {
1696            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1697        }
1698
1699        let resultado = cmd_keys_import(&arquivo_env);
1700
1701        let caminho = descobrir_caminho_config().expect("Deve ter caminho XDG");
1702        let config = ler_config_toml_do_caminho(&caminho).expect("Deve ler config após import");
1703
1704        unsafe {
1705            std::env::remove_var("XDG_CONFIG_HOME");
1706        }
1707
1708        resultado.expect("import de .env válido deve funcionar");
1709        assert_eq!(config.keys.len(), 2, "Deve ter importado 2 chaves");
1710
1711        let valores: Vec<&str> = config.keys.iter().map(|c| c.value.as_str()).collect();
1712        assert!(valores.contains(&"ctx7sk-import-alpha"));
1713        assert!(valores.contains(&"ctx7sk-import-beta"));
1714    }
1715
1716    #[test]
1717    #[serial_test::serial]
1718    fn testa_cmd_keys_import_env_sem_chaves_retorna_err() {
1719        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1720        let arquivo_env = dir_temp.path().join("vazio.env");
1721        std::fs::write(&arquivo_env, "# apenas comentario\nOUTRA_VAR=valor\n")
1722            .expect("Deve escrever .env sem chaves");
1723
1724        // SAFETY: idem
1725        unsafe {
1726            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1727        }
1728
1729        let resultado = cmd_keys_import(&arquivo_env);
1730
1731        unsafe {
1732            std::env::remove_var("XDG_CONFIG_HOME");
1733        }
1734
1735        assert!(
1736            resultado.is_err(),
1737            "Import de .env sem chaves CONTEXT7_API deve retornar Err"
1738        );
1739    }
1740
1741    #[test]
1742    #[serial_test::serial]
1743    fn testa_cmd_keys_import_arquivo_inexistente_retorna_err() {
1744        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1745        let arquivo_inexistente = dir_temp.path().join("nao_existe.env");
1746
1747        // SAFETY: idem
1748        unsafe {
1749            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1750        }
1751
1752        let resultado = cmd_keys_import(&arquivo_inexistente);
1753
1754        unsafe {
1755            std::env::remove_var("XDG_CONFIG_HOME");
1756        }
1757
1758        assert!(
1759            resultado.is_err(),
1760            "Import de arquivo inexistente deve retornar Err"
1761        );
1762    }
1763
1764    #[test]
1765    #[serial_test::serial]
1766    fn testa_cmd_keys_import_roundtrip_add_depois_list() {
1767        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1768        let arquivo_env = dir_temp.path().join("roundtrip.env");
1769        std::fs::write(
1770            &arquivo_env,
1771            "CONTEXT7_API=ctx7sk-rtrip-01\nCONTEXT7_API=ctx7sk-rtrip-02\n",
1772        )
1773        .expect("Deve escrever .env de roundtrip");
1774
1775        // SAFETY: idem
1776        unsafe {
1777            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1778        }
1779
1780        cmd_keys_import(&arquivo_env).expect("Import deve funcionar");
1781
1782        let config = ler_config_xdg_raw()
1783            .expect("Deve retornar Ok")
1784            .expect("Deve retornar Some após import");
1785
1786        unsafe {
1787            std::env::remove_var("XDG_CONFIG_HOME");
1788        }
1789
1790        assert_eq!(
1791            config.keys.len(),
1792            2,
1793            "Roundtrip: deve ter 2 chaves após import"
1794        );
1795        assert_eq!(config.keys[0].value, "ctx7sk-rtrip-01");
1796        assert_eq!(config.keys[1].value, "ctx7sk-rtrip-02");
1797    }
1798
1799    // ── cmd_keys_export ───────────────────────────────────────────────────────
1800
1801    #[test]
1802    #[serial_test::serial]
1803    fn testa_cmd_keys_export_em_config_vazio_retorna_ok() {
1804        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1805        // SAFETY: idem
1806        unsafe {
1807            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1808        }
1809
1810        let resultado = cmd_keys_export();
1811
1812        unsafe {
1813            std::env::remove_var("XDG_CONFIG_HOME");
1814        }
1815
1816        assert!(
1817            resultado.is_ok(),
1818            "Export de config vazio deve retornar Ok, obteve: {:?}",
1819            resultado
1820        );
1821    }
1822
1823    #[test]
1824    #[serial_test::serial]
1825    fn testa_cmd_keys_export_retorna_ok_com_chaves_existentes() {
1826        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1827        // SAFETY: idem
1828        unsafe {
1829            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1830        }
1831
1832        escrever_config_xdg("ctx7sk-export-um").expect("Deve escrever chave 1");
1833        escrever_config_xdg("ctx7sk-export-dois").expect("Deve escrever chave 2");
1834
1835        let resultado = cmd_keys_export();
1836
1837        unsafe {
1838            std::env::remove_var("XDG_CONFIG_HOME");
1839        }
1840
1841        assert!(
1842            resultado.is_ok(),
1843            "Export com chaves existentes deve retornar Ok, obteve: {:?}",
1844            resultado
1845        );
1846    }
1847
1848    // ── ler_config_xdg_raw ────────────────────────────────────────────────────
1849
1850    #[test]
1851    #[serial_test::serial]
1852    fn testa_ler_config_xdg_raw_retorna_none_sem_arquivo() {
1853        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1854        // SAFETY: idem
1855        unsafe {
1856            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1857        }
1858
1859        let resultado = ler_config_xdg_raw();
1860
1861        unsafe {
1862            std::env::remove_var("XDG_CONFIG_HOME");
1863        }
1864
1865        let valor = resultado.expect("Deve retornar Ok");
1866        assert!(
1867            valor.is_none(),
1868            "Deve retornar None quando config.toml não existe"
1869        );
1870    }
1871
1872    #[test]
1873    #[serial_test::serial]
1874    fn testa_ler_config_xdg_raw_retorna_config_com_chaves() {
1875        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1876        let dir_context7 = dir_temp.path().join("context7");
1877        std::fs::create_dir_all(&dir_context7).expect("Deve criar diretório context7");
1878
1879        let toml = r#"schema_version = 1
1880
1881[[keys]]
1882value = "ctx7sk-raw-01"
1883added_at = "2026-04-08T00:00:00+00:00"
1884
1885[[keys]]
1886value = "ctx7sk-raw-02"
1887added_at = "2026-04-08T00:01:00+00:00"
1888"#;
1889        std::fs::write(dir_context7.join("config.toml"), toml).expect("Deve escrever config.toml");
1890
1891        // SAFETY: idem
1892        unsafe {
1893            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1894        }
1895
1896        let resultado = ler_config_xdg_raw();
1897
1898        unsafe {
1899            std::env::remove_var("XDG_CONFIG_HOME");
1900        }
1901
1902        let config = resultado
1903            .expect("Deve retornar Ok")
1904            .expect("Deve retornar Some com config");
1905        assert_eq!(config.keys.len(), 2);
1906        assert_eq!(config.keys[0].value, "ctx7sk-raw-01");
1907        assert_eq!(config.keys[1].value, "ctx7sk-raw-02");
1908    }
1909
1910    #[test]
1911    #[serial_test::serial]
1912    fn testa_cmd_keys_path_retorna_ok() {
1913        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1914        // SAFETY: idem
1915        unsafe {
1916            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1917        }
1918
1919        let resultado = cmd_keys_path();
1920
1921        unsafe {
1922            std::env::remove_var("XDG_CONFIG_HOME");
1923        }
1924
1925        resultado.expect("cmd_keys_path deve retornar Ok");
1926    }
1927
1928    #[test]
1929    #[serial_test::serial]
1930    fn testa_descobrir_caminho_config_termina_com_config_toml() {
1931        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1932        // SAFETY: idem
1933        unsafe {
1934            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1935        }
1936
1937        let caminho = descobrir_caminho_config();
1938
1939        unsafe {
1940            std::env::remove_var("XDG_CONFIG_HOME");
1941        }
1942
1943        let caminho = caminho.expect("Deve retornar caminho XDG válido");
1944        assert!(
1945            caminho.to_string_lossy().ends_with("config.toml"),
1946            "Caminho deve terminar com config.toml, obteve: {}",
1947            caminho.display()
1948        );
1949        assert!(
1950            caminho.to_string_lossy().contains("context7"),
1951            "Caminho deve conter 'context7', obteve: {}",
1952            caminho.display()
1953        );
1954    }
1955
1956    // ── fluxo completo ────────────────────────────────────────────────────────
1957
1958    #[test]
1959    #[serial_test::serial]
1960    fn testa_fluxo_completo_add_list_remove_clear() {
1961        let dir_temp = tempfile::TempDir::new().expect("Deve criar diretório temporário");
1962        // SAFETY: idem
1963        unsafe {
1964            std::env::set_var("XDG_CONFIG_HOME", dir_temp.path());
1965        }
1966
1967        cmd_keys_add("ctx7sk-fluxo-01").expect("Add 1 deve funcionar");
1968        cmd_keys_add("ctx7sk-fluxo-02").expect("Add 2 deve funcionar");
1969        cmd_keys_add("ctx7sk-fluxo-03").expect("Add 3 deve funcionar");
1970
1971        let config_antes = ler_config_xdg_raw()
1972            .expect("Ok")
1973            .expect("Some com 3 chaves");
1974        assert_eq!(config_antes.keys.len(), 3, "Deve ter 3 chaves após 3 adds");
1975
1976        cmd_keys_remove(2).expect("Remove índice 2 deve funcionar");
1977
1978        let config_pos_remove = ler_config_xdg_raw()
1979            .expect("Ok")
1980            .expect("Some com 2 chaves");
1981        assert_eq!(
1982            config_pos_remove.keys.len(),
1983            2,
1984            "Deve ter 2 chaves após remove"
1985        );
1986        assert_eq!(config_pos_remove.keys[0].value, "ctx7sk-fluxo-01");
1987        assert_eq!(config_pos_remove.keys[1].value, "ctx7sk-fluxo-03");
1988
1989        cmd_keys_clear(true).expect("Clear com yes=true deve funcionar");
1990
1991        let caminho = descobrir_caminho_config().expect("Deve ter caminho");
1992        let config_final = ler_config_toml_do_caminho(&caminho).expect("Deve ler config final");
1993
1994        unsafe {
1995            std::env::remove_var("XDG_CONFIG_HOME");
1996        }
1997
1998        assert!(
1999            config_final.keys.is_empty(),
2000            "Após clear, chaves devem estar vazias"
2001        );
2002    }
2003}