//! PAP-Compliant Manifest-based Path Resolution
//!
//! This module provides a simple, configuration-driven path resolution system
//! that follows Rustelo's Project Architecture Principles (PAP):
//! - Configuration-driven (no hardcoded defaults)
//! - Language-agnostic (discovered from content)
//! - No fallbacks (explicit failures)
//! - Single source of truth (`rustelo.manifest.ncl`)
//!
//! The canonical format is NCL (`rustelo.manifest.ncl`). TOML
//! (`rustelo.manifest.toml`) is supported as a backward-compatible fallback.
//! NCL is evaluated first; when found, environment variable interpolation is
//! handled natively by Nickel at export time.

use std::path::{Path, PathBuf};
use std::sync::OnceLock;

use serde::{Deserialize, Serialize};

/// Error type for manifest operations
#[derive(Debug)]
pub enum ManifestError {
    /// Neither rustelo.manifest.ncl nor rustelo.manifest.toml found in
    /// directory tree
    ManifestNotFound,
    /// I/O error reading manifest
    Io(std::io::Error),
    /// TOML parsing error (legacy format)
    TomlParse(toml::de::Error),
    /// JSON deserialization error (NCL export output)
    JsonParse(serde_json::Error),
    /// `nickel export` subprocess failed
    NclExport(String),
    /// Invalid path in manifest
    InvalidPath,
    /// Required environment variable not found
    MissingEnvVar(String),
    /// Unknown path key requested
    UnknownPath(String),
    /// Missing path configuration
    MissingPath(String),
    /// WASM platform doesn't support filesystem operations
    WasmNotSupported,
}

impl std::fmt::Display for ManifestError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ManifestError::ManifestNotFound => write!(
                f,
                "rustelo.manifest.ncl (or .toml) not found in current directory tree"
            ),
            ManifestError::Io(e) => write!(f, "I/O error: {e}"),
            ManifestError::TomlParse(e) => write!(f, "TOML parse error: {e}"),
            ManifestError::JsonParse(e) => write!(f, "NCL export JSON parse error: {e}"),
            ManifestError::NclExport(msg) => write!(f, "nickel export failed: {msg}"),
            ManifestError::InvalidPath => write!(f, "Invalid path in manifest"),
            ManifestError::MissingEnvVar(var) => {
                write!(f, "Required environment variable not found: {var}")
            }
            ManifestError::UnknownPath(key) => write!(f, "Unknown path key: {key}"),
            ManifestError::MissingPath(path) => write!(f, "Missing path configuration: {path}"),
            ManifestError::WasmNotSupported => {
                write!(f, "Manifest loading not supported in WASM environment")
            }
        }
    }
}

impl std::error::Error for ManifestError {}

impl From<std::io::Error> for ManifestError {
    fn from(error: std::io::Error) -> Self {
        ManifestError::Io(error)
    }
}

impl From<toml::de::Error> for ManifestError {
    fn from(error: toml::de::Error) -> Self {
        ManifestError::TomlParse(error)
    }
}

impl From<serde_json::Error> for ManifestError {
    fn from(error: serde_json::Error) -> Self {
        ManifestError::JsonParse(error)
    }
}

/// Manifest metadata
#[derive(Debug, Deserialize, Serialize)]
pub struct ManifestMeta {
    pub version: String,
    #[serde(default)]
    pub schema: Option<String>,
}

/// Project information
#[derive(Debug, Deserialize, Serialize)]
pub struct ProjectInfo {
    pub name: String,
    #[serde(rename = "type")]
    pub project_type: String,
    #[serde(default)]
    pub description: Option<String>,
}

/// Path configuration - all paths relative to manifest location
#[derive(Debug, Deserialize, Serialize)]
pub struct PathConfig {
    pub content: String,
    pub config: String,
    pub routes: String,
    pub i18n: String,
    pub assets: String,
    pub ui: String,
    #[serde(default)]
    pub menus: Option<String>,
    #[serde(default)]
    pub footers: Option<String>,
    pub build_output: String,
    pub wasm_output: String,
}

/// Discovery configuration for language-agnostic operation
#[derive(Debug, Deserialize, Serialize)]
pub struct DiscoveryConfig {
    pub default_lang: String,  // Default lang
    pub languages: String,     // "auto" or specific list
    pub content_types: String, // "auto" or specific list
}

