trail-config 0.4.0

A Rust library for reading config files with path-based access, typed deserialization, environment overlays, deep merging, env variable interpolation, and hot reload support.
Documentation
use super::{Config, ConfigError, YAML};

#[test]
fn yaml_parse_error() {
    let invalid_yaml = "invalid: [unclosed";
    let result = Config::load_yaml(invalid_yaml, "/");

    assert!(result.is_err());
    match result {
        Err(ConfigError::YamlError(_)) => (),
        _ => panic!("Expected YamlError"),
    }
}

#[test]
fn invalid_yaml_formats() {
    let test_cases = vec![
        "invalid: {unclosed",
        "- item1\n - item2\n- item3\n : invalid",
        ": invalid_key",
    ];

    for invalid_yaml in test_cases {
        let result = Config::load_yaml(invalid_yaml, "/");
        assert!(result.is_err(), "Expected error for: {}", invalid_yaml);

        match result {
            Err(ConfigError::YamlError(_)) => (),
            _ => panic!("Expected YamlError for: {}", invalid_yaml),
        }
    }
}

#[test]
fn empty_yaml() {
    let result = Config::load_yaml("", "/");

    assert!(result.is_ok());
    let config = result.unwrap();
    assert!(!config.contains("any/path"));
}

#[test]
fn empty_separator_in_load_optional() {
    let result = Config::load_optional("config.yaml", "", None);

    assert!(result.is_err());
    match result {
        Err(ConfigError::FormatError(msg)) => assert!(msg.contains("empty")),
        _ => panic!("Expected FormatError for empty separator"),
    }
}

#[test]
fn empty_separator_in_load_yaml() {
    let result = Config::load_yaml(YAML, "");

    assert!(result.is_err());
    match result {
        Err(ConfigError::FormatError(msg)) => assert!(msg.contains("empty")),
        _ => panic!("Expected FormatError for empty separator"),
    }
}

#[test]
fn load_optional_missing_file_returns_empty_config() {
    let result = Config::load_optional("nonexistent_file_12345.yaml", "/", None);

    assert!(result.is_ok());
    let config = result.unwrap();
    assert!(!config.contains("any/path"));
    assert_eq!(config.str("any/path"), "");
}

#[test]
fn load_optional_invalid_yaml_returns_error() {
    use std::fs::{self, File};
    use std::io::Write;

    let test_file = "test_optional_invalid.yaml";
    let mut file = File::create(test_file).unwrap();
    writeln!(file, "invalid: [unclosed").unwrap();
    drop(file);

    let result = Config::load_optional(test_file, "/", None);
    assert!(result.is_err());
    match result {
        Err(ConfigError::YamlError(_)) => (),
        _ => panic!("Expected YamlError for malformed file"),
    }

    fs::remove_file(test_file).ok();
}

#[test]
fn load_or_create_creates_file_when_missing() {
    use std::fs;

    const DEFAULTS: &str = "app:\n  port: 8080\n  debug: false\n";
    let test_file = "test_load_or_create_new.yaml";
    fs::remove_file(test_file).ok();

    let result = Config::load_or_create(test_file, "/", None, DEFAULTS);

    assert!(result.is_ok());
    let config = result.unwrap();
    assert_eq!(config.str("app/port"), "8080");
    assert_eq!(config.get_bool("app/debug"), Some(false));

    assert!(fs::metadata(test_file).is_ok());
    let written = fs::read_to_string(test_file).unwrap();
    assert_eq!(written, DEFAULTS);

    fs::remove_file(test_file).ok();
}

#[test]
fn load_or_create_loads_existing_file() {
    use std::fs::{self, File};
    use std::io::Write;

    let test_file = "test_load_or_create_existing.yaml";
    let mut file = File::create(test_file).unwrap();
    writeln!(file, "app:\n  port: 9090").unwrap();
    drop(file);

    let config = Config::load_or_create(test_file, "/", None, "app:\n  port: 8080\n").unwrap();
    assert_eq!(config.str("app/port"), "9090");

    fs::remove_file(test_file).ok();
}

#[test]
fn load_or_create_invalid_defaults_returns_error() {
    use std::fs;

    let test_file = "test_load_or_create_invalid.yaml";
    fs::remove_file(test_file).ok();

    let result = Config::load_or_create(test_file, "/", None, "invalid: [unclosed");

    assert!(result.is_err());
    match result {
        Err(ConfigError::YamlError(_)) => (),
        _ => panic!("Expected YamlError for invalid defaults"),
    }

    fs::remove_file(test_file).ok();
}

#[test]
fn load_or_create_invalid_existing_file_returns_error() {
    use std::fs::{self, File};
    use std::io::Write;

    let test_file = "test_load_or_create_broken.yaml";
    let mut file = File::create(test_file).unwrap();
    writeln!(file, "invalid: [unclosed").unwrap();
    drop(file);

    let result = Config::load_or_create(test_file, "/", None, "app:\n  port: 8080\n");

    assert!(result.is_err());
    match result {
        Err(ConfigError::YamlError(_)) => (),
        _ => panic!("Expected YamlError for broken existing file"),
    }

    fs::remove_file(test_file).ok();
}

#[test]
fn load_required_file_not_found() {
    let result = Config::load_required("nonexistent_file_xyz.yaml", "/", None);

    assert!(result.is_err());
    match result {
        Err(ConfigError::IoError(_)) => (),
        _ => panic!("Expected IoError for missing file"),
    }
}

#[test]
fn load_required_with_env() {
    let result = Config::load_required("config_{env}.yaml", "/", Some("dev"));

    assert!(result.is_err());
    match result {
        Err(ConfigError::IoError(_)) => (),
        _ => panic!("Expected IoError for missing file"),
    }
}

#[test]
fn load_required_rejects_empty_filename() {
    let config = Config::load_required("", "/", None);

    assert!(config.is_err());
    match config {
        Err(ConfigError::IoError(_)) => (),
        _ => panic!("Expected IoError for empty filename"),
    }
}

#[test]
fn error_display_messages() {
    let io_err = ConfigError::IoError(std::io::Error::new(
        std::io::ErrorKind::NotFound,
        "test file not found",
    ));
    assert!(io_err.to_string().contains("IO error"));

    let yaml_err = ConfigError::YamlError("invalid syntax".to_string());
    assert!(yaml_err.to_string().contains("YAML parse error"));

    let path_err = ConfigError::PathNotFound("db/missing/key".to_string());
    assert!(path_err.to_string().contains("Path not found"));

    let fmt_err = ConfigError::FormatError("invalid format".to_string());
    assert!(fmt_err.to_string().contains("Format error"));
}