//! README generator for alef.

use crate::backends::gleam::naming::{gleam_app_name, gleam_nif_module};
use crate::core::backend::GeneratedFile;
use crate::core::config::{Language, ResolvedCrateConfig};
use crate::core::ir::ApiSurface;
use heck::ToUpperCamelCase;
use minijinja::{Environment, Value};

fn to_pascal_case(s: &str) -> String {
    s.to_upper_camel_case()
}
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

mod template_env;

/// Generate README files for the given languages.
pub fn generate_readmes(
    api: &ApiSurface,
    config: &ResolvedCrateConfig,
    languages: &[Language],
) -> anyhow::Result<Vec<GeneratedFile>> {
    let mut files = vec![];
    for &lang in languages {
        if let Some(file) = generate_readme(api, config, lang)? {
            files.push(file);
        }
    }
    Ok(files)
}

fn generate_readme(
    api: &ApiSurface,
    config: &ResolvedCrateConfig,
    lang: Language,
) -> anyhow::Result<Option<GeneratedFile>> {
    // Rust is the source crate, not a binding. The canonical Rust README lives at
    // the workspace crate (e.g. `crates/<name>/README.md`) and is hand-written or
    // managed by the consumer repo. Only emit a Rust README when the user has
    // explicitly opted in via `[readme.languages.rust]` with an `output_path`.
    if matches!(lang, Language::Rust) && !rust_readme_explicitly_configured(config) {
        return Ok(None);
    }

    // Language::Jni and Language::C are FFI shim glue layers, not publishable
    // bindings — they share their public surface with the host language (kotlin-android,
    // ffi). The hardcoded fallback used to emit them at `packages/zig/README.md`,
    // which collided with the actual Zig README. Skip silently — consumers that
    // want a C/JNI README can opt in via `[readme.languages.c]` / `.jni`.
    if matches!(lang, Language::C | Language::Jni) {
        return Ok(None);
    }

    // Try template-based generation first when readme config is present
    if let Some(readme_cfg) = &config.readme {
        if let Some(template_dir) = &readme_cfg.template_dir {
            let workspace_root = config.workspace_root.clone().unwrap_or_else(|| PathBuf::from("."));
            let abs_template_dir = workspace_root.join(template_dir);
            if abs_template_dir.exists() {
                if let Some(file) =
                    try_template_readme(api, config, lang, readme_cfg, &workspace_root, &abs_template_dir)?
                {
                    return Ok(Some(file));
                }
            }
        }
    }

    // Fall back to hardcoded generation
    Ok(Some(generate_readme_hardcoded(api, config, lang)?))
}

/// Returns true when the user has explicitly configured a Rust README in
/// `[readme.languages.rust]` with an `output_path` (or `output`). The default
/// behavior is to skip Rust because the Rust crate's README is the source-of-
/// truth `crates/<name>/README.md`, not a `packages/rust/` stub.
fn rust_readme_explicitly_configured(config: &ResolvedCrateConfig) -> bool {
    let Some(readme_cfg) = &config.readme else {
        return false;
    };
    let Some(rust_cfg) = readme_cfg.languages.get("rust") else {
        return false;
    };
    rust_cfg
        .get("output_path")
        .or_else(|| rust_cfg.get("output"))
        .and_then(|v| v.as_str())
        .is_some()
}

/// Attempt to render a README using a minijinja template. Returns `None` when no
/// language-specific template entry is found in the config (signals caller to fall back).
fn try_template_readme(
    api: &ApiSurface,
    config: &ResolvedCrateConfig,
    lang: Language,
    readme_cfg: &crate::core::config::ReadmeConfig,
    workspace_root: &Path,
    abs_template_dir: &Path,
) -> anyhow::Result<Option<GeneratedFile>> {
    let lang_code = lang_code(lang);

    // Resolve per-language JSON value. Prefer inline `languages` map in alef.toml;
    // fall back to the deprecated external YAML file when `config` is set.
    let lang_json: Option<serde_json::Value> = if !readme_cfg.languages.is_empty() {
        readme_cfg.languages.get(lang_code).cloned()
    } else if let Some(config_path) = &readme_cfg.config {
        let abs_config = workspace_root.join(config_path);
        if abs_config.exists() {
            let content = fs::read_to_string(&abs_config)
                .map_err(|e| anyhow::anyhow!("Failed to read readme config {:?}: {}", abs_config, e))?;
            let yaml: serde_yaml::Value = serde_yaml::from_str(&content)
                .map_err(|e| anyhow::anyhow!("Failed to parse readme config YAML: {}", e))?;
            // Convert the YAML value to JSON so the rest of the function can use a single type.
            let as_json = serde_json::to_value(&yaml)
                .map_err(|e| anyhow::anyhow!("Failed to convert readme YAML to JSON: {}", e))?;
            as_json.get("languages").and_then(|l| l.get(lang_code)).cloned()
        } else {
            None
        }
    } else {
        None
    };

    let Some(lang_json) = lang_json else {
        // No entry for this language — signal caller to fall back
        return Ok(None);
    };

    // Resolve top-level discord_url / banner_url. Prefer inline fields; fall back to
    // what may have been loaded from the external YAML (not re-loaded here — callers
    // using the deprecated path still get the values injected via the JSON block).
    let discord_url = readme_cfg.discord_url.as_deref().unwrap_or("").to_string();
    let banner_url = readme_cfg.banner_url.as_deref().unwrap_or("").to_string();

    // Determine template name: prefer lang config, then default
    let template_name = lang_json
        .get("template")
        .and_then(|v| v.as_str())
        .unwrap_or("language_package.md")
        .to_string();

    let template_file = abs_template_dir.join(&template_name);
    if !template_file.exists() {
        // Template file missing — fall back to hardcoded
        return Ok(None);
    }

    // Set up minijinja environment.
    //
    // Match `template_env::make_env()`: strip the newline after `{% ... %}` tags and the leading
    // whitespace before them, and preserve the trailing newline of loaded template files. Without
    // these, every Jinja control tag leaks a newline into the output and `{% include %}` drops the
    // trailing newline of the partial, both of which corrupt spacing around `## Heading` sections.
    let abs_template_dir_owned = abs_template_dir.to_path_buf();
    let mut env = Environment::new();
    env.set_trim_blocks(true);
    env.set_lstrip_blocks(true);
    env.set_keep_trailing_newline(true);
    env.set_loader(move |name: &str| {
        let path = abs_template_dir_owned.join(name);
        match fs::read_to_string(&path) {
            Ok(content) => Ok(Some(content)),
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
            Err(e) => Err(minijinja::Error::new(
                minijinja::ErrorKind::InvalidOperation,
                format!("Failed to read template {name}: {e}"),
            )),
        }
    });

    // Register include_snippet filter: {{ path | include_snippet(language) }}
    let snippets_dir = readme_cfg.snippets_dir.as_ref().map(|s| workspace_root.join(s));
    let snippets_dir_clone = snippets_dir.clone();
    env.add_filter("include_snippet", move |path: String, language: String| -> String {
        match &snippets_dir_clone {
            Some(dir) => include_snippet(dir, &language, &path),
            None => format!("<!-- snippet not found: {path} -->"),
        }
    });

    // Register render_performance_table filter: {{ perf | render_performance_table(name) }}
    env.add_filter(
        "render_performance_table",
        |benchmarks: Value, name: String| -> String { render_performance_table(&benchmarks, &name) },
    );

    // Register has_migration function
    let workspace_root_clone = workspace_root.to_path_buf();
    env.add_function("has_migration", move |_lang: String, _version: String| -> bool {
        // Check for migration guide file: docs/migrations/{lang}/{version}.md
        let path = workspace_root_clone
            .join("docs")
            .join("migrations")
            .join(&_lang)
            .join(format!("{_version}.md"));
        path.exists()
    });

    // Build template context.
    //
    // Multi-line TOML strings (`description = """..."""`) preserve a trailing newline. Combined
    // with `{{ description }}<blank line>## Heading` in the template, that renders as a double
    // blank line in the output (independent of `trim_blocks` because `{{ ... }}` is an output
    // tag, not a block tag). Trim it so the template controls the surrounding whitespace.
    let name = &config.name;
    let description = config
        .scaffold
        .as_ref()
        .and_then(|s| s.description.clone())
        .map(|s| s.trim_end().to_string())
        .unwrap_or_else(|| format!("Bindings for {name}"));
    let repository = config.github_repo();
    let license = config
        .scaffold
        .as_ref()
        .and_then(|s| s.license.clone())
        .unwrap_or_else(|| "MIT".to_string());

    let mut ctx: HashMap<&str, Value> = HashMap::new();
    ctx.insert("version", Value::from(api.version.clone()));
    ctx.insert("name", Value::from(name.clone()));
    ctx.insert("description", Value::from(description));
    ctx.insert("license", Value::from(license));
    ctx.insert("repository", Value::from(repository));
    ctx.insert("discord_url", Value::from(discord_url));
    ctx.insert("banner_url", Value::from(banner_url));
    ctx.insert("language", Value::from(lang_code.to_string()));

    // Flatten per-language config fields into top-level context
    // (templates expect snippets, features, performance, etc. at top level)
    //
    // String values may themselves contain template expressions (e.g. `{{ version }}`
    // in Java/Elixir install_command). We render those inline before inserting them
    // so the outer template receives the final text.
    if let serde_json::Value::Object(map) = &lang_json {
        for (key, val) in map {
            let rendered_val = if let serde_json::Value::String(s) = val {
                if s.contains("{{") {
                    let rendered = env.render_str(s, &ctx).unwrap_or_else(|_| s.clone());
                    Value::from(rendered)
                } else {
                    json_to_minijinja_value(val)
                }
            } else {
                json_to_minijinja_value(val)
            };
            ctx.insert(
                // SAFETY: we leak the string to get a &'static str for the HashMap key.
                // This is fine since readmes are generated once per run.
                Box::leak(key.clone().into_boxed_str()),
                rendered_val,
            );
        }
    }

    // Ensure `snippets` is always defined so templates can access `snippets.X`
    // unconditionally without raising an undefined-variable error. When the
    // per-language config omits the `snippets` key (e.g. sample_crate's `ffi` block),
    // accessing `snippets.basic_extraction` would otherwise cause minijinja to
    // error with "could not render include".
    ctx.entry("snippets")
        .or_insert_with(|| json_to_minijinja_value(&serde_json::Value::Object(Default::default())));

    let tmpl = env
        .get_template(&template_name)
        .map_err(|e| anyhow::anyhow!("Failed to load template '{}': {}", template_name, e))?;

    let mut content = tmpl
        .render(ctx)
        .map_err(|e| anyhow::anyhow!("Failed to render template '{}': {}", template_name, e))?;

    // Ensure POSIX-compliant trailing newline
    if !content.ends_with('\n') {
        content.push('\n');
    }

    // Determine output path
    let path = readme_output_path(config, lang, readme_cfg, &lang_json);

    Ok(Some(GeneratedFile {
        path,
        content,
        generated_header: false,
    }))
}

