//! Safe Rust client API for GemStone/S over GCI.
//!
//! This crate builds on `gemstone-gci`, which owns the dynamic `libgcirpc`
//! loading and raw ABI calls. `Session` provides RAII login/logout behavior,
//! explicit OOP values, conservative transaction helpers, and basic value
//! marshalling for nil, booleans, small integers, characters, and raw OOPs.

pub mod browser;
pub mod codegen;

pub use gemstone_gci::Oop;
use gemstone_gci::{
    char_from_oop, is_char, is_smallint, GciErrSType, GciLibrary, RawOop, GCI_ENCRYPT_BUF_SIZE,
    GCI_INVALID_SESSION,
};
use std::cell::Cell;
use std::env;
use std::error::Error as StdError;
use std::ffi::{c_char, c_double, c_uint, CString, NulError};
use std::fmt;
use std::marker::PhantomData;
use std::path::PathBuf;
use std::rc::Rc;

pub type Result<T> = std::result::Result<T, Error>;

#[derive(Debug)]
pub enum Error {
    Gci(gemstone_gci::GciError),
    MissingEnvironment(&'static str),
    MissingConfig(&'static str),
    Nul(NulError),
    NotLoggedIn,
    GemStone {
        number: i32,
        fatal: bool,
        message: String,
    },
    IllegalOop {
        operation: &'static str,
    },
    NegativeSize(i64),
    ArgumentCountTooLarge(usize),
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Gci(err) => write!(f, "{err}"),
            Self::MissingEnvironment(name) => {
                write!(f, "missing required GemStone environment variable {name}")
            }
            Self::MissingConfig(name) => write!(f, "missing required GemStone config field {name}"),
            Self::Nul(err) => write!(f, "string contains NUL byte: {err}"),
            Self::NotLoggedIn => write!(f, "GemStone session is not logged in"),
            Self::GemStone {
                number,
                fatal,
                message,
            } => write!(f, "GemStone error #{number} fatal={fatal}: {message}"),
            Self::IllegalOop { operation } => {
                write!(f, "GemStone operation {operation} returned OOP_ILLEGAL")
            }
            Self::NegativeSize(size) => write!(f, "GemStone returned a negative size: {size}"),
            Self::ArgumentCountTooLarge(count) => {
                write!(f, "too many GemStone selector arguments: {count}")
            }
        }
    }
}

impl StdError for Error {
    fn source(&self) -> Option<&(dyn StdError + 'static)> {
        match self {
            Self::Gci(err) => Some(err),
            Self::Nul(err) => Some(err),
            Self::MissingEnvironment(_)
            | Self::MissingConfig(_)
            | Self::NotLoggedIn
            | Self::GemStone { .. }
            | Self::IllegalOop { .. }
            | Self::NegativeSize(_)
            | Self::ArgumentCountTooLarge(_) => None,
        }
    }
}

impl From<gemstone_gci::GciError> for Error {
    fn from(value: gemstone_gci::GciError) -> Self {
        Self::Gci(value)
    }
}