/// Deployment configuration with ENV variable support
#[derive(Debug, Deserialize, Serialize)]
pub struct DeploymentConfig {
    pub base_url: String,
    pub content_url: String,
    pub content_root: String,
    pub cache_path: String,
    pub api_endpoint: String,
    pub database_url: String,
}

/// Build configuration
#[derive(Debug, Deserialize, Serialize)]
pub struct BuildConfig {
    pub leptos_output_name: String,
    pub site_addr: String,
    pub reload_port: u16,
    pub debug: u8,
    pub cache_build_path: String,
}

/// Complete Rustelo manifest structure
#[derive(Debug, Deserialize, Serialize)]
pub struct RusteloManifest {
    pub manifest: ManifestMeta,
    pub project: ProjectInfo,
    pub paths: PathConfig,
    pub discovery: DiscoveryConfig,
    pub deployment: DeploymentConfig,
    pub build: BuildConfig,
}

/// Main manifest resolver - loads once and caches
pub struct ManifestResolver {
    manifest: RusteloManifest,
    root: PathBuf,
}

impl ManifestResolver {
    /// Load manifest — NCL takes precedence over TOML. Fails if neither is
    /// found.
    pub fn load() -> Result<Self, ManifestError> {
        let (manifest_path, root) = Self::find_manifest()?;

        let manifest = if manifest_path.extension().and_then(|e| e.to_str()) == Some("ncl") {
            Self::load_ncl(&manifest_path)?
        } else {
            let content = std::fs::read_to_string(&manifest_path)?;
            let mut m: RusteloManifest = toml::from_str(&content)?;
            // TOML format requires manual env-var resolution; NCL resolves at export time.
            m.deployment = Self::resolve_deployment_env(m.deployment)?;
            m
        };

        Ok(Self { manifest, root })
    }

    /// Evaluate a NCL manifest via `nickel export --format json` and
    /// deserialize the output.
    fn load_ncl(path: &Path) -> Result<RusteloManifest, ManifestError> {
        let output = std::process::Command::new("nickel")
            .args(["export", "--format", "json", path.to_str().unwrap_or("")])
            .output()
            .map_err(|e| {
                ManifestError::NclExport(format!("failed to spawn nickel (is it on PATH?): {e}"))
            })?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(ManifestError::NclExport(stderr.trim().to_string()));
        }