/// Determine the output path for a language README.
fn readme_output_path(
    config: &ResolvedCrateConfig,
    lang: Language,
    readme_cfg: &crate::core::config::ReadmeConfig,
    lang_json: &serde_json::Value,
) -> PathBuf {
    // Check for explicit output_path in per-language config
    if let Some(output) = lang_json
        .get("output_path")
        .or_else(|| lang_json.get("output"))
        .and_then(|v| v.as_str())
    {
        return PathBuf::from(output);
    }

    // Check output_pattern in ReadmeConfig (e.g. "packages/{language}/README.md")
    if let Some(pattern) = &readme_cfg.output_pattern {
        let dir = lang_dir_name(lang);
        return PathBuf::from(pattern.replace("{language}", dir));
    }

    // Default to the same paths as the hardcoded generator
    default_readme_path(config, lang)
}

fn default_readme_path(config: &ResolvedCrateConfig, lang: Language) -> PathBuf {
    let name = &config.name;
    match lang {
        Language::Ffi => PathBuf::from(format!("crates/{name}-ffi/README.md")),
        Language::Wasm => PathBuf::from(format!("crates/{name}-wasm/README.md")),
        // Node: the real publish target is the NAPI-RS crate, not a packages/node/ stub.
        Language::Node => PathBuf::from(format!("crates/{name}-node/README.md")),
        // Rust: the source crate IS the canonical crate — no packages/rust/ stub.
        Language::Rust => PathBuf::from(format!("crates/{name}/README.md")),
        _ => PathBuf::from(format!("packages/{}/README.md", lang_dir_name(lang))),
    }
}

/// Return the short directory/key name for a language. This is the canonical
/// `packages/<dir>/` directory name used when no explicit `output_path` is
/// configured. For Language::Node we return `"node"` (matching the alef-scaffold
/// directory convention); the YAML/TOML config key remains `"typescript"`
/// (see [`lang_code`]).
fn lang_dir_name(lang: Language) -> &'static str {
    match lang {
        Language::Python => "python",
        Language::Node => "node",
        Language::Ruby => "ruby",
        Language::Php => "php",
        Language::Elixir => "elixir",
        Language::Go => "go",
        Language::Java => "java",
        Language::Csharp => "csharp",
        Language::Ffi => "ffi",
        Language::Wasm => "wasm",
        Language::R => "r",
        Language::Rust => "rust",
        Language::Kotlin => "kotlin",
        Language::KotlinAndroid => "kotlin-android",
        Language::Swift => "swift",
        Language::Dart => "dart",
        Language::Gleam => "gleam",
        Language::Zig => "zig",
        Language::C | Language::Jni => "c",
    }
}

/// Return the YAML config key for a language.
fn lang_code(lang: Language) -> &'static str {
    match lang {
        Language::Python => "python",
        Language::Node => "typescript",
        Language::Ruby => "ruby",
        Language::Php => "php",
        Language::Elixir => "elixir",
        Language::Go => "go",
        Language::Java => "java",
        Language::Csharp => "csharp",
        Language::Ffi => "ffi",
        Language::Wasm => "wasm",
        Language::R => "r",
        Language::Rust => "rust",
        Language::Kotlin => "kotlin",
        Language::KotlinAndroid => "kotlin_android",
        Language::Swift => "swift",
        Language::Dart => "dart",
        Language::Gleam => "gleam",
        Language::Zig => "zig",
        Language::C | Language::Jni => "c",
    }
}

/// Load a snippet file. For `.md` files, extract the first fenced code block.
/// For other files, wrap the content in a fenced code block.
fn include_snippet(snippets_dir: &Path, lang_code: &str, path: &str) -> String {
    let file = snippets_dir.join(lang_code).join(path);
    if !file.exists() {
        return format!("<!-- snippet not found: {path} -->");
    }
    let content = fs::read_to_string(&file).unwrap_or_default();
    if path.ends_with(".md") {
        extract_code_block(&content)
    } else {
        let ext = Path::new(path).extension().and_then(|e| e.to_str()).unwrap_or("");
        format!("```{ext}\n{}\n```", content.trim())
    }
}

/// Extract the first fenced code block from a Markdown string.
/// Returns the original content (including fence markers) if no block is found.
fn extract_code_block(md: &str) -> String {
    let mut in_block = false;
    let mut block_lines: Vec<&str> = vec![];
    let mut fence_marker = "";

    for line in md.lines() {
        if !in_block {
            if line.starts_with("```") || line.starts_with("~~~") {
                in_block = true;
                fence_marker = if line.starts_with("```") { "```" } else { "~~~" };
                block_lines.push(line);
            }
        } else {
            block_lines.push(line);
            if line.trim() == fence_marker {
                break;
            }
        }
    }

    if block_lines.is_empty() {
        md.to_string()
    } else {
        block_lines.join("\n")
    }
}

