use crate::errors::AppError;
use crate::i18n::validacao;
use chrono::{DateTime, TimeZone, Utc};
use chrono_tz::Tz;
use std::sync::OnceLock;
static FUSO_GLOBAL: OnceLock<Tz> = OnceLock::new();
fn resolver_tz_de_env() -> Result<Tz, AppError> {
match std::env::var("SQLITE_GRAPHRAG_DISPLAY_TZ") {
Ok(v) if !v.trim().is_empty() => v
.trim()
.parse::<Tz>()
.map_err(|_| AppError::Validation(validacao::tz_invalido(v.trim()))),
_ => Ok(Tz::UTC),
}
}
pub fn init(explicit: Option<Tz>) -> Result<(), AppError> {
let fuso = match explicit {
Some(tz) => tz,
None => resolver_tz_de_env()?,
};
let _ = FUSO_GLOBAL.set(fuso);
Ok(())
}
pub fn fuso_atual() -> Tz {
*FUSO_GLOBAL.get_or_init(|| resolver_tz_de_env().unwrap_or(Tz::UTC))
}
pub fn formatar_iso(ts: DateTime<Utc>) -> String {
let fuso = fuso_atual();
ts.with_timezone(&fuso)
.format("%Y-%m-%dT%H:%M:%S%:z")
.to_string()
}
pub fn epoch_para_iso(epoch: i64) -> String {
Utc.timestamp_opt(epoch, 0)
.single()
.map(formatar_iso)
.unwrap_or_else(|| "1970-01-01T00:00:00+00:00".to_string())
}
#[cfg(test)]
mod testes {
use super::*;
use serial_test::serial;
#[test]
#[serial]
fn utc_default_quando_env_ausente() {
std::env::remove_var("SQLITE_GRAPHRAG_DISPLAY_TZ");
let resultado = resolver_tz_de_env().expect("não deve falhar com env ausente");
assert_eq!(resultado, Tz::UTC);
}
#[test]
#[serial]
fn env_valido_aplica_timezone() {
std::env::set_var("SQLITE_GRAPHRAG_DISPLAY_TZ", "America/Sao_Paulo");
let resultado = resolver_tz_de_env().expect("America/Sao_Paulo é válido");
assert_eq!(resultado.name(), "America/Sao_Paulo");
std::env::remove_var("SQLITE_GRAPHRAG_DISPLAY_TZ");
}
#[test]
#[serial]
fn env_invalido_retorna_erro_validation() {
std::env::set_var("SQLITE_GRAPHRAG_DISPLAY_TZ", "Invalido/Naoexiste");
let resultado = resolver_tz_de_env();
assert!(resultado.is_err(), "timezone inválida deve retornar Err");
match resultado {
Err(AppError::Validation(msg)) => {
assert!(
msg.contains("SQLITE_GRAPHRAG_DISPLAY_TZ"),
"mensagem deve citar a env var"
);
assert!(
msg.contains("Invalido/Naoexiste"),
"mensagem deve citar o valor inválido"
);
}
other => panic!("esperado AppError::Validation, obtido: {other:?}"),
}
std::env::remove_var("SQLITE_GRAPHRAG_DISPLAY_TZ");
}
#[test]
fn epoch_zero_gera_utc_iso() {
std::env::remove_var("SQLITE_GRAPHRAG_DISPLAY_TZ");
let resultado = {
let tz = Tz::UTC;
Utc.timestamp_opt(0, 0)
.single()
.map(|dt| {
dt.with_timezone(&tz)
.format("%Y-%m-%dT%H:%M:%S%:z")
.to_string()
})
.unwrap_or_else(|| "1970-01-01T00:00:00+00:00".to_string())
};
assert_eq!(resultado, "1970-01-01T00:00:00+00:00");
}
#[test]
fn formatar_iso_utc_preserva_offset_zero() {
let ts = Utc.timestamp_opt(1_705_320_000, 0).single().unwrap();
let resultado = ts
.with_timezone(&Tz::UTC)
.format("%Y-%m-%dT%H:%M:%S%:z")
.to_string();
assert_eq!(resultado, "2024-01-15T12:00:00+00:00");
}
#[test]
fn formatar_iso_sao_paulo_aplica_offset() {
let ts = Utc.timestamp_opt(1_705_320_000, 0).single().unwrap();
let sao_paulo: Tz = "America/Sao_Paulo".parse().unwrap();
let resultado = ts
.with_timezone(&sao_paulo)
.format("%Y-%m-%dT%H:%M:%S%:z")
.to_string();
assert!(
resultado.contains("-03:00"),
"esperado offset -03:00, obtido: {resultado}"
);
}
}