use crate::errors::KamError;
use crate::types::kam_toml::KamToml;
use crate::types::kam_toml::enums::ModuleType;
use crate::types::kam_toml::sections::TmplSection;
use std::collections::{BTreeMap, HashMap};
use std::fs;
use std::path::{Path, PathBuf};
use tera::{Context, Tera};

// 解压压缩包的辅助函数
// 支持tar.gz和zip，根据扩展名判断（简单粗暴）
fn extract_archive(path: &Path, dst: &Path) -> Result<(), KamError> {
    let file = fs::File::open(path).map_err(KamError::Io)?;
    // Use case-insensitive extension checks when possible and
    // normalize the file name for `.tar.gz` checks.
    let file_name_lower = path
        .file_name()
        .and_then(|s| s.to_str())
        .map(str::to_ascii_lowercase)
        .unwrap_or_default();
    if file_name_lower.ends_with(".tar.gz")
        || path
            .extension()
            .is_some_and(|ext| ext.eq_ignore_ascii_case("tgz"))
    {
        let tar = flate2::read::GzDecoder::new(file);
        let mut archive = tar::Archive::new(tar);
        archive.unpack(dst).map_err(KamError::Io)?;
    } else if path
        .extension()
        .is_some_and(|ext| ext.eq_ignore_ascii_case("zip"))
    {
        let mut archive = zip::ZipArchive::new(file).map_err(KamError::Zip)?;
        archive.extract(dst).map_err(KamError::Zip)?;
    } else {
        return Err(KamError::UnsupportedArchive(format!(
            "Unsupported archive: {}",
            path.display()
        )));
    }
    Ok(())
}

fn merge_template_defaults(
    kt_path: &Path,
    template_vars: &mut HashMap<String, String>,
) -> Result<(), KamError> {
    if !kt_path.exists() {
        return Ok(());
    }
    let kt = KamToml::load_from_file(kt_path)?;
    if let Some(tmpl) = kt.tmpl.as_ref() {
        for (k, v) in &tmpl.variables {
            if let Some(default_val) = &v.default {
                template_vars
                    .entry(k.clone())
                    .or_insert_with(|| default_val.clone());
            }
        }
    }
    if let Some(tmpl) = kt.kam.tmpl.as_ref() {
        for (k, v) in &tmpl.variables {
            if let Some(default_val) = &v.default {
                template_vars
                    .entry(k.clone())
                    .or_insert_with(|| default_val.clone());
            }
        }
    }
    Ok(())
}

/// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public struct 'InitImplParams'. Replace with real documentation. (2025-12-30T22:23:21Z)

pub struct InitImplParams<'a> {
    /// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public field 'id' in 'InitImplParams'. Replace with real documentation. (2025-12-30T22:23:21Z)
    pub id: &'a str,
    /// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public field 'name' in 'InitImplParams'. Replace with real documentation. (2025-12-30T22:23:21Z)
    pub name: String,
    /// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public field 'version' in 'InitImplParams'. Replace with real documentation. (2025-12-30T22:23:21Z)
    pub version: &'a str,
    /// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public field 'author' in 'InitImplParams'. Replace with real documentation. (2025-12-30T22:23:21Z)
    pub author: &'a str,
    /// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public field 'description' in 'InitImplParams'. Replace with real documentation. (2025-12-30T22:23:21Z)
    pub description: String,
    /// Update JSON URL to write into generated `kam.toml`.
    pub update_json: Option<String>,
    /// KernelSU module source URL to write into generated `kam.toml`.
    pub source_url: Option<String>,
    /// Whether the module is a KernelSU metamodule.
    pub metamodule: bool,
    /// Type of project being initialized.
    pub module_type: ModuleType,
    /// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public field 'impl_source' in 'InitImplParams'. Replace with real documentation. (2025-12-30T22:23:21Z)
    pub impl_source: &'a Path,
    /// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public field 'template_vars' in 'InitImplParams'. Replace with real documentation. (2025-12-30T22:23:21Z)
    pub template_vars: &'a mut HashMap<String, String>,
    /// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public field 'force' in 'InitImplParams'. Replace with real documentation. (2025-12-30T22:23:21Z)
    pub force: bool,
    /// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public field 'explicit_template_id' in 'InitImplParams'. Replace with real documentation. (2025-12-30T22:23:21Z)
    pub explicit_template_id: Option<&'a str>,
}