/// Render a Markdown performance table from a minijinja benchmarks Value.
///
/// Expects the value to be a sequence of mappings with keys:
/// `name`, `value`, `unit` (optional), `notes` (optional).
fn render_performance_table(perf: &Value, _name: &str) -> String {
    use minijinja::value::ValueKind;

    // Extract platform/function/note metadata
    let platform = perf
        .get_attr("platform")
        .ok()
        .and_then(|v: Value| v.as_str().map(str::to_string))
        .unwrap_or_default();
    let function = perf
        .get_attr("function")
        .ok()
        .and_then(|v: Value| v.as_str().map(str::to_string))
        .unwrap_or_default();
    let note = perf
        .get_attr("note")
        .ok()
        .and_then(|v: Value| v.as_str().map(str::to_string))
        .unwrap_or_default();

    // Extract benchmarks array
    let benchmarks = match perf.get_attr("benchmarks") {
        Ok(v) if v.kind() == ValueKind::Seq => v,
        _ => return String::new(),
    };
    let Ok(iter) = benchmarks.try_iter() else {
        return String::new();
    };

    let mut out = String::new();
    if !platform.is_empty() {
        out.push_str(&template_env::render(
            "performance_context.jinja",
            minijinja::context! { platform => platform, function => function, note => note },
        ));
        out.push('\n');
    }

    // Detect table format: latency/throughput or ops/sec
    let items: Vec<Value> = iter.collect();
    let has_throughput = items
        .iter()
        .any(|item| item.get_attr("throughput").ok().is_some_and(|v| !v.is_undefined()));

    if has_throughput {
        out.push_str("| Document | Size | Latency | Throughput |\n");
        out.push_str("|----------|------|---------|------------|\n");
        for item in &items {
            let name = item
                .get_attr("name")
                .ok()
                .and_then(|v: Value| v.as_str().map(str::to_string))
                .unwrap_or_default();
            let size = item
                .get_attr("size")
                .ok()
                .and_then(|v: Value| v.as_str().map(str::to_string))
                .unwrap_or_default();
            let latency = item
                .get_attr("latency")
                .ok()
                .and_then(|v: Value| v.as_str().map(str::to_string))
                .unwrap_or_default();
            let throughput = item
                .get_attr("throughput")
                .ok()
                .and_then(|v: Value| v.as_str().map(str::to_string))
                .unwrap_or_default();
            out.push_str(&template_env::render(
                "performance_throughput_row.jinja",
                minijinja::context! { name => name, size => size, latency => latency, throughput => throughput },
            ));
        }
    } else {
        out.push_str("| Document | Size | Ops/sec |\n");
        out.push_str("|----------|------|---------|\n");
        for item in &items {
            let name = item
                .get_attr("name")
                .ok()
                .and_then(|v: Value| v.as_str().map(str::to_string))
                .unwrap_or_default();
            let size = item
                .get_attr("size")
                .ok()
                .and_then(|v: Value| v.as_str().map(str::to_string))
                .unwrap_or_default();
            let ops = item
                .get_attr("ops_sec")
                .ok()
                .map(|v: Value| format!("{v}"))
                .unwrap_or_default();
            out.push_str(&template_env::render(
                "performance_ops_row.jinja",
                minijinja::context! { name => name, size => size, ops => ops },
            ));
        }
    }
    out
}

/// Convert a `serde_json::Value` into a `minijinja::Value` via serde serialization.
fn json_to_minijinja_value(json: &serde_json::Value) -> Value {
    Value::from_serialize(json)
}

// ---------------------------------------------------------------------------
// Hardcoded fallback generator (original implementation)
// ---------------------------------------------------------------------------