        serde_json::from_slice(&output.stdout).map_err(ManifestError::JsonParse)
    }

    /// Find manifest by walking up from cwd.
    ///
    /// Returns `(manifest_path, project_root)` — the project root is always the
    /// directory where the manifest anchor was found, regardless of nesting
    /// depth:
    ///
    /// | File found                          | root      |
    /// |-------------------------------------|-----------|
    /// | `<root>/rustelo/manifest.ncl`       | `<root>`  |
    /// | `<root>/rustelo.manifest.ncl`       | `<root>`  |
    /// | `<root>/rustelo.manifest.toml`      | `<root>`  |
    fn find_manifest() -> Result<(PathBuf, PathBuf), ManifestError> {
        let mut current = std::env::current_dir().map_err(ManifestError::Io)?;

        for _ in 0..10 {
            // Canonical layout: rustelo/manifest.ncl — root is `current`, not
            // `current/rustelo`
            let ncl_nested = current.join("rustelo").join("manifest.ncl");
            if ncl_nested.exists() {
                return Ok((ncl_nested, current));
            }
            for candidate in [
                current.join("rustelo.manifest.ncl"),
                current.join("rustelo.manifest.toml"),
            ] {
                if candidate.exists() {
                    return Ok((candidate, current.clone()));
                }
            }
            match current.parent() {
                Some(parent) => current = parent.to_path_buf(),
                None => break,
            }
        }

        Err(ManifestError::ManifestNotFound)
    }

    /// Get absolute path for a configured path key
    pub fn get_path(&self, key: &str) -> Result<PathBuf, ManifestError> {
        match key {
            "content" => Ok(self.root.join(&self.manifest.paths.content)),
            "config" => Ok(self.root.join(&self.manifest.paths.config)),
            "routes" => Ok(self.root.join(&self.manifest.paths.routes)),
            "i18n" => Ok(self.root.join(&self.manifest.paths.i18n)),
            "assets" => Ok(self.root.join(&self.manifest.paths.assets)),
            "ui" => Ok(self.root.join(&self.manifest.paths.ui)),
            "build_output" => Ok(self.root.join(&self.manifest.paths.build_output)),
            "wasm_output" => Ok(self.root.join(&self.manifest.paths.wasm_output)),
            _ => Err(ManifestError::UnknownPath(key.to_string())),
        }
    }

    /// Get the manifest root directory
    pub fn root(&self) -> &Path {
        &self.root
    }

    /// Get the loaded manifest
    pub fn manifest(&self) -> &RusteloManifest {
        &self.manifest
    }

    /// Resolve deployment ENV variables
    fn resolve_deployment_env(
        mut config: DeploymentConfig,
    ) -> Result<DeploymentConfig, ManifestError> {
        config.base_url = Self::resolve_env_var(&config.base_url)?;
        config.content_url = Self::resolve_env_var(&config.content_url)?;
        config.content_root = Self::resolve_env_var(&config.content_root)?;
        config.api_endpoint = Self::resolve_env_var(&config.api_endpoint)?;
        config.database_url = Self::resolve_env_var(&config.database_url)?;
        Ok(config)
    }

    /// Resolve a single ENV variable with ${VAR} or ${VAR:-default} syntax
    fn resolve_env_var(template: &str) -> Result<String, ManifestError> {
        if !template.starts_with("${") {
            return Ok(template.to_string());
        }

        let inner = template.trim_start_matches("${").trim_end_matches("}");

        if let Some((var, default)) = inner.split_once(":-") {
            // ${VAR:-default} syntax
            Ok(std::env::var(var).unwrap_or_else(|_| default.to_string()))
        } else {
            // ${VAR} syntax - required
            std::env::var(inner).map_err(|_| ManifestError::MissingEnvVar(inner.to_string()))
        }
    }

    /// Empty WASM resolver — filesystem access is not available in WASM.
    /// WASM clients receive path/lang config from server hydration, not the
    /// manifest.
    #[cfg(target_arch = "wasm32")]
    fn wasm_resolver() -> Self {
        Self {
            manifest: RusteloManifest {
                manifest: ManifestMeta {
                    version: "wasm".to_string(),
                    schema: None,
                },
                project: ProjectInfo {
                    name: String::new(),
                    project_type: "rustelo-wasm".to_string(),
                    description: None,
                },
                paths: PathConfig {
                    content: String::new(),
                    config: String::new(),
                    routes: String::new(),
                    i18n: String::new(),
                    assets: String::new(),
                    ui: String::new(),
                    menus: None,
                    footers: None,
                    build_output: String::new(),
                    wasm_output: String::new(),
                },
                discovery: DiscoveryConfig {
                    default_lang: String::new(),
                    languages: String::new(),
                    content_types: String::new(),
                },
                deployment: DeploymentConfig {
                    base_url: String::new(),
                    content_url: String::new(),
                    content_root: String::new(),
                    cache_path: String::new(),
                    api_endpoint: String::new(),
                    database_url: String::new(),
                },
                build: BuildConfig {
                    leptos_output_name: String::new(),
                    site_addr: String::new(),
                    reload_port: 0,
                    debug: 0,
                    cache_build_path: String::new(),
                },
            },
            root: std::path::PathBuf::new(),
        }
    }
}

/// Global manifest instance - loaded once on first access (SSR only)
#[cfg(not(target_arch = "wasm32"))]
/// Global manifest — stores the resolved manifest or the load error (as string
/// for Sync).
#[cfg(not(target_arch = "wasm32"))]
static MANIFEST_INSTANCE: OnceLock<Result<ManifestResolver, String>> = OnceLock::new();

/// Get the global manifest resolver. Fails with `ManifestError` if the manifest
/// was not found or could not be parsed. The error is stored on first load and
/// returned on every subsequent call.
#[cfg(not(target_arch = "wasm32"))]
pub fn get_manifest() -> Result<&'static ManifestResolver, ManifestError> {
    let result =
        MANIFEST_INSTANCE.get_or_init(|| ManifestResolver::load().map_err(|e| e.to_string()));
    result
        .as_ref()
        .map_err(|e| ManifestError::NclExport(e.clone()))
}