// Parameters for init_template grouped into a struct to satisfy Clippy's too_many_arguments lint.
/// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public struct 'InitTemplateParams'. Replace with real documentation. (2025-12-30T22:23:21Z)
pub struct InitTemplateParams<'a> {
    /// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public field 'id' in 'InitTemplateParams'. Replace with real documentation. (2025-12-30T22:23:21Z)
    pub id: &'a str,
    /// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public field 'name' in 'InitTemplateParams'. Replace with real documentation. (2025-12-30T22:23:21Z)
    pub name: String,
    /// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public field 'version' in 'InitTemplateParams'. Replace with real documentation. (2025-12-30T22:23:21Z)
    pub version: &'a str,
    /// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public field 'author' in 'InitTemplateParams'. Replace with real documentation. (2025-12-30T22:23:21Z)
    pub author: &'a str,
    /// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public field 'description' in 'InitTemplateParams'. Replace with real documentation. (2025-12-30T22:23:21Z)
    pub description: String,
    /// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public field 'var' in 'InitTemplateParams'. Replace with real documentation. (2025-12-30T22:23:21Z)
    pub var: &'a [String],
    /// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public field 'impl_template' in 'InitTemplateParams'. Replace with real documentation. (2025-12-30T22:23:21Z)
    pub impl_template: Option<String>,
    /// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public field 'force' in 'InitTemplateParams'. Replace with real documentation. (2025-12-30T22:23:21Z)
    pub force: bool,
    /// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public field 'module_type' in 'InitTemplateParams'. Replace with real documentation. (2025-12-30T22:23:21Z)
    pub module_type: ModuleType,
    /// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public field 'update_json' in 'InitTemplateParams'. Replace with real documentation. (2025-12-30T22:23:21Z)
    pub update_json: Option<String>,
    /// KernelSU module source URL for module.json sourceUrl.
    pub source_url: Option<String>,
    /// Whether the initialized module is a KernelSU metamodule.
    pub metamodule: bool,
}

fn resolve_template_source(
    source: &Path,
    temp_dir: &tempfile::TempDir,
) -> Result<PathBuf, KamError> {
    if !source.exists() {
        return Err(KamError::InvalidDirectory(format!(
            "Template source not found: {}",
            source.display()
        )));
    }

    let mut template_path = source.to_path_buf();
    if template_path.is_file() {
        let extract_dst = temp_dir.path().join("extracted");
        fs::create_dir_all(&extract_dst).map_err(KamError::Io)?;
        extract_archive(&template_path, &extract_dst)?;
        template_path = extract_dst;
    }

    collapse_single_template_dir(template_path)
}

fn collapse_single_template_dir(template_path: PathBuf) -> Result<PathBuf, KamError> {
    if !template_path.is_dir() {
        return Ok(template_path);
    }

    let entries: Vec<_> = fs::read_dir(&template_path)
        .map_err(KamError::Io)?
        .filter_map(Result::ok)
        .collect();

    if entries.len() == 1 && entries[0].path().is_dir() {
        Ok(entries[0].path())
    } else {
        Ok(template_path)
    }
}

fn new_project_kam_toml(params: &InitImplParams<'_>, archive_id: &str) -> KamToml {
    let mut kt = KamToml::new_with_current_timestamp(
        params.id.to_string(),
        params.name.clone(),
        params.version.to_string(),
        Some(params.author.to_string()),
        params.description.clone(),
        params.update_json.clone(),
        None,
    );
    kt.prop.metamodule = params.metamodule;
    kt.kam.module_type = params.module_type.clone();
    if let Some(source_url) = &params.source_url {
        kt.mmrl
            .get_or_insert_with(crate::types::kam_toml::sections::MmrlSection::default);
        if let Some(mmrl) = &mut kt.mmrl {
            if mmrl.repo.is_none() {
                mmrl.repo = Some(crate::types::kam_toml::sections::RepoSection::default());
            }
            if let Some(repo) = &mut mmrl.repo {
                repo.repository = Some(source_url.clone());
            }
        }
    }
    kt.kam.tmpl = Some(TmplSection {
        used_template: Some(archive_id.to_string()),
        variables: BTreeMap::new(),
    });
    kt
}