fn generate_readme_hardcoded(
    api: &ApiSurface,
    config: &ResolvedCrateConfig,
    lang: Language,
) -> anyhow::Result<GeneratedFile> {
    let name = &config.name;
    let description = config
        .scaffold
        .as_ref()
        .and_then(|s| s.description.clone())
        .unwrap_or_else(|| format!("Bindings for {}", name));
    let repository = config.github_repo();
    let example_pointer = format!("See {repository} for usage examples.");

    let (lang_display, install_instructions, example_code, dir_name) = match lang {
        // Examples are derived from the API surface so the snippet shows a real call
        // signature instead of a `// TODO` placeholder. Falls back to a "see main README"
        // pointer when the API has no public functions to demonstrate.
        Language::Python => {
            let module = config.python_module_name().trim_start_matches('_').to_string();
            let example_body = api
                .functions
                .first()
                .map(|f| {
                    format!(
                        "# result = {module}.{name}(...)\n# See the main repository's docs for full usage.",
                        name = f.name
                    )
                })
                .unwrap_or_else(|| format!("# {example_pointer}"));
            (
                "Python",
                format!("```bash\npip install {name}\n```"),
                format!("```python\nimport {module}\n\n{example_body}\n```"),
                "python",
            )
        }
        Language::Node => {
            let pkg = config.node_package_name();
            let example_body = api
                .functions
                .first()
                .map(|f| {
                    format!(
                        "// const result = await {fname}(...);\n// See the main repository's docs for full usage.",
                        fname = to_camel(&f.name)
                    )
                })
                .unwrap_or_else(|| format!("// {example_pointer}"));
            (
                "Node.js",
                format!("```bash\nnpm install {pkg}\n```"),
                format!("```typescript\nimport {{ /* ... */ }} from '{pkg}';\n\n{example_body}\n```"),
                "node",
            )
        }
        Language::Ruby => {
            let gem = config.ruby_gem_name();
            let example_body = format!("# {example_pointer}");
            (
                "Ruby",
                format!("```bash\ngem install {gem}\n```"),
                format!("```ruby\nrequire '{gem}'\n\n{example_body}\n```"),
                "ruby",
            )
        }
        Language::Php => {
            let ext = config.php_extension_name();
            let example_body = format!("// {example_pointer}");
            // Composer requires a `<vendor>/<package>` form. Derive the vendor
            // from the configured repository URL; fall back to the crate name
            // (which produces `<crate>/<crate>` — recognizably wrong without
            // smuggling a specific organization's name).
            let vendor = config
                .try_github_repo()
                .ok()
                .as_deref()
                .and_then(crate::core::config::derive_repo_org)
                .unwrap_or_else(|| name.clone());
            (
                "PHP",
                format!("```bash\ncomposer require {vendor}/{name}\n```"),
                format!("```php\n<?php\n\nuse {ext};\n\n{example_body}\n```"),
                "php",
            )
        }
        Language::Elixir => {
            let app = config.elixir_app_name();
            let module = capitalize_first(&app);
            let example_body = format!("# {example_pointer}");
            (
                "Elixir",
                format!(
                    "Add `:{app}` to your `mix.exs` dependencies:\n\n```elixir\ndefp deps do\n  [\n    {{:{app}, \"~> {version}\"}}\n  ]\nend\n```",
                    version = api.version,
                ),
                format!("```elixir\n{module}.hello()\n\n{example_body}\n```"),
                "elixir",
            )
        }
        Language::Go => {
            let module = config.go_module();
            let example_body = format!("\t// {example_pointer}");
            (
                "Go",
                format!("```bash\ngo get {module}\n```"),
                format!("```go\npackage main\n\nimport \"{module}\"\n\nfunc main() {{\n{example_body}\n}}\n```"),
                "go",
            )
        }
        Language::Java => {
            let package = config.java_package();
            let example_body = format!("// {example_pointer}");
            (
                "Java",
                format!(
                    "Add to your `pom.xml`:\n\n```xml\n<dependency>\n    <groupId>{package}</groupId>\n    <artifactId>{name}</artifactId>\n    <version>{version}</version>\n</dependency>\n```",
                    version = api.version,
                ),
                format!("```java\nimport {package}.*;\n\n{example_body}\n```"),
                "java",
            )
        }
        Language::Csharp => {
            let ns = config.csharp_namespace();
            let example_body = format!("// {example_pointer}");
            (
                "C#",
                format!("```bash\ndotnet add package {ns}\n```"),
                format!("```csharp\nusing {ns};\n\n{example_body}\n```"),
                "csharp",
            )
        }
        Language::Ffi => {
            let header = config.ffi_header_name();
            let example_body = format!("    // {example_pointer}");
            (
                "FFI (C/C++)",
                format!(
                    "Link against `lib{name}_ffi` and include `{header}`.\n\nSee the build instructions in the main repository.",
                ),
                format!("```c\n#include \"{header}\"\n\nint main(void) {{\n{example_body}\n    return 0;\n}}\n```"),
                "ffi",
            )
        }
        Language::Wasm => {
            let example_body = format!("// {example_pointer}");
            (
                "WebAssembly",
                format!("```bash\nnpm install {name}-wasm\n```"),
                format!("```javascript\nimport init from '{name}-wasm';\n\nawait init();\n{example_body}\n```"),
                "wasm",
            )
        }
        Language::R => {
            let pkg = config.r_package_name();
            let example_body = format!("# {example_pointer}");
            (
                "R",
                format!("```r\ninstall.packages('{pkg}')\n```"),
                format!("```r\nlibrary({pkg})\n\n{example_body}\n```"),
                "r",
            )
        }
        Language::Rust => {
            let import = config.core_import_name();
            let example_body = format!("// {example_pointer}");
            (
                "Rust",
                format!("```bash\ncargo add {name}\n```"),
                format!("```rust\nuse {import};\n\n{example_body}\n```"),
                "rust",
            )
        }
        Language::Kotlin => {
            let module = config.name.replace('-', "_");
            (
                "Kotlin",
                format!(
                    "Add the generated package to your `build.gradle.kts`:\n\n```kotlin\ndependencies {{\n    implementation(\"{}:{}:VERSION\")\n}}\n```",
                    config.kotlin_package(),
                    module
                ),
                format!(
                    "```kotlin\nimport {}.{}\n\n// Call generated APIs through the {} object.\n```",
                    config.kotlin_package(),
                    to_pascal_case(&config.name),
                    to_pascal_case(&config.name)
                ),
                "kotlin",
            )
        }
        Language::KotlinAndroid => {
            let module = config.name.replace('-', "_");
            (
                "Kotlin/Android",
                format!(
                    "Add the generated AAR to your Android module's `build.gradle.kts`:\n\n```kotlin\ndependencies {{\n    implementation(\"{}:{}-android:VERSION\")\n}}\n```",
                    config.kotlin_package(),
                    module
                ),
                format!(
                    "```kotlin\nimport {}.{}\n\n// The bundled native library is loaded via System.loadLibrary().\n```",
                    config.kotlin_package(),
                    to_pascal_case(&config.name)
                ),
                "kotlin-android",
            )
        }
        Language::Swift => (
            "Swift",
            format!(
                "Add to `Package.swift`:\n\n```swift\n.package(url: \"<repo-url>\", from: \"{}\")\n```",
                config.name
            ),
            "```swift\n// Phase 2: Swift bindings via swift-bridge. Skeleton only.\n```".to_string(),
            "swift",
        ),
        Language::Dart => (
            "Dart",
            format!(
                "Add to `pubspec.yaml`:\n\n```yaml\ndependencies:\n  {}:\n    git: <repo-url>\n```",
                config.name.replace('-', "_")
            ),
            "```dart\n// Phase 2: Dart bindings via flutter_rust_bridge. Skeleton only.\n```".to_string(),
            "dart",
        ),
        Language::Gleam => {
            let app = gleam_app_name(config);
            (
                "Gleam",
                format!("```sh\ngleam add {app}\n```"),
                format!(
                    "```gleam\nimport {app}\n\n// Call functions exported by the generated module.\n// The NIF is loaded via `@external(erlang, \"{}\", ...)`.\n```",
                    gleam_nif_module(config)
                ),
                "gleam",
            )
        }
        Language::C | Language::Jni | Language::Zig => {
            let module = config.zig_module_name();
            (
                "Zig",
                format!(
                    "Add to `build.zig.zon`:\n\n```zig\n.dependencies = .{{\n    .{module} = .{{ .url = \"<tarball-url>\" }},\n}};\n```"
                ),
                format!(
                    "```zig\nconst {module} = @import(\"{module}\");\n\n// Call generated wrapper functions; strings allocated by the FFI must\n// be released with `{module}._free_string`.\n```"
                ),
                "zig",
            )
        }
    };

    let content = format!(
        r#"# {name} - {lang_display} Bindings

{description}

## Installation

{install}

## Quick Start

{example}

## Documentation

For full documentation, see the [{name} repository]({repository}).

## License

See the [LICENSE]({repository}/blob/main/LICENSE) file in the root repository.
"#,
        name = name,
        lang_display = lang_display,
        description = description,
        install = install_instructions,
        example = example_code,
        repository = repository,
    );

    // Use the readme config output pattern if provided, otherwise default.
    // Node and Rust publish from their crate directories, not packages/ stubs.
    let path = match lang {
        Language::Ffi => PathBuf::from(format!("crates/{}-ffi/README.md", name)),
        Language::Wasm => PathBuf::from(format!("crates/{}-wasm/README.md", name)),
        Language::Node => PathBuf::from(format!("crates/{}-node/README.md", name)),
        Language::Rust => PathBuf::from(format!("crates/{}/README.md", name)),
        _ => PathBuf::from(format!("packages/{}/README.md", dir_name)),
    };

    Ok(GeneratedFile {
        path,
        content,
        generated_header: false,
    })
}

/// Convert snake_case to camelCase. Used to format function names in README examples.
fn to_camel(s: &str) -> String {
    let mut result = String::with_capacity(s.len());
    let mut upper_next = false;
    for (i, ch) in s.chars().enumerate() {
        if ch == '_' {
            upper_next = true;
        } else if upper_next {
            result.extend(ch.to_uppercase());
            upper_next = false;
        } else if i == 0 {
            result.extend(ch.to_lowercase());
        } else {
            result.push(ch);
        }
    }
    result
}