impl From<NulError> for Error {
    fn from(value: NulError) -> Self {
        Self::Nul(value)
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Config {
    pub stone: String,
    pub netldi: String,
    pub host: String,
    pub username: String,
    pub password: String,
    pub host_username: String,
    pub host_password: String,
    pub gem_service: String,
    pub lib_path: Option<PathBuf>,
}

impl Config {
    pub fn builder() -> ConfigBuilder {
        ConfigBuilder::default()
    }

    pub fn from_env() -> Result<Self> {
        Self::builder()
            .stone(
                env::var("GS_STONE")
                    .or_else(|_| env::var("GS_STONE_NAME"))
                    .unwrap_or_else(|_| "gs64stone".to_string()),
            )
            .netldi(env::var("GS_NETLDI").unwrap_or_else(|_| "netldi".to_string()))
            .host(env::var("GS_HOST").unwrap_or_else(|_| "localhost".to_string()))
            .username(required_env("GS_USERNAME")?)
            .password(required_env("GS_PASSWORD")?)
            .host_username(env::var("GS_HOST_USERNAME").unwrap_or_default())
            .host_password(env::var("GS_HOST_PASSWORD").unwrap_or_default())
            .gem_service(env::var("GS_GEM_SERVICE").unwrap_or_else(|_| "gemnetobject".to_string()))
            .maybe_lib_path(
                env::var("GS_LIB_PATH")
                    .ok()
                    .filter(|value| !value.is_empty()),
            )
            .build()
    }

    pub fn stone_nrs(&self) -> String {
        if !self.host.is_empty() && self.host != "localhost" && self.host != "127.0.0.1" {
            format!("!@{}!{}!{}", self.host, self.netldi, self.stone)
        } else {
            self.stone.clone()
        }
    }
}

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ConfigBuilder {
    stone: Option<String>,
    netldi: Option<String>,
    host: Option<String>,
    username: Option<String>,
    password: Option<String>,
    host_username: Option<String>,
    host_password: Option<String>,
    gem_service: Option<String>,
    lib_path: Option<PathBuf>,
}

impl ConfigBuilder {
    pub fn stone(mut self, value: impl Into<String>) -> Self {
        self.stone = Some(value.into());
        self
    }

    pub fn netldi(mut self, value: impl Into<String>) -> Self {
        self.netldi = Some(value.into());
        self
    }

    pub fn host(mut self, value: impl Into<String>) -> Self {
        self.host = Some(value.into());
        self
    }

    pub fn username(mut self, value: impl Into<String>) -> Self {
        self.username = Some(value.into());
        self
    }

    pub fn password(mut self, value: impl Into<String>) -> Self {
        self.password = Some(value.into());
        self
    }

    pub fn host_username(mut self, value: impl Into<String>) -> Self {
        self.host_username = Some(value.into());
        self
    }

    pub fn host_password(mut self, value: impl Into<String>) -> Self {
        self.host_password = Some(value.into());
        self
    }

    pub fn gem_service(mut self, value: impl Into<String>) -> Self {
        self.gem_service = Some(value.into());
        self
    }

    pub fn lib_path(mut self, value: impl Into<PathBuf>) -> Self {
        self.lib_path = Some(value.into());
        self
    }

    pub fn maybe_lib_path(mut self, value: Option<impl Into<PathBuf>>) -> Self {
        self.lib_path = value.map(Into::into);
        self
    }

    pub fn build(self) -> Result<Config> {
        let username = required_config("username", self.username)?;
        let password = required_config("password", self.password)?;

        Ok(Config {
            stone: self.stone.unwrap_or_else(|| "gs64stone".to_string()),
            netldi: self.netldi.unwrap_or_else(|| "netldi".to_string()),
            host: self.host.unwrap_or_else(|| "localhost".to_string()),
            username,
            password,
            host_username: self.host_username.unwrap_or_default(),
            host_password: self.host_password.unwrap_or_default(),
            gem_service: self
                .gem_service
                .unwrap_or_else(|| "gemnetobject".to_string()),
            lib_path: self.lib_path,
        })
    }
}

impl Default for Config {
    fn default() -> Self {
        Self {
            stone: "gs64stone".to_string(),
            netldi: "netldi".to_string(),
            host: "localhost".to_string(),
            username: String::new(),
            password: String::new(),
            host_username: String::new(),
            host_password: String::new(),
            gem_service: "gemnetobject".to_string(),
            lib_path: None,
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TransactionPolicy {
    Manual,
    CommitOnSuccess,
    AbortOnExit,
}

#[derive(Clone, Debug, PartialEq)]
pub enum Value {
    Nil,
    Bool(bool),
    SmallInt(i64),
    Char(char),
    String(String),
    Oop(Oop),
}

impl Value {
    pub fn as_oop(&self) -> Option<Oop> {
        match self {
            Self::Oop(oop) => Some(*oop),
            _ => None,
        }
    }
}

pub struct Session {
    lib: GciLibrary,
    session_id: i32,
    logged_in: Cell<bool>,
    _not_send_sync: PhantomData<Rc<()>>,
}

impl Session {
    pub fn login(config: Config) -> Result<Self> {
        if config.username.is_empty() {
            return Err(Error::MissingEnvironment("GS_USERNAME"));
        }
        if config.password.is_empty() {
            return Err(Error::MissingEnvironment("GS_PASSWORD"));
        }

        let lib = GciLibrary::load(config.lib_path.clone())?;
        let mut session = Self {
            lib,
            session_id: GCI_INVALID_SESSION as i32,
            logged_in: Cell::new(false),
            _not_send_sync: PhantomData,
        };
        session.open(config)?;
        Ok(session)
    }

    pub fn session_id(&self) -> i32 {
        self.session_id
    }

    pub fn is_logged_in(&self) -> bool {
        self.logged_in.get()
    }

    pub fn eval(&mut self, source: &str) -> Result<Value> {
        let oop = self.eval_oop(source)?;
        self.marshal(oop)
    }

    pub fn execute(&mut self, source: &str) -> Result<Oop> {
        self.eval_oop(source)
    }

    pub fn eval_oop(&mut self, source: &str) -> Result<Oop> {
        self.activate()?;
        let source = CString::new(source)?;
        let oop = unsafe { self.lib.gci_execute_str(&source, OOP_NIL)? };
        self.check_oop("eval", oop)?;
        Ok(Oop(oop))
    }

    pub fn perform(&mut self, receiver: Oop, selector: &str, args: &[Oop]) -> Result<Value> {
        let oop = self.perform_oop(receiver, selector, args)?;
        self.marshal(oop)
    }

    pub fn perform_oop(&mut self, receiver: Oop, selector: &str, args: &[Oop]) -> Result<Oop> {
        self.activate()?;
        let selector = CString::new(selector)?;
        if args.len() > i32::MAX as usize {
            return Err(Error::ArgumentCountTooLarge(args.len()));
        }
        let raw_args: Vec<RawOop> = args.iter().map(|oop| oop.raw()).collect();
        let oop = unsafe {
            self.lib.gci_perform(
                receiver.raw(),
                &selector,
                raw_args.as_ptr(),
                raw_args.len() as i32,
            )?
        };
        self.check_oop("perform", oop)?;
        Ok(Oop(oop))
    }

    pub fn resolve(&mut self, name: &str) -> Result<Oop> {
        self.activate()?;
        let name = CString::new(name)?;
        let oop = unsafe { self.lib.gci_resolve_symbol(&name, OOP_NIL)? };
        self.check_oop("resolve", oop)?;
        Ok(Oop(oop))
    }

    pub fn new_string(&mut self, value: &str) -> Result<Oop> {
        self.activate()?;
        let value = CString::new(value)?;
        let oop = unsafe { self.lib.gci_new_string(&value)? };
        self.check_oop("new_string", oop)?;
        Ok(Oop(oop))
    }

    pub fn new_symbol(&mut self, value: &str) -> Result<Oop> {
        self.activate()?;
        let value = CString::new(value)?;
        let oop = unsafe { self.lib.gci_new_symbol(&value)? };
        self.check_oop("new_symbol", oop)?;
        Ok(Oop(oop))
    }

    pub fn new_object(&mut self, class_oop: Oop) -> Result<Oop> {
        self.activate()?;
        let oop = unsafe { self.lib.gci_new_oop(class_oop.raw())? };
        self.check_oop("new_object", oop)?;
        Ok(Oop(oop))
    }

    pub fn smallint_oop(&self, value: i64) -> Oop {
        Oop::from_smallint(value)
    }

    pub fn bool_oop(&self, value: bool) -> Oop {
        Oop::from_bool(value)
    }

    pub fn nil_oop(&self) -> Oop {
        Oop::NIL
    }

    pub fn float_oop(&mut self, value: f64) -> Result<Oop> {
        self.activate()?;
        let oop = unsafe { self.lib.gci_flt_to_oop(value as c_double)? };
        if oop == OOP_ILLEGAL || oop == OOP_NIL {
            return Err(Error::IllegalOop {
                operation: "float_oop",
            });
        }
        Ok(Oop(oop))
    }

    pub fn try_oop_to_float(&mut self, oop: Oop) -> Result<Option<f64>> {
        self.activate()?;
        let mut value: c_double = 0.0;
        let ok = unsafe {
            self.lib
                .gci_oop_to_flt(oop.raw(), &mut value as *mut c_double)?
        };
        Ok((ok != 0).then_some(value as f64))
    }

    pub fn value_to_oop(&mut self, value: &Value) -> Result<Oop> {
        match value {
            Value::Nil => Ok(Oop::NIL),
            Value::Bool(value) => Ok(Oop::from_bool(*value)),
            Value::SmallInt(value) => Ok(Oop::from_smallint(*value)),
            Value::Char(value) => Ok(Oop::from_char(*value)),
            Value::String(value) => self.new_string(value),
            Value::Oop(oop) => Ok(*oop),
        }
    }

    pub fn fetch_size(&mut self, oop: Oop) -> Result<i64> {
        self.activate()?;
        Ok(unsafe { self.lib.gci_fetch_size(oop.raw())? })
    }

    pub fn fetch_string(&mut self, oop: Oop) -> Result<String> {
        let size = self.fetch_size(oop)?;
        if size < 0 {
            return Err(Error::NegativeSize(size));
        }
        if size == 0 {
            return Ok(String::new());
        }

        let mut buffer = vec![0 as c_char; size as usize + 1];
        self.activate()?;
        let fetched = unsafe {
            self.lib
                .gci_fetch_bytes(oop.raw(), 1, buffer.as_mut_ptr(), size)?
        };
        if fetched < 0 {
            return Err(Error::NegativeSize(fetched));
        }
        let bytes: Vec<u8> = buffer
            .iter()
            .take(fetched as usize)
            .map(|byte| *byte as u8)
            .collect();
        Ok(String::from_utf8_lossy(&bytes).into_owned())
    }

    pub fn fetch_class(&mut self, oop: Oop) -> Result<Oop> {
        self.activate()?;
        let class_oop = unsafe { self.lib.gci_fetch_class(oop.raw())? };
        self.check_oop("fetch_class", class_oop)?;
        Ok(Oop(class_oop))
    }

    pub fn global_get(&mut self, symbol_name: &str) -> Result<Oop> {
        let user_globals = self.resolve("UserGlobals")?;
        let key = CString::new(symbol_name)?;
        let mut value = OOP_ILLEGAL;
        let mut assoc = OOP_ILLEGAL;
        self.activate()?;
        unsafe {
            self.lib.gci_sym_dict_at(
                user_globals.raw(),
                &key,
                &mut value as *mut RawOop,
                &mut assoc as *mut RawOop,
            )?;
        }
        self.check_oop("global_get", value)?;
        Ok(Oop(value))
    }

    pub fn global_put(&mut self, symbol_name: &str, value: Oop) -> Result<()> {
        let user_globals = self.resolve("UserGlobals")?;
        let key = self.new_symbol(symbol_name)?;
        self.activate()?;
        unsafe {
            self.lib
                .gci_sym_dict_at_obj_put(user_globals.raw(), key.raw(), value.raw())?;
        }
        Ok(())
    }

    pub fn str_dict_get(&mut self, dict: Oop, key: &str) -> Result<Value> {
        let key = CString::new(key)?;
        let mut value = OOP_ILLEGAL;
        self.activate()?;
        unsafe {
            self.lib
                .gci_str_key_value_dict_at(dict.raw(), &key, &mut value as *mut RawOop)?;
        }
        self.check_oop("str_dict_get", value)?;
        self.marshal(Oop(value))
    }

    pub fn str_dict_put(&mut self, dict: Oop, key: &str, value: Oop) -> Result<()> {
        let key = CString::new(key)?;
        self.activate()?;
        unsafe {
            self.lib
                .gci_str_key_value_dict_at_put(dict.raw(), &key, value.raw())?;
        }
        Ok(())
    }

    pub fn retain_oop(&mut self, oop: Oop) -> Result<OopHandle<'_>> {
        self.add_to_export_set(oop)?;
        Ok(OopHandle {
            session: self,
            oop,
            released: false,
        })
    }

    pub fn add_to_export_set(&mut self, oop: Oop) -> Result<()> {
        self.activate()?;
        unsafe {
            if !self
                .lib
                .call_optional_oop_export(b"GciAddOopToExportSet", oop.raw())?
            {
                self.lib
                    .call_optional_oop_export(b"GciAddObjToExportSet", oop.raw())?;
            }
        }
        Ok(())
    }

    pub fn remove_from_export_set(&mut self, oop: Oop) -> Result<()> {
        if !self.logged_in.get() {
            return Ok(());
        }
        self.activate()?;
        unsafe {
            if !self
                .lib
                .call_optional_oop_export(b"GciRemoveOopFromExportSet", oop.raw())?
            {
                self.lib
                    .call_optional_oop_export(b"GciRemoveObjFromExportSet", oop.raw())?;
            }
        }
        Ok(())
    }

    pub fn needs_commit(&mut self) -> Result<bool> {
        self.activate()?;
        Ok(unsafe { self.lib.gci_needs_commit()? != 0 })
    }

    pub fn in_transaction(&mut self) -> Result<bool> {
        self.activate()?;
        Ok(unsafe { self.lib.gci_in_transaction()? != 0 })
    }

    pub fn commit(&mut self) -> Result<()> {
        self.activate()?;
        let mut err = GciErrSType::default();
        let ok = unsafe { self.lib.gci_commit(&mut err)? };
        if ok == 0 {
            return Err(gemstone_error(err));
        }
        Ok(())
    }

    pub fn abort(&mut self) -> Result<()> {
        self.activate()?;
        let mut err = GciErrSType::default();
        let ok = unsafe { self.lib.gci_abort(&mut err)? };
        if ok == 0 && err.number != 0 {
            return Err(gemstone_error(err));
        }
        Ok(())
    }

    pub fn transaction<T>(&mut self, body: impl FnOnce(&mut Session) -> Result<T>) -> Result<T> {
        match body(self) {
            Ok(value) => {
                self.commit()?;
                Ok(value)
            }
            Err(err) => {
                let _ = self.abort();
                Err(err)
            }
        }
    }

    pub fn logout(&mut self) -> Result<()> {
        if !self.logged_in.get() {
            return Ok(());
        }
        unsafe {
            self.lib.gci_set_session_id(self.session_id)?;
            self.lib.gci_logout()?;
        }
        self.logged_in.set(false);
        self.session_id = GCI_INVALID_SESSION as i32;
        Ok(())
    }

    fn open(&mut self, config: Config) -> Result<()> {
        unsafe {
            self.lib.gci_init()?;
        }

        let stone_nrs = CString::new(config.stone_nrs())?;
        let host_username = CString::new(config.host_username)?;
        let host_password = CString::new(config.host_password)?;
        let gem_service = CString::new(config.gem_service)?;
        let username = CString::new(config.username)?;
        let password = CString::new(config.password)?;

        let mut encrypted_host_password = vec![0 as c_char; GCI_ENCRYPT_BUF_SIZE];
        unsafe {
            self.lib.gci_encrypt(
                &host_password,
                encrypted_host_password.as_mut_ptr(),
                GCI_ENCRYPT_BUF_SIZE as c_uint,
            )?;
            self.lib.gci_set_net(
                &stone_nrs,
                &host_username,
                encrypted_host_password.as_ptr(),
                &gem_service,
            )?;
            let ok = self.lib.gci_login_ex(&username, &password, 0, 0)?;
            if ok == 0 {
                let mut err = GciErrSType::default();
                self.lib.gci_err(&mut err)?;
                return Err(gemstone_error(err));
            }
            self.session_id = self.lib.gci_get_session_id()?;
        }
        self.logged_in.set(true);
        Ok(())
    }

    fn activate(&self) -> Result<()> {
        if !self.logged_in.get() {
            return Err(Error::NotLoggedIn);
        }
        unsafe {
            self.lib.gci_set_session_id(self.session_id)?;
        }
        Ok(())
    }

    fn marshal(&self, oop: Oop) -> Result<Value> {
        match oop.raw() {
            OOP_NIL => Ok(Value::Nil),
            OOP_TRUE => Ok(Value::Bool(true)),
            OOP_FALSE => Ok(Value::Bool(false)),
            raw if is_smallint(raw) => Ok(Value::SmallInt(gemstone_gci::smallint_to_i64(raw))),
            raw if is_char(raw) => Ok(Value::Char(char_from_oop(raw)?)),
            _ => Ok(Value::Oop(oop)),
        }
    }

    fn check_oop(&self, operation: &'static str, oop: RawOop) -> Result<()> {
        if oop == OOP_ILLEGAL {
            Err(Error::IllegalOop { operation })
        } else {
            Ok(())
        }
    }
}

impl Drop for Session {
    fn drop(&mut self) {
        let _ = self.logout();
    }
}

pub use gemstone_gci::{OOP_FALSE, OOP_ILLEGAL, OOP_NIL, OOP_TRUE};

pub struct OopHandle<'a> {
    session: &'a mut Session,
    oop: Oop,
    released: bool,
}

impl<'a> OopHandle<'a> {
    pub fn oop(&self) -> Oop {
        self.oop
    }

    pub fn release(mut self) -> Result<()> {
        self.release_now()
    }

    fn release_now(&mut self) -> Result<()> {
        if self.released {
            return Ok(());
        }
        self.session.remove_from_export_set(self.oop)?;
        self.released = true;
        Ok(())
    }
}

impl Drop for OopHandle<'_> {
    fn drop(&mut self) {
        let _ = self.release_now();
    }
}

fn required_env(name: &'static str) -> Result<String> {
    env::var(name)
        .ok()
        .filter(|value| !value.is_empty())
        .ok_or(Error::MissingEnvironment(name))
}

fn required_config(name: &'static str, value: Option<String>) -> Result<String> {
    value
        .filter(|value| !value.is_empty())
        .ok_or(Error::MissingConfig(name))
}

fn gemstone_error(err: GciErrSType) -> Error {
    Error::GemStone {
        number: err.number,
        fatal: err.fatal != 0,
        message: err.full_message(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn config_formats_remote_stone_nrs() {
        let config = Config {
            stone: "seaside".to_string(),
            netldi: "netldi".to_string(),
            host: "db.example.test".to_string(),
            username: "DataCurator".to_string(),
            password: "swordfish".to_string(),
            ..Config::default()
        };

        assert_eq!(config.stone_nrs(), "!@db.example.test!netldi!seaside");
    }

    #[test]
    fn config_builder_applies_defaults_and_required_fields() -> Result<()> {
        let config = Config::builder()
            .username("DataCurator")
            .password("swordfish")
            .stone("seaside")
            .build()?;

        assert_eq!(config.stone, "seaside");
        assert_eq!(config.host, "localhost");
        assert_eq!(config.netldi, "netldi");
        assert_eq!(config.gem_service, "gemnetobject");
        Ok(())
    }

    #[test]
    fn config_builder_requires_credentials() {
        let err = Config::builder().password("swordfish").build().unwrap_err();
        assert!(matches!(err, Error::MissingConfig("username")));
    }

    #[test]
    fn smallint_values_marshal_without_a_live_session() {
        let value = Value::SmallInt(gemstone_gci::smallint_to_i64(
            gemstone_gci::i64_to_smallint(7),
        ));
        assert_eq!(value, Value::SmallInt(7));
    }

    #[test]
    fn value_oop_accessors_are_explicit() {
        let oop = Oop::from_smallint(7);
        assert_eq!(Value::Oop(oop).as_oop(), Some(oop));
        assert_eq!(Value::SmallInt(7).as_oop(), None);
        assert_eq!(Oop::from_bool(true).as_bool(), Some(true));
        assert_eq!(Oop::from_char('A').as_char().unwrap(), Some('A'));
    }

    #[test]
    fn live_eval_smoke_returns_seven_when_enabled() -> Result<()> {
        if env::var("GS_RUN_LIVE_RUST").as_deref() != Ok("1") {
            return Ok(());
        }

        let mut session = Session::login(Config::from_env()?)?;
        assert_eq!(session.eval("3 + 4")?, Value::SmallInt(7));
        session.logout()?;
        Ok(())
    }

    #[test]
    fn live_login_logout_when_enabled() -> Result<()> {
        let Some(mut session) = live_session()? else {
            return Ok(());
        };

        assert!(session.is_logged_in());
        session.logout()?;
        assert!(!session.is_logged_in());
        Ok(())
    }

    #[test]
    fn live_perform_when_enabled() -> Result<()> {
        let Some(mut session) = live_session()? else {
            return Ok(());
        };

        let seven = session.smallint_oop(7);
        let printed = session.perform_oop(seven, "printString", &[])?;
        assert_eq!(session.fetch_string(printed)?, "7");
        Ok(())
    }

    #[test]
    fn live_global_string_round_trip_when_enabled() -> Result<()> {
        let Some(mut session) = live_session()? else {
            return Ok(());
        };

        let key = live_key("GemStoneRsRoundTrip");
        let text = session.new_string("hello from gemstone-rs")?;
        session.global_put(&key, text)?;
        let stored = session.global_get(&key)?;
        assert_eq!(session.fetch_string(stored)?, "hello from gemstone-rs");
        session.global_put(&key, Oop::NIL)?;
        session.commit()?;
        Ok(())
    }

    #[test]
    fn live_error_handling_when_enabled() -> Result<()> {
        let Some(mut session) = live_session()? else {
            return Ok(());
        };

        let err = session
            .eval("Object gemstoneRsSelectorThatShouldNotExist")
            .unwrap_err();
        assert!(matches!(
            err,
            Error::IllegalOop { .. } | Error::GemStone { .. }
        ));
        Ok(())
    }

    #[test]
    fn live_transaction_commit_and_abort_when_enabled() -> Result<()> {
        let Some(mut session) = live_session()? else {
            return Ok(());
        };

        let committed_key = live_key("GemStoneRsCommitted");
        session.transaction(|session| {
            let value = session.new_string("committed")?;
            session.global_put(&committed_key, value)
        })?;
        let committed = session.global_get(&committed_key)?;
        assert_eq!(session.fetch_string(committed)?, "committed");

        let aborted_key = live_key("GemStoneRsAborted");
        let aborted_value = session.new_string("aborted")?;
        session.global_put(&aborted_key, aborted_value)?;
        session.abort()?;
        assert!(session.global_get(&aborted_key).is_err());

        session.global_put(&committed_key, Oop::NIL)?;
        session.commit()?;
        Ok(())
    }

    fn live_session() -> Result<Option<Session>> {
        if env::var("GS_RUN_LIVE_RUST").as_deref() != Ok("1") {
            return Ok(None);
        }
        Session::login(Config::from_env()?).map(Some)
    }

    fn live_key(prefix: &str) -> String {
        let nanos = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|value| value.as_nanos())
            .unwrap_or_default();
        format!("{prefix}{nanos}")
    }
}