fn take_kam_vars(template_vars: &mut HashMap<String, String>) -> Vec<(String, String)> {
    let keys: Vec<String> = template_vars.keys().cloned().collect();
    let mut kam_vars = Vec::new();

    for key in keys {
        if key.starts_with('#')
            && let Some(value) = template_vars.remove(&key)
        {
            kam_vars.push((key, value));
        }
    }

    kam_vars
}

fn merge_kam_toml_vars(kt: &KamToml, template_vars: &mut HashMap<String, String>) {
    let kt_vars = crate::template::TemplateVariableProcessor::flatten_kam_toml(kt);
    for (key, value) in kt_vars {
        template_vars.entry(key).or_insert(value);
    }

    template_vars
        .entry("id".to_string())
        .or_insert_with(|| kt.prop.id.clone());
    template_vars
        .entry("version".to_string())
        .or_insert_with(|| kt.prop.version.clone());
    template_vars
        .entry("versionCode".to_string())
        .or_insert_with(|| kt.prop.versionCode.to_string());
    template_vars
        .entry("author".to_string())
        .or_insert_with(|| kt.prop.author.clone().unwrap_or_default());
    template_vars
        .entry("name".to_string())
        .or_insert_with(|| kt.prop.name.clone());
    template_vars
        .entry("description".to_string())
        .or_insert_with(|| kt.prop.description.clone());
}

fn render_project_kam_toml(
    kam_toml_path: &Path,
    params: &InitImplParams<'_>,
    kt: &mut KamToml,
    existed_before_copy: bool,
) -> Result<(), KamError> {
    if !kam_toml_path.exists() || (!params.force && existed_before_copy) {
        return Ok(());
    }

    let content = fs::read_to_string(kam_toml_path).map_err(KamError::Io)?;
    let mut tera_ctx = Context::new();
    for (key, value) in params.template_vars.iter() {
        tera_ctx.insert(key, value);
    }

    let mut tera = Tera::default();
    let rendered = tera.render_str(&content, &tera_ctx).map_err(|e| {
        KamError::TemplateRenderError(format!("Failed to render kam.toml template: {e}"))
    })?;
    let mut rendered_kt = toml::from_str::<KamToml>(&rendered)
        .map_err(|e| KamError::Toml(format!("Failed to parse rendered kam.toml: {e}")))?;

    finalize_rendered_kam_toml(&mut rendered_kt, params, kt.prop.versionCode);

    let fixed_content = toml::to_string_pretty(&rendered_kt)
        .map_err(|e| KamError::Toml(format!("Failed to serialize KamToml: {e}")))?;
    fs::write(kam_toml_path, fixed_content).map_err(KamError::Io)?;
    *kt = rendered_kt;

    Ok(())
}

fn finalize_rendered_kam_toml(
    rendered_kt: &mut KamToml,
    params: &InitImplParams<'_>,
    version_code: i64,
) {
    rendered_kt.prop.id = params.id.to_string();
    rendered_kt.prop.name.clone_from(&params.name);
    rendered_kt.prop.version = params.version.to_string();
    rendered_kt.prop.author = Some(params.author.to_string());
    rendered_kt.prop.description.clone_from(&params.description);
    rendered_kt.prop.versionCode = version_code;
    rendered_kt.prop.updateJson.clone_from(&params.update_json);
    rendered_kt.prop.metamodule = params.metamodule;

    if params.module_type == ModuleType::Kam {
        let default_build = crate::types::kam_toml::sections::BuildSection::default();
        let build = rendered_kt
            .kam
            .build
            .get_or_insert_with(|| default_build.clone());

        build.source_dir = Some(format!("src/{}", rendered_kt.prop.id));
        build.target_dir = Some("dist".to_string());
        build.output_file = Some("{{id}}".to_string());
        build.hooks_dir = Some("hooks".to_string());
        build.exclude.clone_from(&default_build.exclude);
        build.include.clone_from(&default_build.include);
        build.respect_gitignore = default_build.respect_gitignore;
    }

    rendered_kt.kam.module_type = params.module_type.clone();
    if let Some(source_url) = &params.source_url {
        rendered_kt
            .mmrl
            .get_or_insert_with(crate::types::kam_toml::sections::MmrlSection::default);
        if let Some(mmrl) = &mut rendered_kt.mmrl {
            if mmrl.repo.is_none() {
                mmrl.repo = Some(crate::types::kam_toml::sections::RepoSection::default());
            }
            if let Some(repo) = &mut mmrl.repo {
                repo.repository = Some(source_url.clone());
            }
        }
    }
    if let Some(ref mut workspace) = rendered_kt.kam.workspace {
        workspace.members = Some(vec![".".to_string()]);
    }
}