/// Capitalize the first character of a string.
fn capitalize_first(s: &str) -> String {
    let mut chars = s.chars();
    match chars.next() {
        None => String::new(),
        Some(c) => c.to_uppercase().to_string() + chars.as_str(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::config::{NewAlefConfig, ReadmeConfig, ResolvedCrateConfig};

    fn test_config() -> ResolvedCrateConfig {
        let cfg: NewAlefConfig = toml::from_str(
            r#"
[workspace]
languages = ["python", "node"]

[[crates]]
name = "my-lib"
sources = ["src/lib.rs"]

[crates.scaffold]
description = "Test library"
license = "MIT"
repository = "https://github.com/test/my-lib"
"#,
        )
        .expect("valid toml");
        cfg.resolve().expect("resolve ok").remove(0)
    }

    fn test_api() -> ApiSurface {
        ApiSurface {
            crate_name: "my-lib".to_string(),
            version: "0.1.0".to_string(),
            types: vec![],
            functions: vec![],
            enums: vec![],
            errors: vec![],
            excluded_type_paths: ::std::collections::HashMap::new(),
            excluded_trait_names: ::std::collections::HashSet::new(),
            services: vec![],
            handler_contracts: vec![],
        }
    }

    #[test]
    fn test_generate_python_readme() {
        let config = test_config();
        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
        assert_eq!(files.len(), 1);
        assert_eq!(files[0].path, PathBuf::from("packages/python/README.md"));
        assert!(files[0].content.contains("Python"));
        assert!(files[0].content.contains("pip install"));
    }

    #[test]
    fn test_generate_node_readme() {
        let config = test_config();
        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Node]).unwrap();
        assert_eq!(files.len(), 1);
        assert_eq!(files[0].path, PathBuf::from("crates/my-lib-node/README.md"));
        assert!(files[0].content.contains("Node.js"));
    }

    #[test]
    fn test_generate_multiple_readmes() {
        let config = test_config();
        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Python, Language::Node]).unwrap();
        assert_eq!(files.len(), 2);
    }

    #[test]
    fn test_extract_code_block() {
        let md = "Some text\n\n```python\nprint('hello')\n```\n\nMore text";
        let result = extract_code_block(md);
        assert!(result.contains("```python"));
        assert!(result.contains("print('hello')"));
    }

    #[test]
    fn test_extract_code_block_no_block() {
        let md = "Just plain text";
        let result = extract_code_block(md);
        assert_eq!(result, "Just plain text");
    }

    #[test]
    fn test_render_performance_table_empty() {
        let v = Value::from(Vec::<Value>::new());
        let result = render_performance_table(&v, "test");
        assert!(result.is_empty());
    }

    #[test]
    fn test_include_snippet_missing() {
        let result = include_snippet(Path::new("/nonexistent"), "python", "foo.py");
        assert!(result.contains("snippet not found"));
    }

    #[test]
    fn test_template_version_in_install_command() {
        // Verify that {{ version }} placeholders in per-language config strings
        // (e.g. Java pom.xml snippet, Elixir mix.exs dep) are rendered.
        let tmp = std::env::temp_dir().join("alef_readme_test_version");
        let _ = fs::remove_dir_all(&tmp);
        fs::create_dir_all(&tmp).unwrap();

        // Minimal template that just outputs the install_command
        fs::write(tmp.join("test.md"), "{{ install_command }}").unwrap();

        let mut config = test_config();
        let mut lang_map = std::collections::HashMap::new();
        lang_map.insert(
            "java".to_string(),
            serde_json::json!({
                "template": "test.md",
                "install_command": "<version>{{ version }}</version>",
                "output_path": "packages/java/README.md"
            }),
        );
        config.readme = Some(ReadmeConfig {
            template_dir: Some(tmp.clone()),
            snippets_dir: None,
            config: None,
            output_pattern: None,
            discord_url: None,
            banner_url: None,
            languages: lang_map,
        });
        config.workspace_root = Some(tmp.clone());

        let api = test_api(); // version = "0.1.0"
        let files = generate_readmes(&api, &config, &[Language::Java]).unwrap();
        assert_eq!(files.len(), 1);
        assert!(
            files[0].content.contains("<version>0.1.0</version>"),
            "Expected version placeholder to be rendered, got: {}",
            files[0].content,
        );
        assert!(
            !files[0].content.contains("{{ version }}"),
            "Raw template placeholder should not remain in output",
        );

        let _ = fs::remove_dir_all(&tmp);
    }

    #[test]
    fn test_json_to_minijinja_value_primitives() {
        let json: serde_json::Value = serde_json::from_str(r#"{"key": "value", "num": 42, "flag": true}"#).unwrap();
        let mj = json_to_minijinja_value(&json);
        // The value should be an object accessible by attribute
        assert!(mj.get_attr("key").is_ok());
    }

    // --- generate_readmes: empty language list ---

    #[test]
    fn test_generate_readmes_empty_languages() {
        let config = test_config();
        let api = test_api();
        let files = generate_readmes(&api, &config, &[]).unwrap();
        assert_eq!(files.len(), 0);
    }

    // --- hardcoded fallback: remaining language variants ---

    #[test]
    fn test_generate_ruby_readme() {
        let config = test_config();
        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Ruby]).unwrap();
        assert_eq!(files.len(), 1);
        assert_eq!(files[0].path, PathBuf::from("packages/ruby/README.md"));
        assert!(files[0].content.contains("Ruby"));
        assert!(files[0].content.contains("gem install"));
    }

    #[test]
    fn test_generate_php_readme() {
        let config = test_config();
        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Php]).unwrap();
        assert_eq!(files.len(), 1);
        assert_eq!(files[0].path, PathBuf::from("packages/php/README.md"));
        assert!(files[0].content.contains("PHP"));
        assert!(files[0].content.contains("composer require"));
    }

    #[test]
    fn test_generate_elixir_readme() {
        let config = test_config();
        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Elixir]).unwrap();
        assert_eq!(files.len(), 1);
        assert_eq!(files[0].path, PathBuf::from("packages/elixir/README.md"));
        assert!(files[0].content.contains("Elixir"));
        assert!(files[0].content.contains("mix.exs"));
    }

    #[test]
    fn test_generate_go_readme() {
        let config = test_config();
        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Go]).unwrap();
        assert_eq!(files.len(), 1);
        assert_eq!(files[0].path, PathBuf::from("packages/go/README.md"));
        assert!(files[0].content.contains("Go"));
        assert!(files[0].content.contains("go get"));
    }

    #[test]
    fn test_generate_java_readme_hardcoded() {
        let config = test_config();
        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Java]).unwrap();
        assert_eq!(files.len(), 1);
        assert_eq!(files[0].path, PathBuf::from("packages/java/README.md"));
        assert!(files[0].content.contains("Java"));
        assert!(files[0].content.contains("pom.xml"));
    }

    #[test]
    fn test_generate_csharp_readme() {
        let config = test_config();
        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Csharp]).unwrap();
        assert_eq!(files.len(), 1);
        assert_eq!(files[0].path, PathBuf::from("packages/csharp/README.md"));
        assert!(files[0].content.contains("C#"));
        assert!(files[0].content.contains("dotnet add package"));
    }

    #[test]
    fn test_generate_ffi_readme() {
        let config = test_config();
        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Ffi]).unwrap();
        assert_eq!(files.len(), 1);
        assert_eq!(files[0].path, PathBuf::from("crates/my-lib-ffi/README.md"));
        assert!(files[0].content.contains("FFI"));
    }

    #[test]
    fn test_generate_wasm_readme() {
        let config = test_config();
        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Wasm]).unwrap();
        assert_eq!(files.len(), 1);
        assert_eq!(files[0].path, PathBuf::from("crates/my-lib-wasm/README.md"));
        assert!(files[0].content.contains("WebAssembly"));
    }

    #[test]
    fn test_generate_r_readme() {
        let config = test_config();
        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::R]).unwrap();
        assert_eq!(files.len(), 1);
        assert_eq!(files[0].path, PathBuf::from("packages/r/README.md"));
        assert!(files[0].content.contains("install.packages"));
    }

    #[test]
    fn test_generate_rust_readme_skipped_by_default() {
        // Rust is the source crate — the canonical README lives at
        // `crates/<name>/README.md` and is hand-managed. alef must NOT emit a
        // `packages/rust/README.md` stub unless the consumer explicitly opts in.
        let config = test_config();
        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Rust]).unwrap();
        assert!(
            files.is_empty(),
            "Rust README should be skipped by default, got: {:?}",
            files.iter().map(|f| &f.path).collect::<Vec<_>>()
        );
    }

    #[test]
    fn test_generate_rust_readme_emitted_when_explicitly_configured() {
        // When `[readme.languages.rust].output_path` is set, alef honors the
        // explicit opt-in and emits the README at that path.
        let mut config = test_config();
        let mut readme_cfg = crate::core::config::ReadmeConfig {
            template_dir: None,
            snippets_dir: None,
            config: None,
            output_pattern: None,
            discord_url: None,
            banner_url: None,
            languages: std::collections::HashMap::new(),
        };
        readme_cfg.languages.insert(
            "rust".to_string(),
            serde_json::json!({ "output_path": "crates/my-lib/README.md" }),
        );
        config.readme = Some(readme_cfg);
        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Rust]).unwrap();
        assert_eq!(files.len(), 1);
        // Hardcoded fallback ignores output_path (it only honors output_pattern via
        // readme_output_path which is only called from the template path). The
        // opt-in gate is what matters here — the file is emitted, not silently dropped.
        assert!(files[0].content.contains("Rust"));
        assert!(files[0].content.contains("cargo add"));
    }

    // --- hardcoded fallback: no scaffold config (default description/repository) ---

    #[test]
    fn test_generate_readme_without_scaffold_uses_placeholder() {
        let mut config = test_config();
        config.scaffold = None;
        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
        assert_eq!(files.len(), 1);
        assert!(
            files[0].content.contains("Bindings for my-lib"),
            "Expected default description, got: {}",
            files[0].content
        );
        // When [scaffold] repository is unset, the README falls back to a
        // vendor-neutral placeholder (`example.invalid`) rather than smuggling
        // another organization's URL into the output.
        assert!(
            files[0].content.contains("https://example.invalid/my-lib"),
            "Expected vendor-neutral placeholder URL, got: {}",
            files[0].content
        );
    }

    // --- capitalize_first ---

    #[test]
    fn test_capitalize_first_normal() {
        assert_eq!(capitalize_first("hello"), "Hello");
    }

    #[test]
    fn test_capitalize_first_empty() {
        assert_eq!(capitalize_first(""), "");
    }

    #[test]
    fn test_capitalize_first_already_upper() {
        assert_eq!(capitalize_first("World"), "World");
    }

    // --- extract_code_block: tilde fence ---

    #[test]
    fn test_extract_code_block_tilde_fence() {
        let md = "~~~python\nprint('hi')\n~~~\n";
        let result = extract_code_block(md);
        assert!(result.contains("~~~python"), "Got: {result}");
        assert!(result.contains("print('hi')"), "Got: {result}");
    }

    // --- include_snippet: file exists ---

    #[test]
    fn test_include_snippet_non_md_file() {
        let tmp = std::env::temp_dir().join("alef_readme_snippet_test_py");
        let _ = fs::remove_dir_all(&tmp);
        let lang_dir = tmp.join("python");
        fs::create_dir_all(&lang_dir).unwrap();
        fs::write(lang_dir.join("example.py"), "print('hello')").unwrap();

        let result = include_snippet(&tmp, "python", "example.py");
        assert!(result.contains("```py"), "Got: {result}");
        assert!(result.contains("print('hello')"), "Got: {result}");

        let _ = fs::remove_dir_all(&tmp);
    }

    #[test]
    fn test_include_snippet_md_file_extracts_code_block() {
        let tmp = std::env::temp_dir().join("alef_readme_snippet_test_md");
        let _ = fs::remove_dir_all(&tmp);
        let lang_dir = tmp.join("python");
        fs::create_dir_all(&lang_dir).unwrap();
        fs::write(
            lang_dir.join("example.md"),
            "Some prose\n\n```python\nfoo()\n```\n\nMore prose",
        )
        .unwrap();

        let result = include_snippet(&tmp, "python", "example.md");
        assert!(result.contains("```python"), "Got: {result}");
        assert!(result.contains("foo()"), "Got: {result}");

        let _ = fs::remove_dir_all(&tmp);
    }

    // --- render_performance_table: ops/sec table ---

    #[test]
    fn test_render_performance_table_ops_sec() {
        let perf = serde_json::json!({
            "platform": "Apple M2",
            "function": "parse",
            "note": "single-threaded",
            "benchmarks": [
                {"name": "small.json", "size": "1 KB", "ops_sec": 12345},
                {"name": "large.json", "size": "1 MB", "ops_sec": 42}
            ]
        });
        let v = Value::from_serialize(&perf);
        let result = render_performance_table(&v, "parse");
        assert!(result.contains("Apple M2"), "Got: {result}");
        assert!(result.contains("| Document | Size | Ops/sec |"), "Got: {result}");
        assert!(result.contains("small.json"), "Got: {result}");
        assert!(result.contains("large.json"), "Got: {result}");
    }

    // --- render_performance_table: throughput table ---

    #[test]
    fn test_render_performance_table_throughput() {
        let perf = serde_json::json!({
            "platform": "Linux x86-64",
            "function": "extract",
            "note": "4 threads",
            "benchmarks": [
                {
                    "name": "doc.pdf",
                    "size": "2 MB",
                    "latency": "10ms",
                    "throughput": "100 MB/s"
                }
            ]
        });
        let v = Value::from_serialize(&perf);
        let result = render_performance_table(&v, "extract");
        assert!(
            result.contains("| Document | Size | Latency | Throughput |"),
            "Got: {result}"
        );
        assert!(result.contains("doc.pdf"), "Got: {result}");
        assert!(result.contains("100 MB/s"), "Got: {result}");
        assert!(
            result.contains("4 threads\n\n| Document"),
            "Expected blank line between context and table header. Got: {result}"
        );
    }

    // --- readme_output_path: output_pattern branch ---

    #[test]
    fn test_template_with_output_pattern() {
        let tmp = std::env::temp_dir().join("alef_readme_test_output_pattern");
        let _ = fs::remove_dir_all(&tmp);
        fs::create_dir_all(&tmp).unwrap();
        fs::write(tmp.join("lang.md"), "# {{ name }}").unwrap();

        let mut config = test_config();
        let mut lang_map = std::collections::HashMap::new();
        // No output_path key — let output_pattern drive the path
        lang_map.insert(
            "python".to_string(),
            serde_json::json!({
                "template": "lang.md"
            }),
        );
        config.readme = Some(ReadmeConfig {
            template_dir: Some(tmp.clone()),
            snippets_dir: None,
            config: None,
            output_pattern: Some("docs/{language}/README.md".to_string()),
            discord_url: None,
            banner_url: None,
            languages: lang_map,
        });
        config.workspace_root = Some(tmp.clone());

        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
        assert_eq!(files.len(), 1);
        assert_eq!(files[0].path, PathBuf::from("docs/python/README.md"));

        let _ = fs::remove_dir_all(&tmp);
    }

    // --- try_template_readme: template file missing falls back to hardcoded ---

    #[test]
    fn test_template_readme_missing_template_falls_back() {
        let tmp = std::env::temp_dir().join("alef_readme_test_missing_tmpl");
        let _ = fs::remove_dir_all(&tmp);
        fs::create_dir_all(&tmp).unwrap();
        // No template file written — the entry points at a file that does not exist

        let mut config = test_config();
        let mut lang_map = std::collections::HashMap::new();
        lang_map.insert(
            "python".to_string(),
            serde_json::json!({
                "template": "nonexistent.md",
                "output_path": "packages/python/README.md"
            }),
        );
        config.readme = Some(ReadmeConfig {
            template_dir: Some(tmp.clone()),
            snippets_dir: None,
            config: None,
            output_pattern: None,
            discord_url: None,
            banner_url: None,
            languages: lang_map,
        });
        config.workspace_root = Some(tmp.clone());

        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
        assert_eq!(files.len(), 1);
        // Falls back to hardcoded — must contain pip install
        assert!(
            files[0].content.contains("pip install"),
            "Expected hardcoded fallback content, got: {}",
            files[0].content
        );

        let _ = fs::remove_dir_all(&tmp);
    }

    // --- try_template_readme: no language entry falls back to hardcoded ---

    #[test]
    fn test_template_readme_no_lang_entry_falls_back() {
        let tmp = std::env::temp_dir().join("alef_readme_test_no_lang_entry");
        let _ = fs::remove_dir_all(&tmp);
        fs::create_dir_all(&tmp).unwrap();

        let mut config = test_config();
        // Languages map is empty — no entry for Python
        config.readme = Some(ReadmeConfig {
            template_dir: Some(tmp.clone()),
            snippets_dir: None,
            config: None,
            output_pattern: None,
            discord_url: None,
            banner_url: None,
            languages: std::collections::HashMap::new(),
        });
        config.workspace_root = Some(tmp.clone());

        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
        assert_eq!(files.len(), 1);
        assert!(files[0].content.contains("pip install"));

        let _ = fs::remove_dir_all(&tmp);
    }

    // --- try_template_readme: deprecated YAML config path ---

    #[test]
    fn test_template_readme_yaml_config() {
        let tmp = std::env::temp_dir().join("alef_readme_test_yaml_cfg");
        let _ = fs::remove_dir_all(&tmp);
        fs::create_dir_all(&tmp).unwrap();

        fs::write(tmp.join("tmpl.md"), "version={{ version }}").unwrap();
        let yaml_content = r#"
languages:
  python:
    template: tmpl.md
    output_path: packages/python/README.md
"#;
        fs::write(tmp.join("readme.yaml"), yaml_content).unwrap();

        let mut config = test_config();
        config.readme = Some(ReadmeConfig {
            template_dir: Some(tmp.clone()),
            snippets_dir: None,
            config: Some(PathBuf::from("readme.yaml")),
            output_pattern: None,
            discord_url: None,
            banner_url: None,
            languages: std::collections::HashMap::new(), // empty — triggers YAML path
        });
        config.workspace_root = Some(tmp.clone());

        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
        assert_eq!(files.len(), 1);
        assert!(
            files[0].content.contains("version=0.1.0"),
            "Expected rendered version, got: {}",
            files[0].content
        );

        let _ = fs::remove_dir_all(&tmp);
    }

    // --- try_template_readme: discord_url and banner_url in context ---

    #[test]
    fn test_template_readme_discord_and_banner_url() {
        let tmp = std::env::temp_dir().join("alef_readme_test_discord_banner");
        let _ = fs::remove_dir_all(&tmp);
        fs::create_dir_all(&tmp).unwrap();

        fs::write(tmp.join("t.md"), "{{ discord_url }}|{{ banner_url }}").unwrap();

        let mut config = test_config();
        let mut lang_map = std::collections::HashMap::new();
        lang_map.insert(
            "python".to_string(),
            serde_json::json!({
                "template": "t.md",
                "output_path": "packages/python/README.md"
            }),
        );
        config.readme = Some(ReadmeConfig {
            template_dir: Some(tmp.clone()),
            snippets_dir: None,
            config: None,
            output_pattern: None,
            discord_url: Some("https://discord.gg/test".to_string()),
            banner_url: Some("https://img.example.com/banner.png".to_string()),
            languages: lang_map,
        });
        config.workspace_root = Some(tmp.clone());

        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
        assert_eq!(files.len(), 1);
        assert!(
            files[0].content.contains("https://discord.gg/test"),
            "Got: {}",
            files[0].content
        );
        assert!(
            files[0].content.contains("https://img.example.com/banner.png"),
            "Got: {}",
            files[0].content
        );

        let _ = fs::remove_dir_all(&tmp);
    }

    // --- try_template_readme: template without scaffold (default description/repo/license) ---

    #[test]
    fn test_template_readme_no_scaffold_uses_defaults() {
        let tmp = std::env::temp_dir().join("alef_readme_test_no_scaffold");
        let _ = fs::remove_dir_all(&tmp);
        fs::create_dir_all(&tmp).unwrap();

        fs::write(tmp.join("t.md"), "{{ description }}|{{ repository }}|{{ license }}").unwrap();

        let mut config = test_config();
        config.scaffold = None;
        let mut lang_map = std::collections::HashMap::new();
        lang_map.insert(
            "python".to_string(),
            serde_json::json!({
                "template": "t.md",
                "output_path": "packages/python/README.md"
            }),
        );
        config.readme = Some(ReadmeConfig {
            template_dir: Some(tmp.clone()),
            snippets_dir: None,
            config: None,
            output_pattern: None,
            discord_url: None,
            banner_url: None,
            languages: lang_map,
        });
        config.workspace_root = Some(tmp.clone());

        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
        assert_eq!(files.len(), 1);
        assert!(
            files[0].content.contains("Bindings for my-lib"),
            "Got: {}",
            files[0].content
        );
        assert!(
            files[0].content.contains("https://example.invalid/my-lib"),
            "Got: {}",
            files[0].content
        );
        assert!(files[0].content.contains("MIT"), "Got: {}", files[0].content);

        let _ = fs::remove_dir_all(&tmp);
    }

    // --- try_template_readme: content already ends with newline (no double-newline) ---

    #[test]
    fn test_template_readme_trailing_newline_not_doubled() {
        let tmp = std::env::temp_dir().join("alef_readme_test_trailing_newline");
        let _ = fs::remove_dir_all(&tmp);
        fs::create_dir_all(&tmp).unwrap();

        // Template content ends with a literal newline already
        fs::write(tmp.join("t.md"), "hello\n").unwrap();

        let mut config = test_config();
        let mut lang_map = std::collections::HashMap::new();
        lang_map.insert(
            "python".to_string(),
            serde_json::json!({
                "template": "t.md",
                "output_path": "packages/python/README.md"
            }),
        );
        config.readme = Some(ReadmeConfig {
            template_dir: Some(tmp.clone()),
            snippets_dir: None,
            config: None,
            output_pattern: None,
            discord_url: None,
            banner_url: None,
            languages: lang_map,
        });
        config.workspace_root = Some(tmp.clone());

        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
        assert_eq!(files.len(), 1);
        assert!(files[0].content.ends_with('\n'), "Must end with newline");
        assert!(
            !files[0].content.ends_with("\n\n"),
            "Must not have double trailing newline, got: {:?}",
            files[0].content
        );

        let _ = fs::remove_dir_all(&tmp);
    }

    // --- default_readme_path: all language variants ---

    #[test]
    fn test_default_readme_path_ffi() {
        let config = test_config();
        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Ffi]).unwrap();
        assert_eq!(files[0].path, PathBuf::from("crates/my-lib-ffi/README.md"));
    }

    #[test]
    fn test_default_readme_path_wasm() {
        let config = test_config();
        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Wasm]).unwrap();
        assert_eq!(files[0].path, PathBuf::from("crates/my-lib-wasm/README.md"));
    }

    #[test]
    fn test_default_readme_path_node() {
        let config = test_config();
        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Node]).unwrap();
        assert_eq!(files[0].path, PathBuf::from("crates/my-lib-node/README.md"));
    }

    #[test]
    fn test_default_readme_path_rust_when_explicitly_configured() {
        let mut config = test_config();
        let mut readme_cfg = ReadmeConfig {
            template_dir: None,
            snippets_dir: None,
            config: None,
            output_pattern: None,
            discord_url: None,
            banner_url: None,
            languages: std::collections::HashMap::new(),
        };
        // Opt in via output_path so the Rust README is not silently skipped.
        readme_cfg.languages.insert(
            "rust".to_string(),
            serde_json::json!({ "output_path": "crates/my-lib/README.md" }),
        );
        config.readme = Some(readme_cfg);
        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Rust]).unwrap();
        assert_eq!(files[0].path, PathBuf::from("crates/my-lib/README.md"));
    }

    // --- readme_output_path: "output" key alias ---

    #[test]
    fn test_template_output_key_alias() {
        let tmp = std::env::temp_dir().join("alef_readme_test_output_alias");
        let _ = fs::remove_dir_all(&tmp);
        fs::create_dir_all(&tmp).unwrap();
        fs::write(tmp.join("t.md"), "hello").unwrap();

        let mut config = test_config();
        let mut lang_map = std::collections::HashMap::new();
        // Use "output" instead of "output_path" to exercise the .or_else() branch
        lang_map.insert(
            "python".to_string(),
            serde_json::json!({
                "template": "t.md",
                "output": "custom/path/README.md"
            }),
        );
        config.readme = Some(ReadmeConfig {
            template_dir: Some(tmp.clone()),
            snippets_dir: None,
            config: None,
            output_pattern: None,
            discord_url: None,
            banner_url: None,
            languages: lang_map,
        });
        config.workspace_root = Some(tmp.clone());

        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
        assert_eq!(files.len(), 1);
        assert_eq!(files[0].path, PathBuf::from("custom/path/README.md"));

        let _ = fs::remove_dir_all(&tmp);
    }

    // --- readme_output_path: falls through to default_readme_path ---

    #[test]
    fn test_template_readme_default_path_fallthrough() {
        let tmp = std::env::temp_dir().join("alef_readme_test_default_path");
        let _ = fs::remove_dir_all(&tmp);
        fs::create_dir_all(&tmp).unwrap();
        fs::write(tmp.join("t.md"), "hello").unwrap();

        let mut config = test_config();
        let mut lang_map = std::collections::HashMap::new();
        // No output_path, no output key, no output_pattern — falls through to default_readme_path
        lang_map.insert("python".to_string(), serde_json::json!({ "template": "t.md" }));
        config.readme = Some(ReadmeConfig {
            template_dir: Some(tmp.clone()),
            snippets_dir: None,
            config: None,
            output_pattern: None,
            discord_url: None,
            banner_url: None,
            languages: lang_map,
        });
        config.workspace_root = Some(tmp.clone());

        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
        assert_eq!(files.len(), 1);
        assert_eq!(files[0].path, PathBuf::from("packages/python/README.md"));

        let _ = fs::remove_dir_all(&tmp);
    }

    // --- try_template_readme: missing snippets key does not error ---

    #[test]
    fn test_template_readme_missing_snippets_renders_gracefully() {
        // Reproduces: sample_crate ffi language config has no `snippets` key.
        // The quick_start partial accesses `snippets.basic_extraction` unconditionally;
        // if `snippets` is absent from the context the render must not error.
        let tmp = std::env::temp_dir().join("alef_readme_test_missing_snippets");
        let _ = fs::remove_dir_all(&tmp);
        let partials_dir = tmp.join("partials");
        fs::create_dir_all(&partials_dir).unwrap();

        // Minimal partial that accesses snippets.basic_extraction unconditionally
        fs::write(
            partials_dir.join("quick_start.md.jinja"),
            "{{ snippets.basic_extraction | include_snippet(language) }}",
        )
        .unwrap();
        fs::write(
            tmp.join("language_package.md"),
            "{% include 'partials/quick_start.md.jinja' %}",
        )
        .unwrap();

        let mut config = test_config();
        let mut lang_map = std::collections::HashMap::new();
        // No `snippets` key — mirrors sample_crate's ffi language block
        lang_map.insert(
            "ffi".to_string(),
            serde_json::json!({
                "template": "language_package.md",
                "output_path": "crates/my-lib-ffi/README.md",
                "name": "FFI"
            }),
        );
        config.readme = Some(ReadmeConfig {
            template_dir: Some(tmp.clone()),
            snippets_dir: None,
            config: None,
            output_pattern: None,
            discord_url: None,
            banner_url: None,
            languages: lang_map,
        });
        config.workspace_root = Some(tmp.clone());

        let api = test_api();
        // Must not error even though `snippets` is absent from the language config
        let files = generate_readmes(&api, &config, &[Language::Ffi]).unwrap();
        assert_eq!(files.len(), 1);

        let _ = fs::remove_dir_all(&tmp);
    }

    // --- include_snippet: snippets_dir in template context (filter exercised via template) ---

    #[test]
    fn test_template_include_snippet_filter() {
        let tmp = std::env::temp_dir().join("alef_readme_test_snippet_filter");
        let _ = fs::remove_dir_all(&tmp);
        let snippets_dir = tmp.join("snippets");
        let lang_snippet_dir = snippets_dir.join("python");
        fs::create_dir_all(&lang_snippet_dir).unwrap();
        fs::write(lang_snippet_dir.join("hello.py"), "print('hi')").unwrap();
        // Template exercises the include_snippet filter
        fs::write(tmp.join("t.md"), r#"{{ "hello.py" | include_snippet("python") }}"#).unwrap();

        let mut config = test_config();
        let mut lang_map = std::collections::HashMap::new();
        lang_map.insert(
            "python".to_string(),
            serde_json::json!({
                "template": "t.md",
                "output_path": "packages/python/README.md"
            }),
        );
        config.readme = Some(ReadmeConfig {
            template_dir: Some(tmp.clone()),
            snippets_dir: Some(PathBuf::from("snippets")),
            config: None,
            output_pattern: None,
            discord_url: None,
            banner_url: None,
            languages: lang_map,
        });
        config.workspace_root = Some(tmp.clone());

        let api = test_api();
        let files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
        assert_eq!(files.len(), 1);
        assert!(
            files[0].content.contains("print('hi')"),
            "Expected snippet content, got: {}",
            files[0].content
        );

        let _ = fs::remove_dir_all(&tmp);
    }

    // --- STY-5 regression test: alef all vs cold alef readme produce identical READMEs ---
    //
    // This test verifies that README generation is consistent regardless of whether it's
    // called after scaffold (as in `alef all`) or in isolation (as in `alef readme`).
    // Previously, alef all would produce READMEs with extra blank lines and different table
    // cell padding compared to cold alef readme, causing CI gate failures even when nothing
    // else changed. The root cause was suspected to be state pollution from earlier pipeline
    // stages or inconsistent minijinja environment setup.
    //
    // This test simulates both paths:
    // Path A: readme generation in isolation (cold path, what `alef readme` does)
    // Path B: readme generation after some operations (warm path, what `alef all` does)
    // Both should produce identical README content.
    #[test]
    fn test_alef_all_and_cold_readme_produce_same_output() {
        let tmp = std::env::temp_dir().join("alef_sty5_test");
        let _ = fs::remove_dir_all(&tmp);
        fs::create_dir_all(&tmp).unwrap();

        // Create a minimal template to test with
        fs::create_dir_all(tmp.join("templates")).unwrap();

        // Template that exercises key paths: variables, performance table
        let template_content = r#"# {{name}}

{{description}}

## Features

- Item 1
- Item 2

{% if performance %}
## Performance

{{ performance | render_performance_table(name) }}
{% endif %}

## Installation

{{ install_command }}
"#;
        fs::write(tmp.join("templates/test.md"), template_content).unwrap();

        // Set up a config with template-based README generation
        let mut config = test_config();
        config.workspace_root = Some(tmp.clone());

        let mut lang_map = std::collections::HashMap::new();
        lang_map.insert(
            "python".to_string(),
            serde_json::json!({
                "template": "test.md",
                "output_path": "packages/python/README.md",
                "install_command": "pip install my-lib==0.1.0",
                "performance": {
                    "platform": "Apple M4",
                    "function": "convert()",
                    "note": "Test doc",
                    "benchmarks": [
                        {
                            "name": "Small",
                            "size": "10KB",
                            "latency": "1.0ms",
                            "throughput": "10 MB/s"
                        },
                        {
                            "name": "Large",
                            "size": "1MB",
                            "latency": "10.0ms",
                            "throughput": "100 MB/s"
                        }
                    ]
                }
            }),
        );
        config.readme = Some(ReadmeConfig {
            template_dir: Some(PathBuf::from("templates")),
            snippets_dir: None,
            config: None,
            output_pattern: None,
            discord_url: None,
            banner_url: None,
            languages: lang_map,
        });

        let api = test_api();

        // Path A: readme generation in isolation (cold path)
        let cold_files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
        assert_eq!(cold_files.len(), 1);
        let cold_content = &cold_files[0].content;

        // Path B: readme generation again (warm path, simulating alef all)
        // In real alef all, scaffold runs first, but that shouldn't affect readme input.
        // If there's state pollution, it would show up here.
        let warm_files = generate_readmes(&api, &config, &[Language::Python]).unwrap();
        assert_eq!(warm_files.len(), 1);
        let warm_content = &warm_files[0].content;

        // Both paths should produce identical output
        if cold_content != warm_content {
            eprintln!("=== COLD OUTPUT ===\n{}\n", cold_content);
            eprintln!("=== WARM OUTPUT ===\n{}\n", warm_content);
            eprintln!("=== DIFF (cold vs warm) ===");
            for (i, (c, w)) in cold_content.lines().zip(warm_content.lines()).enumerate() {
                if c != w {
                    eprintln!("Line {}: COLD: {}", i + 1, c);
                    eprintln!("Line {}: WARM: {}", i + 1, w);
                }
            }
        }
        assert_eq!(
            cold_content, warm_content,
            "README generation must be deterministic: alef readme and alef all must produce identical output (STY-5 regression)"
        );

        let _ = fs::remove_dir_all(&tmp);
    }
}