/// WASM32: manifest loading is not available in-browser. Callers get an empty
/// resolver; path accessors return `WasmNotSupported`. Language and route
/// config reaches WASM via server-side hydration.
#[cfg(target_arch = "wasm32")]
pub fn get_manifest() -> Result<&'static ManifestResolver, ManifestError> {
    static WASM_RESOLVER: OnceLock<ManifestResolver> = OnceLock::new();
    Ok(WASM_RESOLVER.get_or_init(ManifestResolver::wasm_resolver))
}

/// Get content directory path (safe version)
/// Uses environment variable fallback on errors or WASM32
pub fn content_path_safe() -> Result<PathBuf, ManifestError> {
    match get_manifest() {
        Ok(manifest) => manifest.get_path("content"),
        Err(_) => {
            // Fallback to environment variable
            std::env::var("SITE_CONTENT_PATH")
                .map(PathBuf::from)
                .map_err(|_| ManifestError::MissingPath("content (SITE_CONTENT_PATH)".to_string()))
        }
    }
}

/// Get content directory path (compatibility version)
/// Uses fallback - for build scripts only
#[cfg(not(target_arch = "wasm32"))]
pub fn content_path() -> PathBuf {
    match content_path_safe() {
        Ok(path) => path,
        Err(_) => {
            // Try environment variable fallback
            std::env::var("SITE_CONTENT_PATH")
                .map(PathBuf::from)
                .unwrap_or_else(|_| {
                    eprintln!("Warning: No SITE_CONTENT_PATH set, using fallback 'site/content'.");
                    PathBuf::from("site/content")
                })
        }
    }
}

/// Get config directory path (safe version)
/// Uses fallback values on errors or WASM32
pub fn config_path_safe() -> Result<PathBuf, ManifestError> {
    match get_manifest() {
        Ok(manifest) => manifest.get_path("config"),
        Err(_) => {
            // Fallback to reasonable default
            Ok(PathBuf::from("site/config"))
        }
    }
}

/// Get config directory path (compatibility version)
/// Panics on failure - for build scripts only
#[cfg(not(target_arch = "wasm32"))]
pub fn config_path() -> PathBuf {
    match config_path_safe() {
        Ok(path) => path,
        Err(e) => {
            eprintln!(
                "Warning: Failed to get config path: {}. Using fallback 'site/config'.",
                e
            );
            PathBuf::from("site/config")
        }
    }
}

/// Get routes directory path (safe version)
/// Uses fallback values on errors or WASM32
pub fn routes_path_safe() -> Result<PathBuf, ManifestError> {
    match get_manifest() {
        Ok(manifest) => manifest.get_path("routes"),
        Err(_) => {
            // Fallback to reasonable default
            Ok(PathBuf::from("site/config/routes"))
        }
    }
}

/// Get routes directory path (compatibility version)
/// Panics on failure - for build scripts only
#[cfg(not(target_arch = "wasm32"))]
pub fn routes_path() -> PathBuf {
    match routes_path_safe() {
        Ok(path) => path,
        Err(e) => {
            panic!(
                "Failed to get routes path: {}. Check manifest configuration.",
                e
            )
        }
    }
}

/// Get i18n directory path (safe version)
/// Uses fallback values on errors or WASM32
pub fn i18n_path_safe() -> Result<PathBuf, ManifestError> {
    match get_manifest() {
        Ok(manifest) => manifest.get_path("i18n"),
        Err(_) => {
            // Fallback to reasonable default
            Ok(PathBuf::from("site/i18n"))
        }
    }
}

/// Get i18n directory path (compatibility version)
/// Uses fallback - for build scripts only
#[cfg(not(target_arch = "wasm32"))]
pub fn i18n_path() -> PathBuf {
    match i18n_path_safe() {
        Ok(path) => path,
        Err(e) => {
            eprintln!(
                "Warning: Failed to get i18n path: {}. Using fallback 'site/i18n'.",
                e
            );
            PathBuf::from("site/i18n")
        }
    }
}

/// Get assets directory path (safe version)
/// Uses fallback values on errors or WASM32
pub fn assets_path_safe() -> Result<PathBuf, ManifestError> {
    match get_manifest() {
        Ok(manifest) => manifest.get_path("assets"),
        Err(_) => {
            // Fallback to reasonable default
            Ok(PathBuf::from("public"))
        }
    }
}