fn write_template_env_file(
    path: &Path,
    kt: &KamToml,
    template_vars: &HashMap<String, String>,
) -> Result<(), KamError> {
    let env_dir = path.join(".kam");
    fs::create_dir_all(&env_dir).map_err(KamError::Io)?;
    let env_file_path = env_dir.join("template-vars.env.init");
    let mut env_lines: Vec<String> = Vec::new();

    env_lines.push(format!(
        "KAM_PROJECT_ROOT=\"{root}\"",
        root = path.to_string_lossy()
    ));
    env_lines.push(format!("KAM_MODULE_ID=\"{id}\"", id = kt.prop.id));
    env_lines.push(format!(
        "KAM_MODULE_VERSION=\"{version}\"",
        version = kt.prop.version
    ));
    env_lines.push(format!(
        "KAM_MODULE_VERSION_CODE=\"{versionCode}\"",
        versionCode = kt.prop.versionCode
    ));
    env_lines.push(format!(
        "KAM_MODULE_NAME=\"{name}\"",
        name = kt.prop.get_name()
    ));
    env_lines.push(format!(
        "KAM_MODULE_AUTHOR=\"{author}\"",
        author = kt.prop.author.as_deref().unwrap_or_default()
    ));
    env_lines.push(format!(
        "KAM_MODULE_DESCRIPTION=\"{description}\"",
        description = kt.prop.get_description()
    ));

    let kt_flatvars = crate::template::TemplateVariableProcessor::flatten_kam_toml(kt);
    for (key, value) in &kt_flatvars {
        let env_key = format!("KAM_{}", key.to_ascii_uppercase().replace(['.', '-'], "_"));
        env_lines.push(format!("{env_key}=\"{}\"", escape_env_value(value)));
    }

    if let Some(tmpl_section) = &kt.kam.tmpl {
        for (var_name, var_def) in &tmpl_section.variables {
            let env_key = format!(
                "KAM_TMPL_{}",
                var_name.to_ascii_uppercase().replace(['.', '-'], "_")
            );
            let value = template_vars
                .get(var_name)
                .cloned()
                .or_else(|| var_def.default.clone())
                .unwrap_or_default();
            env_lines.push(format!("{env_key}=\"{}\"", escape_env_value(&value)));
        }
    }

    fs::write(&env_file_path, env_lines.join("\n")).map_err(KamError::Io)
}

fn escape_env_value(value: &str) -> String {
    value.replace('"', "\\\"")
}

fn init_from_source(
    path: &Path,
    params: &InitTemplateParams<'_>,
    template_vars: &mut HashMap<String, String>,
    source: &Path,
    template_id: &str,
) -> Result<(), KamError> {
    let mut init_params = InitImplParams {
        id: params.id,
        name: params.name.clone(),
        version: params.version,
        author: params.author,
        description: params.description.clone(),
        update_json: params.update_json.clone(),
        source_url: params.source_url.clone(),
        metamodule: params.metamodule,
        module_type: params.module_type.clone(),
        impl_source: source,
        template_vars,
        force: params.force,
        explicit_template_id: Some(template_id),
    };
    init_impl(path, &mut init_params)
}

