1use crate::errors::AppError;
13use crate::i18n::validacao;
14use chrono::{DateTime, TimeZone, Utc};
15use chrono_tz::Tz;
16use std::sync::OnceLock;
17
18static FUSO_GLOBAL: OnceLock<Tz> = OnceLock::new();
19
20fn resolver_tz_de_env() -> Result<Tz, AppError> {
25 match std::env::var("SQLITE_GRAPHRAG_DISPLAY_TZ") {
26 Ok(v) if !v.trim().is_empty() => v
27 .trim()
28 .parse::<Tz>()
29 .map_err(|_| AppError::Validation(validacao::tz_invalido(v.trim()))),
30 _ => Ok(Tz::UTC),
31 }
32}
33
34pub fn init(explicit: Option<Tz>) -> Result<(), AppError> {
42 let fuso = match explicit {
43 Some(tz) => tz,
44 None => resolver_tz_de_env()?,
45 };
46 let _ = FUSO_GLOBAL.set(fuso);
47 Ok(())
48}
49
50pub fn fuso_atual() -> Tz {
54 *FUSO_GLOBAL.get_or_init(|| resolver_tz_de_env().unwrap_or(Tz::UTC))
55}
56
57pub fn formatar_iso(ts: DateTime<Utc>) -> String {
62 let fuso = fuso_atual();
63 ts.with_timezone(&fuso)
64 .format("%Y-%m-%dT%H:%M:%S%:z")
65 .to_string()
66}
67
68pub fn epoch_para_iso(epoch: i64) -> String {
73 Utc.timestamp_opt(epoch, 0)
74 .single()
75 .map(formatar_iso)
76 .unwrap_or_else(|| "1970-01-01T00:00:00+00:00".to_string())
77}
78
79#[cfg(test)]
80mod testes {
81 use super::*;
82 use serial_test::serial;
83
84 #[test]
85 #[serial]
86 fn utc_default_quando_env_ausente() {
87 std::env::remove_var("SQLITE_GRAPHRAG_DISPLAY_TZ");
89 let resultado = resolver_tz_de_env().expect("não deve falhar com env ausente");
90 assert_eq!(resultado, Tz::UTC);
91 }
92
93 #[test]
94 #[serial]
95 fn env_valido_aplica_timezone() {
96 std::env::set_var("SQLITE_GRAPHRAG_DISPLAY_TZ", "America/Sao_Paulo");
97 let resultado = resolver_tz_de_env().expect("America/Sao_Paulo é válido");
98 assert_eq!(resultado.name(), "America/Sao_Paulo");
99 std::env::remove_var("SQLITE_GRAPHRAG_DISPLAY_TZ");
100 }
101
102 #[test]
103 #[serial]
104 fn env_invalido_retorna_erro_validation() {
105 std::env::set_var("SQLITE_GRAPHRAG_DISPLAY_TZ", "Invalido/Naoexiste");
106 let resultado = resolver_tz_de_env();
107 assert!(resultado.is_err(), "timezone inválida deve retornar Err");
108 match resultado {
109 Err(AppError::Validation(msg)) => {
110 assert!(
111 msg.contains("SQLITE_GRAPHRAG_DISPLAY_TZ"),
112 "mensagem deve citar a env var"
113 );
114 assert!(
115 msg.contains("Invalido/Naoexiste"),
116 "mensagem deve citar o valor inválido"
117 );
118 }
119 other => panic!("esperado AppError::Validation, obtido: {other:?}"),
120 }
121 std::env::remove_var("SQLITE_GRAPHRAG_DISPLAY_TZ");
122 }
123
124 #[test]
125 fn epoch_zero_gera_utc_iso() {
126 std::env::remove_var("SQLITE_GRAPHRAG_DISPLAY_TZ");
128 let resultado = {
129 let tz = Tz::UTC;
131 Utc.timestamp_opt(0, 0)
132 .single()
133 .map(|dt| {
134 dt.with_timezone(&tz)
135 .format("%Y-%m-%dT%H:%M:%S%:z")
136 .to_string()
137 })
138 .unwrap_or_else(|| "1970-01-01T00:00:00+00:00".to_string())
139 };
140 assert_eq!(resultado, "1970-01-01T00:00:00+00:00");
141 }
142
143 #[test]
144 fn formatar_iso_utc_preserva_offset_zero() {
145 let ts = Utc.timestamp_opt(1_705_320_000, 0).single().unwrap();
146 let resultado = ts
148 .with_timezone(&Tz::UTC)
149 .format("%Y-%m-%dT%H:%M:%S%:z")
150 .to_string();
151 assert_eq!(resultado, "2024-01-15T12:00:00+00:00");
152 }
153
154 #[test]
155 fn formatar_iso_sao_paulo_aplica_offset() {
156 let ts = Utc.timestamp_opt(1_705_320_000, 0).single().unwrap();
157 let sao_paulo: Tz = "America/Sao_Paulo".parse().unwrap();
158 let resultado = ts
159 .with_timezone(&sao_paulo)
160 .format("%Y-%m-%dT%H:%M:%S%:z")
161 .to_string();
162 assert!(
164 resultado.contains("-03:00"),
165 "esperado offset -03:00, obtido: {resultado}"
166 );
167 }
168}