/// Get assets directory path (compatibility version)
/// Uses fallback - for build scripts only
#[cfg(not(target_arch = "wasm32"))]
pub fn assets_path() -> PathBuf {
    match assets_path_safe() {
        Ok(path) => path,
        Err(e) => {
            eprintln!(
                "Warning: Failed to get assets path: {}. Using fallback 'public'.",
                e
            );
            PathBuf::from("public")
        }
    }
}

/// Get UI directory path (safe version)
/// Uses fallback values on errors or WASM32
pub fn ui_path_safe() -> Result<PathBuf, ManifestError> {
    match get_manifest() {
        Ok(manifest) => manifest.get_path("ui"),
        Err(_) => {
            // Fallback to reasonable default
            Ok(PathBuf::from("site/ui"))
        }
    }
}

/// Get UI directory path (compatibility version)
/// Uses fallback - for build scripts only
#[cfg(not(target_arch = "wasm32"))]
pub fn ui_path() -> PathBuf {
    match ui_path_safe() {
        Ok(path) => path,
        Err(e) => {
            eprintln!(
                "Warning: Failed to get UI path: {}. Using fallback 'site/ui'.",
                e
            );
            PathBuf::from("site/ui")
        }
    }
}

/// Get build output directory path (safe version)
/// Uses fallback values on errors or WASM32
pub fn build_output_path_safe() -> Result<PathBuf, ManifestError> {
    match get_manifest() {
        Ok(manifest) => manifest.get_path("build_output"),
        Err(_) => {
            // Fallback to reasonable default
            Ok(PathBuf::from("target/site"))
        }
    }
}

/// Get build output directory path (compatibility version)
/// Uses fallback - for build scripts only
#[cfg(not(target_arch = "wasm32"))]
pub fn build_output_path() -> PathBuf {
    match build_output_path_safe() {
        Ok(path) => path,
        Err(e) => {
            eprintln!(
                "Warning: Failed to get build output path: {}. Using fallback 'target/site'.",
                e
            );
            PathBuf::from("target/site")
        }
    }
}

/// Get WASM output directory path (safe version)
/// Uses fallback values on errors or WASM32
pub fn wasm_output_path_safe() -> Result<PathBuf, ManifestError> {
    match get_manifest() {
        Ok(manifest) => manifest.get_path("wasm_output"),
        Err(_) => {
            // Fallback to reasonable default
            Ok(PathBuf::from("pkg"))
        }
    }
}

/// Get WASM output directory path (compatibility version)
/// Uses fallback - for build scripts only
#[cfg(not(target_arch = "wasm32"))]
pub fn wasm_output_path() -> PathBuf {
    match wasm_output_path_safe() {
        Ok(path) => path,
        Err(e) => {
            eprintln!(
                "Warning: Failed to get WASM output path: {}. Using fallback 'pkg'.",
                e
            );
            PathBuf::from("pkg")
        }
    }
}

/// Get build cache root directory path (safe version)
/// Uses fallback values on errors or WASM32
pub fn cache_build_path_safe() -> Result<PathBuf, ManifestError> {
    match get_manifest() {
        Ok(manifest) => {
            let cache_path = &manifest.manifest.build.cache_build_path;
            Ok(manifest.root().join("target").join(cache_path))
        }
        Err(_) => {
            // Fallback to reasonable default based on manifest
            Ok(PathBuf::from("target/rustelo-cache"))
        }
    }
}

/// Get build cache root directory path (compatibility version)
/// Uses fallback - for build scripts and tools
#[cfg(not(target_arch = "wasm32"))]
pub fn cache_build_path() -> PathBuf {
    match cache_build_path_safe() {
        Ok(path) => path,
        Err(e) => {
            eprintln!(
                "Warning: Failed to get cache build path: {}. Using fallback.",
                e
            );
            PathBuf::from("target/rustelo-cache")
        }
    }
}

/// Get client cache directory path (safe version)
/// Uses fallback values on errors or WASM32
pub fn cache_client_path_safe() -> Result<PathBuf, ManifestError> {
    Ok(cache_build_path_safe()?.join("client"))
}

/// Get client cache directory path (compatibility version)
#[cfg(not(target_arch = "wasm32"))]
pub fn cache_client_path() -> PathBuf {
    cache_build_path().join("client")
}