fn project_local_template_candidates(spec: &str) -> Vec<PathBuf> {
    let mut candidates = Vec::new();
    for dir in crate::utils::PROJECT_TEMPLATE_DIRS {
        let base = Path::new(dir);
        candidates.push(base.join(spec));
        for ext in crate::utils::DEFAULT_ARCHIVE_EXTS {
            candidates.push(base.join(format!("{spec}{ext}")));
        }
    }
    for ext in crate::utils::DEFAULT_ARCHIVE_EXTS {
        candidates.push(Path::new(&format!("{spec}{ext}")).to_path_buf());
    }
    candidates
}

/// Initialize a project implementation from a template source.
///
/// # Errors
///
/// Returns `Err(KamError)` if filesystem operations, archive extraction, or template rendering fail.
pub fn init_impl(path: &Path, params: &mut InitImplParams<'_>) -> Result<(), KamError> {
    let temp_dir = tempfile::tempdir().map_err(KamError::Io)?;
    let template_path = resolve_template_source(params.impl_source, &temp_dir)?;
    let archive_id = params
        .explicit_template_id
        .map_or_else(|| "template".to_string(), ToString::to_string);

    let template_kam_path = template_path.join("kam.toml");
    if template_kam_path.exists() {
        merge_template_defaults(&template_kam_path, params.template_vars)?;
    }

    let mut kt = new_project_kam_toml(params, &archive_id);
    let kam_vars = take_kam_vars(params.template_vars);
    if !kam_vars.is_empty() {
        kt.apply_vars(kam_vars)?;
    }

    if !path.exists() {
        fs::create_dir_all(path).map_err(KamError::Io)?;
    }

    merge_kam_toml_vars(&kt, params.template_vars);

    // Check if kam.toml exists BEFORE copy_and_replace
    let kam_toml_path = path.join("kam.toml");
    let kam_toml_existed_before_copy = kam_toml_path.exists();

    // Copy files (respect build.include/build.exclude if present)
    let excludes = kt.kam.build.as_ref().and_then(|b| b.exclude.as_deref());
    let includes = kt.kam.build.as_ref().and_then(|b| b.include.as_deref());

    crate::template::TemplateManager::copy_and_replace_with_rules(
        &template_path,
        path,
        params.template_vars,
        params.force,
        &archive_id,
        excludes,
        includes,
    )?;

    // If kam.toml doesn't exist (not in template), write the generated one
    if !kam_toml_path.exists() {
        kt.write_to_dir(path)?;
    }

    render_project_kam_toml(
        &kam_toml_path,
        params,
        &mut kt,
        kam_toml_existed_before_copy,
    )?;
    write_template_env_file(path, &kt, params.template_vars)?;

    Ok(())
}

/* legacy wrapper removed - use `InitTemplateParams`-based signature:
`init_template(path, InitTemplateParams { ... })` */

