//! Nickel configuration loader.
//!
//! Evaluates `.ncl` files to JSON via the `nickel export` CLI.
//! Optionally resolves OCI imports via `ncl-import-resolver` when a
//! `resolver-manifest.json` file is present alongside the config.
//!
//! # Requirements
//!
//! - `nickel` must be on `PATH`.
//! - `ncl-import-resolver` must be on `PATH` when OCI imports are used.

use std::path::Path;

use anyhow::{anyhow, Context};
use serde::de::DeserializeOwned;
use tokio::process::Command;

/// Evaluate a Nickel configuration file and deserialize the result into `T`.
///
/// Steps:
/// 1. If a `resolver-manifest.json` exists beside the `.ncl` file, run
///    `ncl-import-resolver <manifest>` to resolve OCI imports.
/// 2. Run `nickel export --format json <ncl_path>` and capture stdout.
/// 3. Deserialize the JSON output via `serde_json`.
///
/// # Errors
///
/// Returns an error if:
/// - `ncl-import-resolver` exits non-zero (when `resolver-manifest.json` exists).
/// - `nickel export` exits non-zero or cannot be found.
/// - The JSON output cannot be deserialized into `T`.
pub async fn load_nickel_config<T: DeserializeOwned>(ncl_path: &Path) -> anyhow::Result<T> {
    // Resolve OCI imports if a manifest is co-located with the config file.
    if let Some(parent) = ncl_path.parent() {
        let manifest = parent.join("resolver-manifest.json");
        if manifest.exists() {
            let status = Command::new("ncl-import-resolver")
                .arg(&manifest)
                .status()
                .await
                .context("launching ncl-import-resolver")?;

            if !status.success() {
                return Err(anyhow!(
                    "ncl-import-resolver failed (exit {:?}) on '{}'",
                    status.code(),
                    manifest.display()
                ));
            }
        }
    }

    // Export the Nickel config to JSON.
    let out = Command::new("nickel")
        .args(["export", "--format", "json"])
        .arg(ncl_path)
        .output()
        .await
        .with_context(|| {
            format!(
                "launching `nickel export` on '{}'",
                ncl_path.display()
            )
        })?;

    if !out.status.success() {
        let stderr = String::from_utf8_lossy(&out.stderr);
        return Err(anyhow!(
            "`nickel export` failed on '{}': {}",
            ncl_path.display(),
            stderr.trim()
        ));
    }

    serde_json::from_slice::<T>(&out.stdout).with_context(|| {
        format!(
            "deserializing Nickel output from '{}' into {}",
            ncl_path.display(),
            std::any::type_name::<T>()
        )
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde::Deserialize;
    use std::io::Write;

    #[derive(Debug, Deserialize, PartialEq)]
    struct Simple {
        name: String,
        value: u32,
    }

    /// Verifies the error path when `nickel` is not installed or the file
    /// doesn't exist (CI without nickel will hit this branch).
    #[tokio::test]
    async fn test_load_nickel_config_missing_file() {
        let result = load_nickel_config::<Simple>(Path::new("/nonexistent/path.ncl")).await;
        assert!(
            result.is_err(),
            "expected error for non-existent .ncl file"
        );
    }

    /// Verifies resolver-manifest detection: if no manifest exists alongside the
    /// config file, resolver is skipped (only nickel export runs).
    #[tokio::test]
    async fn test_load_nickel_config_no_manifest_skips_resolver() {
        let dir = tempfile::tempdir().unwrap();
        let ncl_path = dir.path().join("config.ncl");
        std::fs::write(&ncl_path, r#"{ name = "test", value = 42 }"#).unwrap();

        // nickel may or may not be installed; either way, no manifest → no resolver invoked.
        let _ = load_nickel_config::<Simple>(&ncl_path).await;
    }
}
