Skip to main content

context7_cli/
storage.rs

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