/// Get server cache directory path (safe version)
/// Uses fallback values on errors or WASM32
pub fn cache_server_path_safe() -> Result<PathBuf, ManifestError> {
    Ok(cache_build_path_safe()?.join("server"))
}

/// Get server cache directory path (compatibility version)
#[cfg(not(target_arch = "wasm32"))]
pub fn cache_server_path() -> PathBuf {
    cache_build_path().join("server")
}

/// Get deployment cache directory path (safe version)
/// Uses fallback values on errors or WASM32
pub fn cache_deployment_path_safe() -> Result<PathBuf, ManifestError> {
    match get_manifest() {
        Ok(manifest) => {
            let cache_path = &manifest.manifest.deployment.cache_path;
            Ok(manifest.root().join(cache_path))
        }
        Err(_) => {
            // Fallback to reasonable default
            Ok(PathBuf::from("target/cache"))
        }
    }
}

/// Get deployment cache directory path (compatibility version)
/// Uses fallback - for build scripts only
#[cfg(not(target_arch = "wasm32"))]
pub fn cache_deployment_path() -> PathBuf {
    match cache_deployment_path_safe() {
        Ok(path) => path,
        Err(e) => {
            eprintln!(
                "Warning: Failed to get deployment cache path: {}. Using fallback 'target/cache'.",
                e
            );
            PathBuf::from("target/cache")
        }
    }
}

/// Get debug level from manifest
/// 0 or less no debug output info
/// 1 main debug output info
/// 2 complementary debug output info
/// > 2 FOLLOW routes lookups and rendering
/// > 3 TRACE routes lookups and rendering
/// > 4 DEEP trace routes lookups and rendering
/// > Uses fallback values on errors or WASM32
pub fn get_debug_level() -> u8 {
    match get_manifest() {
        Ok(manifest) => manifest.manifest.build.debug,
        Err(_) => 1, // Reasonable default debug level
    }
}

// Compatibility functions for build scripts (unwrap for build-time errors)

/// Get content directory path (build script compatibility)
/// Uses fallback - for build scripts only
#[cfg(not(target_arch = "wasm32"))]
pub fn manifest_content_path() -> PathBuf {
    content_path()
}

/// Get config directory path (build script compatibility)
/// Uses fallback - for build scripts only
#[cfg(not(target_arch = "wasm32"))]
pub fn manifest_config_path() -> PathBuf {
    config_path()
}

/// Get manifest root directory (build script compatibility)
/// Uses current directory as fallback - for build scripts only
#[cfg(not(target_arch = "wasm32"))]
pub fn manifest_root() -> PathBuf {
    match get_manifest() {
        Ok(manifest) => manifest.root().to_path_buf(),
        Err(e) => {
            eprintln!(
                "Warning: Failed to get manifest root: {}. Using current directory.",
                e
            );
            std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
        }
    }
}

#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::TempDir;

    use super::*;

    #[test]
    fn test_manifest_parsing() {
        let manifest_content = r#"
[manifest]
version = "1.0"

[project]
name = "test-project"
type = "rustelo-app"

[paths]
content = "content"
config = "config"
routes = "config/routes"
i18n = "i18n"
assets = "assets"
ui = "ui"
build_output = "target/site"
wasm_output = "target/site/pkg"

[discovery]
default_lang = "en"
languages = "auto"
content_types = "auto"

[deployment]
base_url = "http://localhost:3000"
content_url = "/r"
content_root = "r"
api_endpoint = "/api"
database_url = "${DATABASE_URL}"

[build]
leptos_output_name = "test"
site_addr = "127.0.0.1:3000"
cache_build_path = "rustelo-cache"
reload_port = 3001
debug = 0
"#;

        let manifest: RusteloManifest =
            toml::from_str(manifest_content).expect("Should parse valid manifest");

        assert_eq!(manifest.project.name, "test-project");
        assert_eq!(manifest.paths.content, "content");
    }

    #[test]
    fn test_env_var_resolution() {
        // Test default value
        let result = ManifestResolver::resolve_env_var("${NONEXISTENT:-default_value}")
            .expect("Should resolve with default");
        assert_eq!(result, "default_value");

        // Test plain string
        let result =
            ManifestResolver::resolve_env_var("plain_string").expect("Should return plain string");
        assert_eq!(result, "plain_string");
    }
}