/// Create a project from a named template.
///
/// # Errors
///
/// Returns `Err(KamError)` when the template cannot be found or I/O/rendering fails.
// TODO: split this function into smaller units to reduce complexity
/// AUTO-GENERATED BY Kam/scripts/add_missing_docs.py: placeholder documentation for public function 'init_template'. Replace with real documentation. (2025-12-30T22:23:21Z)
#[allow(clippy::too_many_lines)]
pub fn init_template(path: &Path, params: &InitTemplateParams<'_>) -> Result<(), KamError> {
    let mut template_vars = crate::template::TemplateManager::parse_template_vars(params.var)?;

    let template_spec = params
        .impl_template
        .as_deref()
        .unwrap_or("kam_template")
        .to_string();

    // Strategy:
    // 1. Check if `template_spec` exists as a local file or directory (relative to CWD).
    // 2. Check if `template_spec` exists in built-in assets.
    // 3. Check project-local `tmpl/` or `templates/` dirs.
    // 4. Check global template cache.
    // 5. If NOT found and `template_spec` does NOT end with `_template` or look like a file archive,
    //    append `_template` and retry steps 2-3 (but not step 1 again, usually).

    let mut potential_names = vec![template_spec.clone()];

    // If it doesn't have an extension and doesn't end in _template, try appending it as a fallback
    let is_archive_or_path = template_spec.contains('/')
        || template_spec.contains('\\')
        || template_spec.to_ascii_lowercase().ends_with(".tar.gz")
        || std::path::Path::new(&template_spec)
            .extension()
            .is_some_and(|ext| ext.eq_ignore_ascii_case("zip"));

    if !template_spec.ends_with("_template") && !is_archive_or_path {
        potential_names.push(format!("{template_spec}_template"));
    }

    for spec in &potential_names {
        // 1. Direct local path (only for the first/raw spec, or if we really want to support 'foo_template' local dir)
        let spec_path = Path::new(spec);
        if spec_path.exists() {
            return init_from_source(path, params, &mut template_vars, spec_path, spec.as_str());
        }

        // 2. Built-in assets
        // We typically store built-ins as "kam_template.tar.gz" -> check spec
        // If the spec is "kam", we might be in the second iteration where spec="kam_template"
        let asset_name = format!("{spec}.tar.gz");
        if let Some(asset) = crate::assets::tmpl::TmplAssets::get(&asset_name) {
            let temp_dir = tempfile::tempdir().map_err(KamError::Io)?;
            let temp_path = temp_dir.path().join(format!("{spec}.tar.gz"));
            fs::write(&temp_path, &asset.data).map_err(KamError::Io)?;

            return init_from_source(path, params, &mut template_vars, &temp_path, spec.as_str());
        }

        // 3. Project-local folder search (tmpl/ or templates/)
        for candidate in project_local_template_candidates(spec) {
            if candidate.exists() {
                return init_from_source(
                    path,
                    params,
                    &mut template_vars,
                    &candidate,
                    spec.as_str(),
                );
            }
        }

        // 4. Cache
        if let Ok(Some(cached_path)) =
            crate::template::TemplateCacheManager::resolve_template_path(spec)
        {
            return init_from_source(
                path,
                params,
                &mut template_vars,
                &cached_path,
                spec.as_str(),
            );
        }
    }

    // Final failure
    Err(KamError::TemplateNotFound(format!(
        "Template '{template_spec}' not found in built-in assets, local path, cache, or project directories."
    )))
}

#[cfg(test)]
mod tests {
    use super::merge_template_defaults;
    use std::collections::HashMap;

    #[test]
    fn merge_template_defaults_reads_kam_tmpl_variables() {
        let temp_dir = tempfile::tempdir().expect("tempdir");
        let kam_toml_path = temp_dir.path().join("kam.toml");
        std::fs::write(
            &kam_toml_path,
            r#"
[prop]
id = "example_template"
name = "Example Template"
version = "v1.0.0"
versionCode = 1
description = "Example"

[kam]
module_type = "template"

[kam.tmpl.variables.repository]
var_type = "string"
required = false
default = "https://github.com/example/repo"
"#,
        )
        .expect("write kam.toml");

        let mut vars = HashMap::new();
        merge_template_defaults(&kam_toml_path, &mut vars).expect("merge defaults");

        assert_eq!(
            vars.get("repository").map(String::as_str),
            Some("https://github.com/example/repo")
        );
    }

    #[test]
    fn merge_template_defaults_does_not_override_user_vars() {
        let temp_dir = tempfile::tempdir().expect("tempdir");
        let kam_toml_path = temp_dir.path().join("kam.toml");
        std::fs::write(
            &kam_toml_path,
            r#"
[prop]
id = "example_template"
name = "Example Template"
version = "v1.0.0"
versionCode = 1
description = "Example"

[kam]
module_type = "template"

[kam.tmpl.variables.repository]
var_type = "string"
required = false
default = "https://github.com/example/repo"
"#,
        )
        .expect("write kam.toml");

        let mut vars = HashMap::from([(
            "repository".to_string(),
            "https://github.com/user/repo".to_string(),
        )]);
        merge_template_defaults(&kam_toml_path, &mut vars).expect("merge defaults");

        assert_eq!(
            vars.get("repository").map(String::as_str),
            Some("https://github.com/user/repo")
        );
    }
}
