use crate::core::ir::{DefaultValue, FieldDef, PrimitiveType, TypeDef, TypeRef};
use heck::{ToPascalCase, ToShoutySnakeCase, ToSnakeCase};

/// Returns true if a field is a tuple struct positional field (e.g., `_0`, `_1`, `0`, `1`).
/// These fields have no meaningful name and must be skipped in languages requiring named fields.
fn is_tuple_field(field: &FieldDef) -> bool {
    (field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit()))
        || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
}

/// Returns true if the Rust default value for a field is its type's inherent default,
/// meaning `.unwrap_or_default()` can be used instead of `.unwrap_or(value)`.
/// This avoids clippy::unwrap_or_default warnings.
fn use_unwrap_or_default(field: &FieldDef) -> bool {
    if let Some(typed_default) = &field.typed_default {
        return matches!(typed_default, DefaultValue::Empty | DefaultValue::None);
    }
    // No typed_default — the fallback default_value_for_field generates type-based zero values
    // which are the same as Default::default() for the type.
    // Named types may not implement Default in some bindings (e.g. Magnus), so they
    // fall through to the explicit default path.
    field.default.is_none() && !matches!(&field.ty, TypeRef::Named(_))
}

fn constructor_fields(typ: &TypeDef) -> impl Iterator<Item = &FieldDef> {
    typ.fields.iter().filter(|field| !field.binding_excluded)
}

/// Generate a PyO3 `#[new]` constructor with kwargs for a type with `has_default`.
/// All fields become keyword args with their defaults in `#[pyo3(signature = (...))]`.
pub fn gen_pyo3_kwargs_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
    let signature_defaults = constructor_fields(typ)
        .map(|field| format!("{}={}", field.name, default_value_for_field(field, "python")))
        .collect::<Vec<_>>()
        .join(", ");
    let fields: Vec<_> = constructor_fields(typ)
        .map(|field| {
            minijinja::context! {
                name => field.name.clone(),
                type => type_mapper(&field.ty),
            }
        })
        .collect();

    crate::codegen::template_env::render(
        "config_gen/pyo3_kwargs_constructor.jinja",
        minijinja::context! {
            signature_defaults => signature_defaults,
            fields => fields,
        },
    )
    .trim_end_matches('\n')
    .to_string()
}

/// Generate NAPI constructor that applies defaults for missing optional fields.
pub fn gen_napi_defaults_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
    let fields: Vec<_> = constructor_fields(typ)
        .map(|field| {
            minijinja::context! {
                name => field.name.clone(),
                type => type_mapper(&field.ty),
                default => default_value_for_field(field, "rust"),
            }
        })
        .collect();

    crate::codegen::template_env::render(
        "config_gen/napi_defaults_constructor.jinja",
        minijinja::context! {
            fields => fields,
        },
    )
    .trim_end_matches('\n')
    .to_string()
}

/// Generate Go functional options pattern for a type with `has_default`.
/// Returns: type definition + Option type + WithField functions + NewConfig constructor
pub fn gen_go_functional_options(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
    let fields: Vec<_> = constructor_fields(typ)
        .filter(|field| !is_tuple_field(field))
        .map(|field| {
            minijinja::context! {
                name => field.name.clone(),
                pascal_name => field.name.to_pascal_case(),
                field_name => field.name.to_pascal_case(),
                go_type => type_mapper(&field.ty),
                default => default_value_for_field(field, "go"),
            }
        })
        .collect();

    crate::codegen::template_env::render(
        "config_gen/go_functional_options.jinja",
        minijinja::context! {
            type_name => typ.name.clone(),
            fields => fields,
        },
    )
    .trim_end_matches('\n')
    .to_string()
}

/// Generate Java builder pattern for a type with `has_default`.
/// Returns: Builder inner class with withField methods + build() method
pub fn gen_java_builder(typ: &TypeDef, package: &str, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
    let fields: Vec<_> = typ
        .fields
        .iter()
        .map(|field| {
            minijinja::context! {
                name_lower => field.name.to_lowercase(),
                type => type_mapper(&field.ty),
                default => default_value_for_field(field, "java"),
                method_name => format!("with{}", field.name.to_pascal_case()),
            }
        })
        .collect();

    crate::codegen::template_env::render(
        "config_gen/java_builder.jinja",
        minijinja::context! {
            package => package,
            type_name => typ.name.clone(),
            fields => fields,
        },
    )
    .trim_end_matches('\n')
    .to_string()
}

/// Generate C# record with init properties for a type with `has_default`.
pub fn gen_csharp_record(typ: &TypeDef, namespace: &str, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
    let fields: Vec<_> = constructor_fields(typ)
        .filter(|field| !is_tuple_field(field))
        .map(|field| {
            minijinja::context! {
                type => type_mapper(&field.ty),
                name_pascal => field.name.to_pascal_case(),
                default => default_value_for_field(field, "csharp"),
            }
        })
        .collect();

    crate::codegen::template_env::render(
        "config_gen/csharp_record.jinja",
        minijinja::context! {
            namespace => namespace,
            type_name => typ.name.clone(),
            fields => fields,
        },
    )
    .trim_end_matches('\n')
    .to_string()
}

/// Get a language-appropriate default value string for a field.
/// Uses `typed_default` if available, falls back to `default` string, or type-based zero value.
pub fn default_value_for_field(field: &FieldDef, language: &str) -> String {
    // First try typed_default if it exists
    if let Some(typed_default) = &field.typed_default {
        return match typed_default {
            DefaultValue::BoolLiteral(b) => match language {
                "python" => {
                    if *b {
                        "True".to_string()
                    } else {
                        "False".to_string()
                    }
                }
                "ruby" => {
                    if *b {
                        "true".to_string()
                    } else {
                        "false".to_string()
                    }
                }
                "go" => {
                    if *b {
                        "true".to_string()
                    } else {
                        "false".to_string()
                    }
                }
                "java" => {
                    if *b {
                        "true".to_string()
                    } else {
                        "false".to_string()
                    }
                }
                "csharp" => {
                    if *b {
                        "true".to_string()
                    } else {
                        "false".to_string()
                    }
                }
                "php" => {
                    if *b {
                        "true".to_string()
                    } else {
                        "false".to_string()
                    }
                }
                "r" => {
                    if *b {
                        "TRUE".to_string()
                    } else {
                        "FALSE".to_string()
                    }
                }
                "rust" => {
                    if *b {
                        "true".to_string()
                    } else {
                        "false".to_string()
                    }
                }
                _ => {
                    if *b {
                        "true".to_string()
                    } else {
                        "false".to_string()
                    }
                }
            },
            DefaultValue::StringLiteral(s) => match language {
                "rust" => format!("\"{}\".to_string()", s.replace('"', "\\\"")),
                _ => format!("\"{}\"", s.replace('"', "\\\"")),
            },
            DefaultValue::IntLiteral(n) => n.to_string(),
            DefaultValue::FloatLiteral(f) => {
                let s = f.to_string();
                if !s.contains('.') { format!("{}.0", s) } else { s }
            }
            DefaultValue::EnumVariant(v) => {
                // When the field's original enum type was excluded/sanitized and mapped to
                // String, we must emit a string literal rather than an enum type path.
                // Example: OutputFormat::Plain → "plain".to_string() (Rust), "plain" (others).
                if matches!(field.ty, TypeRef::String) {
                    let snake = v.to_snake_case();
                    return match language {
                        "rust" => format!("\"{}\".to_string()", snake),
                        _ => format!("\"{}\"", snake),
                    };
                }
                match language {
                    "python" => format!("{}.{}", field.ty.type_name(), v.to_shouty_snake_case()),
                    "ruby" => format!("{}::{}", field.ty.type_name(), v.to_pascal_case()),
                    "go" => format!("{}{}", field.ty.type_name(), v.to_pascal_case()),
                    "java" => format!("{}.{}", field.ty.type_name(), v.to_shouty_snake_case()),
                    "csharp" => format!("{}.{}", field.ty.type_name(), v.to_pascal_case()),
                    "php" => format!("{}::{}", field.ty.type_name(), v.to_pascal_case()),
                    "r" => format!("{}${}", field.ty.type_name(), v.to_pascal_case()),
                    "rust" => format!("{}::{}", field.ty.type_name(), v.to_pascal_case()),
                    _ => v.clone(),
                }
            }
            DefaultValue::Empty => {
                // Empty means "type's default" — check field type to pick the right zero value
                match &field.ty {
                    TypeRef::Vec(_) => match language {
                        "python" | "ruby" | "csharp" => "[]".to_string(),
                        "go" => "nil".to_string(),
                        "java" => "List.of()".to_string(),
                        "php" => "[]".to_string(),
                        "r" => "c()".to_string(),
                        "rust" => "vec![]".to_string(),
                        _ => "null".to_string(),
                    },
                    TypeRef::Map(_, _) => match language {
                        "python" => "{}".to_string(),
                        "go" => "nil".to_string(),
                        "java" => "Map.of()".to_string(),
                        "rust" => "Default::default()".to_string(),
                        _ => "null".to_string(),
                    },
                    TypeRef::Primitive(p) => match p {
                        PrimitiveType::Bool => match language {
                            "python" => "False".to_string(),
                            "ruby" => "false".to_string(),
                            _ => "false".to_string(),
                        },
                        PrimitiveType::F32 | PrimitiveType::F64 => "0.0".to_string(),
                        _ => "0".to_string(),
                    },
                    TypeRef::String | TypeRef::Char | TypeRef::Path => match language {
                        "rust" => "String::new()".to_string(),
                        _ => "\"\"".to_string(),
                    },
                    TypeRef::Json => match language {
                        "python" | "ruby" => "{}".to_string(),
                        "go" => "json.RawMessage(nil)".to_string(),
                        "java" => "new com.fasterxml.jackson.databind.node.ObjectNode(null)".to_string(),
                        "csharp" => "JObject.Parse(\"{}\")".to_string(),
                        "php" => "[]".to_string(),
                        "r" => "list()".to_string(),
                        "rust" => "serde_json::json!({})".to_string(),
                        _ => "{}".to_string(),
                    },
                    TypeRef::Duration => "0".to_string(),
                    TypeRef::Bytes => match language {
                        "python" => "b\"\"".to_string(),
                        "go" => "[]byte{}".to_string(),
                        "rust" => "vec![]".to_string(),
                        _ => "\"\"".to_string(),
                    },
                    _ => match language {
                        "python" => "None".to_string(),
                        "ruby" => "nil".to_string(),
                        "go" => "nil".to_string(),
                        "rust" => "Default::default()".to_string(),
                        _ => "null".to_string(),
                    },
                }
            }
            DefaultValue::None => match language {
                "python" => "None".to_string(),
                "ruby" => "nil".to_string(),
                "go" => "nil".to_string(),
                "java" => "null".to_string(),
                "csharp" => "null".to_string(),
                "php" => "null".to_string(),
                "r" => "NULL".to_string(),
                "rust" => "None".to_string(),
                _ => "null".to_string(),
            },
        };
    }

    // Fall back to string default if it exists. The extractor sets a
    // "/* serde(default) */" placeholder for fields with #[serde(default)]
    // but no explicit value — this is a signal flag for backends that need
    // to skip required-field markers, NOT a real value to emit. Treat it as
    // absent and fall through to the type-based zero value below.
    if let Some(default_str) = &field.default
        && default_str != "/* serde(default) */"
    {
        return default_str.clone();
    }

    // Final fallback: type-based zero value
    match &field.ty {
        TypeRef::Primitive(p) => match p {
            crate::core::ir::PrimitiveType::Bool => match language {
                "python" => "False".to_string(),
                "ruby" => "false".to_string(),
                "csharp" => "false".to_string(),
                "java" => "false".to_string(),
                "php" => "false".to_string(),
                "r" => "FALSE".to_string(),
                _ => "false".to_string(),
            },
            crate::core::ir::PrimitiveType::U8
            | crate::core::ir::PrimitiveType::U16
            | crate::core::ir::PrimitiveType::U32
            | crate::core::ir::PrimitiveType::U64
            | crate::core::ir::PrimitiveType::I8
            | crate::core::ir::PrimitiveType::I16
            | crate::core::ir::PrimitiveType::I32
            | crate::core::ir::PrimitiveType::I64
            | crate::core::ir::PrimitiveType::Usize
            | crate::core::ir::PrimitiveType::Isize => "0".to_string(),
            crate::core::ir::PrimitiveType::F32 | crate::core::ir::PrimitiveType::F64 => "0.0".to_string(),
        },
        TypeRef::String | TypeRef::Char => match language {
            "python" => "\"\"".to_string(),
            "ruby" => "\"\"".to_string(),
            "go" => "\"\"".to_string(),
            "java" => "\"\"".to_string(),
            "csharp" => "\"\"".to_string(),
            "php" => "\"\"".to_string(),
            "r" => "\"\"".to_string(),
            "rust" => "String::new()".to_string(),
            _ => "\"\"".to_string(),
        },
        TypeRef::Bytes => match language {
            "python" => "b\"\"".to_string(),
            "ruby" => "\"\"".to_string(),
            "go" => "[]byte{}".to_string(),
            "java" => "new byte[]{}".to_string(),
            "csharp" => "new byte[]{}".to_string(),
            "php" => "\"\"".to_string(),
            "r" => "raw()".to_string(),
            "rust" => "vec![]".to_string(),
            _ => "[]".to_string(),
        },
        TypeRef::Optional(_) => match language {
            "python" => "None".to_string(),
            "ruby" => "nil".to_string(),
            "go" => "nil".to_string(),
            "java" => "null".to_string(),
            "csharp" => "null".to_string(),
            "php" => "null".to_string(),
            "r" => "NULL".to_string(),
            "rust" => "None".to_string(),
            _ => "null".to_string(),
        },
        TypeRef::Vec(_) => match language {
            "python" => "[]".to_string(),
            "ruby" => "[]".to_string(),
            "go" => "[]interface{}{}".to_string(),
            "java" => "new java.util.ArrayList<>()".to_string(),
            "csharp" => "[]".to_string(),
            "php" => "[]".to_string(),
            "r" => "c()".to_string(),
            "rust" => "vec![]".to_string(),
            _ => "[]".to_string(),
        },
        TypeRef::Map(_, _) => match language {
            "python" => "{}".to_string(),
            "ruby" => "{}".to_string(),
            "go" => "make(map[string]interface{})".to_string(),
            "java" => "new java.util.HashMap<>()".to_string(),
            "csharp" => "new Dictionary<string, object>()".to_string(),
            "php" => "[]".to_string(),
            "r" => "list()".to_string(),
            "rust" => "std::collections::HashMap::new()".to_string(),
            _ => "{}".to_string(),
        },
        TypeRef::Json => match language {
            "python" => "{}".to_string(),
            "ruby" => "{}".to_string(),
            "go" => "json.RawMessage(nil)".to_string(),
            "java" => "new com.fasterxml.jackson.databind.JsonNode()".to_string(),
            "csharp" => "JObject.Parse(\"{}\")".to_string(),
            "php" => "[]".to_string(),
            "r" => "list()".to_string(),
            "rust" => "serde_json::json!({})".to_string(),
            _ => "{}".to_string(),
        },
        TypeRef::Named(name) => match language {
            "rust" => format!("{name}::default()"),
            "python" => "None".to_string(),
            "ruby" => "nil".to_string(),
            "go" => "nil".to_string(),
            "java" => "null".to_string(),
            "csharp" => "null".to_string(),
            "php" => "null".to_string(),
            "r" => "NULL".to_string(),
            _ => "null".to_string(),
        },
        _ => match language {
            "python" => "None".to_string(),
            "ruby" => "nil".to_string(),
            "go" => "nil".to_string(),
            "java" => "null".to_string(),
            "csharp" => "null".to_string(),
            "php" => "null".to_string(),
            "r" => "NULL".to_string(),
            "rust" => "Default::default()".to_string(),
            _ => "null".to_string(),
        },
    }
}

// Helper trait extension for TypeRef to get type name
trait TypeRefExt {
    fn type_name(&self) -> String;
}

impl TypeRefExt for TypeRef {
    fn type_name(&self) -> String {
        match self {
            TypeRef::Named(n) => n.clone(),
            TypeRef::Primitive(p) => format!("{:?}", p),
            TypeRef::String | TypeRef::Char => "String".to_string(),
            TypeRef::Bytes => "Bytes".to_string(),
            TypeRef::Optional(inner) => format!("Option<{}>", inner.type_name()),
            TypeRef::Vec(inner) => format!("Vec<{}>", inner.type_name()),
            TypeRef::Map(k, v) => format!("Map<{}, {}>", k.type_name(), v.type_name()),
            TypeRef::Path => "Path".to_string(),
            TypeRef::Unit => "()".to_string(),
            TypeRef::Json => "Json".to_string(),
            TypeRef::Duration => "Duration".to_string(),
        }
    }
}

/// The maximum arity supported by Magnus `function!` macro.
const MAGNUS_MAX_ARITY: usize = 15;

/// Generate a Magnus (Ruby) kwargs constructor for a type with `has_default`.
///
/// For types with <=15 fields, generates a positional `Option<T>` parameter constructor.
/// For types with >15 fields (exceeding Magnus arity limit), generates a hash-based constructor
/// using `RHash` that extracts fields by name, applying defaults for missing keys.
pub fn gen_magnus_kwargs_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
    // Always use the hash-based constructor so Ruby callers can pass keyword args
    // (`Type.new(field1: ..., field2: ...)`) regardless of field count. Magnus
    // function! macro caps arity at 15, but the hash form uses variadic arity (-1)
    // and works for any number of fields.
    let _ = MAGNUS_MAX_ARITY;
    gen_magnus_hash_constructor(typ, type_mapper)
}

/// Wrap a type string for use as a type-path prefix in Rust.
///
/// Types containing `<` (generics like `Vec<String>`, `Option<T>`) cannot be used as
/// `Vec<String>::try_convert(v)` — that's a parse error. They must use the UFCS form
/// `<Vec<String>>::try_convert(v)` instead. Simple names like `String`, `bool` can use
/// `String::try_convert(v)` directly.
fn as_type_path_prefix(type_str: &str) -> String {
    if type_str.contains('<') {
        format!("<{type_str}>")
    } else {
        type_str.to_string()
    }
}

/// Generate a hash-based Magnus constructor for types with many fields.
/// Accepts `(kwargs: RHash)` and extracts each field by symbol name, applying defaults.
fn gen_magnus_hash_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
    let fields: Vec<_> = constructor_fields(typ)
        .map(|field| {
            let is_optional = field_is_optional_in_rust(field);
            // Use inner type for try_convert, since the hash value is T, not Option<T>.
            // When field.ty is already Optional(T) and field.optional is true, strip one layer so we
            // call <T>::try_convert, not <Option<T>>::try_convert (which would yield Option<Option<T>>).
            let effective_inner_ty = match &field.ty {
                TypeRef::Optional(inner) if is_optional => inner.as_ref(),
                ty => ty,
            };
            let inner_type = type_mapper(effective_inner_ty);
            let type_prefix = as_type_path_prefix(&inner_type);

            let assignment = if is_optional {
                // Field is Option<T>: extract from hash, wrap in Some, default to None
                format!(
                    "kwargs.get(ruby.to_symbol(\"{}\")).and_then(|v| {}::try_convert(v).ok()),",
                    field.name, type_prefix
                )
            } else if use_unwrap_or_default(field) {
                format!(
                    "kwargs.get(ruby.to_symbol(\"{}\")).and_then(|v| {}::try_convert(v).ok()).unwrap_or_default(),",
                    field.name, type_prefix
                )
            } else if matches!(effective_inner_ty, TypeRef::Named(_))
                && !matches!(&field.typed_default, Some(DefaultValue::EnumVariant(_)))
            {
                // Named types in the Magnus binding default to required because
                // Magnus-wrapped structs (`#[magnus::wrap]`) never implement
                // `Default`. The exception is Named *enum* fields whose typed
                // default we resolved to a specific variant (e.g.
                // `output_format: OutputFormat::Plain` from the parent's
                // `impl Default`): those have a concrete fallback expression
                // and fall through to the explicit-default branch below so
                // missing kwargs still construct successfully.
                format!(
                    "kwargs.get(ruby.to_symbol(\"{}\")).and_then(|v| {}::try_convert(v).ok()).ok_or_else(|| magnus::Error::new(unsafe {{ magnus::Ruby::get_unchecked() }}.exception_arg_error(), \"missing required field: {}\"))?,",
                    field.name, type_prefix, field.name
                )
            } else {
                // When the binding maps the field type to String (e.g. an excluded enum), but the
                // original default is an EnumVariant, `default_value_for_field` would emit
                // `TypeName::Variant` which is invalid for a `String` field. Fall back to the
                // string-literal form in that case.
                let default_str = if inner_type == "String" {
                    if let Some(DefaultValue::EnumVariant(variant)) = &field.typed_default {
                        use heck::ToSnakeCase;
                        format!("\"{}\".to_string()", variant.to_snake_case())
                    } else {
                        default_value_for_field(field, "rust")
                    }
                } else {
                    default_value_for_field(field, "rust")
                };
                format!(
                    "kwargs.get(ruby.to_symbol(\"{}\")).and_then(|v| {}::try_convert(v).ok()).unwrap_or({}),",
                    field.name, type_prefix, default_str
                )
            };

            minijinja::context! {
                name => field.name.clone(),
                assignment => assignment,
            }
        })
        .collect();

    crate::codegen::template_env::render(
        "config_gen/magnus_hash_constructor.jinja",
        minijinja::context! {
            fields => fields,
        },
    )
}

/// Returns true if the generated Rust field type is already `Option<T>`.
/// This covers both:
/// - Fields with `optional: true` (the Rust field type becomes `Option<inner_type>`)
/// - Fields whose `TypeRef` is explicitly `Optional(_)` (rare, for nested Option types)
fn field_is_optional_in_rust(field: &FieldDef) -> bool {
    field.optional || matches!(&field.ty, TypeRef::Optional(_))
}

/// Generate a PHP kwargs constructor for a type with `has_default`.
/// All fields become `Option<T>` parameters so PHP users can omit any field.
/// Assignments wrap non-Optional fields in `Some()` and apply defaults.
pub fn gen_php_kwargs_constructor(typ: &TypeDef, type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
    let fields: Vec<_> = constructor_fields(typ)
        .map(|field| {
            let mapped = type_mapper(&field.ty);
            let is_optional_field = field.optional || matches!(&field.ty, TypeRef::Optional(_));

            let assignment = if is_optional_field {
                // Struct field is Option<T>, param is Option<T> — pass through directly
                field.name.clone()
            } else if use_unwrap_or_default(field) {
                // Struct field is T, param is Option<T> — unwrap with type's default
                format!("{}.unwrap_or_default()", field.name)
            } else {
                // Struct field is T, param is Option<T> — unwrap with explicit default
                let default_str = default_value_for_field(field, "rust");
                format!("{}.unwrap_or({})", field.name, default_str)
            };

            minijinja::context! {
                name => field.name.clone(),
                ty => mapped,
                assignment => assignment,
            }
        })
        .collect();

    crate::codegen::template_env::render(
        "config_gen/php_kwargs_constructor.jinja",
        minijinja::context! {
            fields => fields,
        },
    )
}

/// Generate a Rustler (Elixir) kwargs constructor for a type with `has_default`.
/// Accepts keyword list or map, applies defaults for missing fields.
/// Fields in `exclude_fields` are skipped (used for bridge fields that cannot implement Encoder/Decoder).
pub fn gen_rustler_kwargs_constructor_with_exclude(
    typ: &TypeDef,
    _type_mapper: &dyn Fn(&TypeRef) -> String,
    exclude_fields: &std::collections::HashSet<String>,
) -> String {
    // Pre-compute field assignments (same logic as gen_rustler_kwargs_constructor but with exclusion)
    let fields: Vec<_> = constructor_fields(typ)
        .filter(|f| !exclude_fields.contains(&f.name))
        .map(|field| {
            let assignment = if field.optional {
                format!("opts.get(\"{}\").and_then(|t| t.decode().ok()),", field.name)
            } else if use_unwrap_or_default(field) {
                format!(
                    "opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or_default(),",
                    field.name
                )
            } else {
                let default_str = default_value_for_field(field, "rust");
                let is_enum_variant_default = default_str.contains("::") || default_str.starts_with("\"");

                if (is_enum_variant_default && matches!(&field.ty, TypeRef::String | TypeRef::Char))
                    || matches!(&field.ty, TypeRef::Named(_))
                {
                    format!(
                        "opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or_default(),",
                        field.name
                    )
                } else {
                    format!(
                        "opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or({}),",
                        field.name, default_str
                    )
                }
            };

            minijinja::context! {
                name => field.name.clone(),
                assignment => assignment,
            }
        })
        .collect();

    crate::codegen::template_env::render(
        "config_gen/rustler_kwargs_constructor.jinja",
        minijinja::context! {
            fields => fields,
        },
    )
}

/// Generate a Rustler (Elixir) kwargs constructor for a type with `has_default`.
/// Accepts keyword list or map, applies defaults for missing fields.
pub fn gen_rustler_kwargs_constructor(typ: &TypeDef, _type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
    // Pre-compute field assignments
    let fields: Vec<_> = constructor_fields(typ)
        .map(|field| {
            let assignment = if field.optional {
                format!("opts.get(\"{}\").and_then(|t| t.decode().ok()),", field.name)
            } else if use_unwrap_or_default(field) {
                format!(
                    "opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or_default(),",
                    field.name
                )
            } else {
                let default_str = default_value_for_field(field, "rust");
                let is_enum_variant_default = default_str.contains("::") || default_str.starts_with("\"");

                let unwrap_default = (is_enum_variant_default && matches!(&field.ty, TypeRef::String | TypeRef::Char))
                    || matches!(&field.ty, TypeRef::Named(_));
                if unwrap_default {
                    format!(
                        "opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or_default(),",
                        field.name
                    )
                } else {
                    format!(
                        "opts.get(\"{}\").and_then(|t| t.decode().ok()).unwrap_or({}),",
                        field.name, default_str
                    )
                }
            };

            minijinja::context! {
                name => field.name.clone(),
                assignment => assignment,
            }
        })
        .collect();

    crate::codegen::template_env::render(
        "config_gen/rustler_kwargs_constructor.jinja",
        minijinja::context! {
            fields => fields,
        },
    )
}

/// Generate an extendr (R) kwargs constructor for a type with `has_default`.
///
/// Rust does not support function-parameter defaults, and extendr 0.9 only allows
/// defaults via the per-parameter `#[extendr(default = "...")]` attribute (not via
/// `param: T = expr` syntax).  Rather than encode every default in attribute form,
/// we accept each field as `Option<T>` and unwrap it via `T::default()` (or via the
/// type's own `Default::default()` for the whole struct as the base) inside the body.
/// The R-side wrapper generated in `generate_public_api` already supplies named
/// arguments with `NULL` defaults, so callers see ergonomic kwargs at the R level.
///
/// `enum_names` is the set of type names that are enums in this API surface.  For
/// fields whose type resolves to a Named enum, the parameter is widened to
/// `Option<String>` (extendr has no `TryFrom<&Robj>` for binding enums) and the body
/// deserialises the string back to the enum via `serde_json::from_str`.
pub fn gen_extendr_kwargs_constructor(
    typ: &TypeDef,
    type_mapper: &dyn Fn(&TypeRef) -> String,
    enum_names: &ahash::AHashSet<String>,
) -> String {
    // Helper predicates to classify field types
    let is_named_enum = |ty: &TypeRef| -> bool { matches!(ty, TypeRef::Named(n) if enum_names.contains(n.as_str())) };
    let is_named_struct =
        |ty: &TypeRef| -> bool { matches!(ty, TypeRef::Named(n) if !enum_names.contains(n.as_str())) };
    let is_optional_named_struct = |ty: &TypeRef| -> bool {
        if let TypeRef::Optional(inner) = ty {
            is_named_struct(inner)
        } else {
            false
        }
    };
    let ty_is_optional = |ty: &TypeRef| -> bool { matches!(ty, TypeRef::Optional(_)) };

    // Pre-collect emittable fields (skip struct-typed fields that extendr cannot convert)
    let emittable_fields: Vec<_> = typ
        .fields
        .iter()
        .filter(|f| f.cfg.is_none() && !is_named_struct(&f.ty) && !is_optional_named_struct(&f.ty))
        .map(|field| {
            let param_type = if is_named_enum(&field.ty) {
                "Option<String>".to_string()
            } else if ty_is_optional(&field.ty) {
                type_mapper(&field.ty)
            } else {
                format!("Option<{}>", type_mapper(&field.ty))
            };

            minijinja::context! {
                name => field.name.clone(),
                type => param_type,
            }
        })
        .collect();

    // Pre-compute body assignments for all fields
    let body_assignments: Vec<_> = typ
        .fields
        .iter()
        .filter(|f| f.cfg.is_none() && !is_named_struct(&f.ty) && !is_optional_named_struct(&f.ty))
        .map(|field| {
            let code = if is_named_enum(&field.ty) {
                if field.optional {
                    format!(
                        "if let Some(v) = {} {{ __out.{} = serde_json::from_str(&format!(\"\\\"{{v}}\\\"\")).ok(); }}",
                        field.name, field.name
                    )
                } else {
                    format!(
                        "if let Some(v) = {} {{ if let Ok(parsed) = serde_json::from_str(&format!(\"\\\"{{v}}\\\"\")) {{ __out.{} = parsed; }} }}",
                        field.name, field.name
                    )
                }
            } else if ty_is_optional(&field.ty) || field.optional {
                format!(
                    "if let Some(v) = {} {{ __out.{} = Some(v); }}",
                    field.name, field.name
                )
            } else {
                format!(
                    "if let Some(v) = {} {{ __out.{} = v; }}",
                    field.name, field.name
                )
            };

            minijinja::context! {
                code => code,
            }
        })
        .collect();

    crate::codegen::template_env::render(
        "config_gen/extendr_kwargs_constructor.jinja",
        minijinja::context! {
            type_name => typ.name.clone(),
            type_name_lower => typ.name.to_lowercase(),
            params => emittable_fields,
            body_assignments => body_assignments,
        },
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::ir::{CoreWrapper, FieldDef, PrimitiveType, TypeRef};

    fn make_test_type() -> TypeDef {
        TypeDef {
            name: "Config".to_string(),
            rust_path: "my_crate::Config".to_string(),
            original_rust_path: String::new(),
            fields: vec![
                FieldDef {
                    name: "timeout".to_string(),
                    ty: TypeRef::Primitive(PrimitiveType::U64),
                    optional: false,
                    default: Some("30".to_string()),
                    doc: "Timeout in seconds".to_string(),
                    sanitized: false,
                    is_boxed: false,
                    type_rust_path: None,
                    cfg: None,
                    typed_default: Some(DefaultValue::IntLiteral(30)),
                    core_wrapper: CoreWrapper::None,
                    vec_inner_core_wrapper: CoreWrapper::None,
                    newtype_wrapper: None,
                    serde_rename: None,
                    serde_flatten: false,
                    binding_excluded: false,
                    binding_exclusion_reason: None,
                    original_type: None,
                },
                FieldDef {
                    name: "enabled".to_string(),
                    ty: TypeRef::Primitive(PrimitiveType::Bool),
                    optional: false,
                    default: None,
                    doc: "Enable feature".to_string(),
                    sanitized: false,
                    is_boxed: false,
                    type_rust_path: None,
                    cfg: None,
                    typed_default: Some(DefaultValue::BoolLiteral(true)),
                    core_wrapper: CoreWrapper::None,
                    vec_inner_core_wrapper: CoreWrapper::None,
                    newtype_wrapper: None,
                    serde_rename: None,
                    serde_flatten: false,
                    binding_excluded: false,
                    binding_exclusion_reason: None,
                    original_type: None,
                },
                FieldDef {
                    name: "name".to_string(),
                    ty: TypeRef::String,
                    optional: false,
                    default: None,
                    doc: "Config name".to_string(),
                    sanitized: false,
                    is_boxed: false,
                    type_rust_path: None,
                    cfg: None,
                    typed_default: Some(DefaultValue::StringLiteral("default".to_string())),
                    core_wrapper: CoreWrapper::None,
                    vec_inner_core_wrapper: CoreWrapper::None,
                    newtype_wrapper: None,
                    serde_rename: None,
                    serde_flatten: false,
                    binding_excluded: false,
                    binding_exclusion_reason: None,
                    original_type: None,
                },
            ],
            methods: vec![],
            is_opaque: false,
            is_clone: true,
            is_copy: false,
            doc: "Configuration type".to_string(),
            cfg: None,
            is_trait: false,
            has_default: true,
            has_stripped_cfg_fields: false,
            is_return_type: false,
            serde_rename_all: None,
            has_serde: false,
            super_traits: vec![],
            binding_excluded: false,
            binding_exclusion_reason: None,
        }
    }

    #[test]
    fn test_default_value_bool_true_python() {
        let field = FieldDef {
            name: "enabled".to_string(),
            ty: TypeRef::Primitive(PrimitiveType::Bool),
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: Some(DefaultValue::BoolLiteral(true)),
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        };
        assert_eq!(default_value_for_field(&field, "python"), "True");
    }

    #[test]
    fn test_default_value_bool_false_go() {
        let field = FieldDef {
            name: "enabled".to_string(),
            ty: TypeRef::Primitive(PrimitiveType::Bool),
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: Some(DefaultValue::BoolLiteral(false)),
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        };
        assert_eq!(default_value_for_field(&field, "go"), "false");
    }

    #[test]
    fn test_default_value_string_literal() {
        let field = FieldDef {
            name: "name".to_string(),
            ty: TypeRef::String,
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: Some(DefaultValue::StringLiteral("hello".to_string())),
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        };
        assert_eq!(default_value_for_field(&field, "python"), "\"hello\"");
        assert_eq!(default_value_for_field(&field, "java"), "\"hello\"");
    }

    #[test]
    fn test_default_value_int_literal() {
        let field = FieldDef {
            name: "timeout".to_string(),
            ty: TypeRef::Primitive(PrimitiveType::U64),
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: Some(DefaultValue::IntLiteral(42)),
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        };
        let result = default_value_for_field(&field, "python");
        assert_eq!(result, "42");
    }

    #[test]
    fn test_default_value_none() {
        let field = FieldDef {
            name: "maybe".to_string(),
            ty: TypeRef::Optional(Box::new(TypeRef::String)),
            optional: true,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: Some(DefaultValue::None),
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        };
        assert_eq!(default_value_for_field(&field, "python"), "None");
        assert_eq!(default_value_for_field(&field, "go"), "nil");
        assert_eq!(default_value_for_field(&field, "java"), "null");
        assert_eq!(default_value_for_field(&field, "csharp"), "null");
    }

    #[test]
    fn test_default_value_fallback_string() {
        let field = FieldDef {
            name: "name".to_string(),
            ty: TypeRef::String,
            optional: false,
            default: Some("\"custom\"".to_string()),
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: None,
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        };
        assert_eq!(default_value_for_field(&field, "python"), "\"custom\"");
    }

    #[test]
    fn test_gen_pyo3_kwargs_constructor() {
        let typ = make_test_type();
        let output = gen_pyo3_kwargs_constructor(&typ, &|tr: &TypeRef| match tr {
            TypeRef::Primitive(p) => format!("{:?}", p),
            TypeRef::String | TypeRef::Char => "str".to_string(),
            _ => "Any".to_string(),
        });

        assert!(output.contains("#[new]"));
        assert!(output.contains("#[pyo3(signature = ("));
        assert!(output.contains("timeout=30"));
        assert!(output.contains("enabled=True"));
        assert!(output.contains("name=\"default\""));
        assert!(output.contains("fn new("));
    }

    #[test]
    fn test_gen_napi_defaults_constructor() {
        let typ = make_test_type();
        let output = gen_napi_defaults_constructor(&typ, &|tr: &TypeRef| match tr {
            TypeRef::Primitive(p) => format!("{:?}", p),
            TypeRef::String | TypeRef::Char => "String".to_string(),
            _ => "Value".to_string(),
        });

        assert!(output.contains("pub fn new(mut env: napi::Env, obj: napi::Object)"));
        assert!(output.contains("timeout"));
        assert!(output.contains("enabled"));
        assert!(output.contains("name"));
    }

    #[test]
    fn test_gen_go_functional_options() {
        let typ = make_test_type();
        let output = gen_go_functional_options(&typ, &|tr: &TypeRef| match tr {
            TypeRef::Primitive(p) => match p {
                PrimitiveType::U64 => "uint64".to_string(),
                PrimitiveType::Bool => "bool".to_string(),
                _ => "interface{}".to_string(),
            },
            TypeRef::String | TypeRef::Char => "string".to_string(),
            _ => "interface{}".to_string(),
        });

        assert!(output.contains("type Config struct {"));
        assert!(output.contains("type ConfigOption func(*Config)"));
        assert!(output.contains("func WithConfigTimeout(val uint64) ConfigOption"));
        assert!(output.contains("func WithConfigEnabled(val bool) ConfigOption"));
        assert!(output.contains("func WithConfigName(val string) ConfigOption"));
        assert!(output.contains("func NewConfig(opts ...ConfigOption) *Config"));
    }

    #[test]
    fn test_gen_java_builder() {
        let typ = make_test_type();
        let output = gen_java_builder(&typ, "dev.test", &|tr: &TypeRef| match tr {
            TypeRef::Primitive(p) => match p {
                PrimitiveType::U64 => "long".to_string(),
                PrimitiveType::Bool => "boolean".to_string(),
                _ => "int".to_string(),
            },
            TypeRef::String | TypeRef::Char => "String".to_string(),
            _ => "Object".to_string(),
        });

        assert!(output.contains("package dev.test;"));
        assert!(output.contains("public class ConfigBuilder"));
        assert!(output.contains("withTimeout"));
        assert!(output.contains("withEnabled"));
        assert!(output.contains("withName"));
        assert!(output.contains("public Config build()"));
    }

    #[test]
    fn test_gen_csharp_record() {
        let typ = make_test_type();
        let output = gen_csharp_record(&typ, "MyNamespace", &|tr: &TypeRef| match tr {
            TypeRef::Primitive(p) => match p {
                PrimitiveType::U64 => "ulong".to_string(),
                PrimitiveType::Bool => "bool".to_string(),
                _ => "int".to_string(),
            },
            TypeRef::String | TypeRef::Char => "string".to_string(),
            _ => "object".to_string(),
        });

        assert!(output.contains("namespace MyNamespace;"));
        assert!(output.contains("public record Config"));
        assert!(output.contains("public ulong Timeout"));
        assert!(output.contains("public bool Enabled"));
        assert!(output.contains("public string Name"));
        assert!(output.contains("init;"));
    }

    #[test]
    fn test_default_value_float_literal() {
        let field = FieldDef {
            name: "ratio".to_string(),
            ty: TypeRef::Primitive(PrimitiveType::F64),
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: Some(DefaultValue::FloatLiteral(1.5)),
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        };
        let result = default_value_for_field(&field, "python");
        assert!(result.contains("1.5"));
    }

    #[test]
    fn test_default_value_no_typed_no_default() {
        let field = FieldDef {
            name: "count".to_string(),
            ty: TypeRef::Primitive(PrimitiveType::U32),
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: None,
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        };
        // Should fall back to type-based zero value
        assert_eq!(default_value_for_field(&field, "python"), "0");
        assert_eq!(default_value_for_field(&field, "go"), "0");
    }

    fn make_field(name: &str, ty: TypeRef) -> FieldDef {
        FieldDef {
            name: name.to_string(),
            ty,
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: None,
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        }
    }

    fn simple_type_mapper(tr: &TypeRef) -> String {
        match tr {
            TypeRef::Primitive(p) => match p {
                PrimitiveType::U64 => "u64".to_string(),
                PrimitiveType::Bool => "bool".to_string(),
                PrimitiveType::U32 => "u32".to_string(),
                _ => "i64".to_string(),
            },
            TypeRef::String | TypeRef::Char => "String".to_string(),
            TypeRef::Optional(inner) => format!("Option<{}>", simple_type_mapper(inner)),
            TypeRef::Vec(inner) => format!("Vec<{}>", simple_type_mapper(inner)),
            TypeRef::Named(n) => n.clone(),
            _ => "Value".to_string(),
        }
    }

    // -------------------------------------------------------------------------
    // default_value_for_field — untested branches
    // -------------------------------------------------------------------------

    #[test]
    fn test_default_value_bool_literal_ruby() {
        let field = FieldDef {
            name: "flag".to_string(),
            ty: TypeRef::Primitive(PrimitiveType::Bool),
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: Some(DefaultValue::BoolLiteral(true)),
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        };
        assert_eq!(default_value_for_field(&field, "ruby"), "true");
        assert_eq!(default_value_for_field(&field, "php"), "true");
        assert_eq!(default_value_for_field(&field, "csharp"), "true");
        assert_eq!(default_value_for_field(&field, "java"), "true");
        assert_eq!(default_value_for_field(&field, "rust"), "true");
    }

    #[test]
    fn test_default_value_bool_literal_r() {
        let field = FieldDef {
            name: "flag".to_string(),
            ty: TypeRef::Primitive(PrimitiveType::Bool),
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: Some(DefaultValue::BoolLiteral(false)),
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        };
        assert_eq!(default_value_for_field(&field, "r"), "FALSE");
    }

    #[test]
    fn test_default_value_string_literal_rust() {
        let field = FieldDef {
            name: "label".to_string(),
            ty: TypeRef::String,
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: Some(DefaultValue::StringLiteral("hello".to_string())),
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        };
        assert_eq!(default_value_for_field(&field, "rust"), "\"hello\".to_string()");
    }

    #[test]
    fn test_default_value_string_literal_escapes_quotes() {
        let field = FieldDef {
            name: "label".to_string(),
            ty: TypeRef::String,
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: Some(DefaultValue::StringLiteral("say \"hi\"".to_string())),
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        };
        assert_eq!(default_value_for_field(&field, "python"), "\"say \\\"hi\\\"\"");
    }

    #[test]
    fn test_default_value_float_literal_whole_number() {
        // A whole-number float should be rendered with ".0" suffix.
        let field = FieldDef {
            name: "scale".to_string(),
            ty: TypeRef::Primitive(PrimitiveType::F32),
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: Some(DefaultValue::FloatLiteral(2.0)),
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        };
        let result = default_value_for_field(&field, "python");
        assert!(result.contains('.'), "whole-number float should contain '.': {result}");
    }

    #[test]
    fn test_default_value_enum_variant_per_language() {
        let field = FieldDef {
            name: "format".to_string(),
            ty: TypeRef::Named("OutputFormat".to_string()),
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: Some(DefaultValue::EnumVariant("JsonOutput".to_string())),
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        };
        assert_eq!(default_value_for_field(&field, "python"), "OutputFormat.JSON_OUTPUT");
        assert_eq!(default_value_for_field(&field, "ruby"), "OutputFormat::JsonOutput");
        assert_eq!(default_value_for_field(&field, "go"), "OutputFormatJsonOutput");
        assert_eq!(default_value_for_field(&field, "java"), "OutputFormat.JSON_OUTPUT");
        assert_eq!(default_value_for_field(&field, "csharp"), "OutputFormat.JsonOutput");
        assert_eq!(default_value_for_field(&field, "php"), "OutputFormat::JsonOutput");
        assert_eq!(default_value_for_field(&field, "r"), "OutputFormat$JsonOutput");
        assert_eq!(default_value_for_field(&field, "rust"), "OutputFormat::JsonOutput");
    }

    #[test]
    fn test_default_value_empty_vec_per_language() {
        let field = FieldDef {
            name: "items".to_string(),
            ty: TypeRef::Vec(Box::new(TypeRef::String)),
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: Some(DefaultValue::Empty),
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        };
        assert_eq!(default_value_for_field(&field, "python"), "[]");
        assert_eq!(default_value_for_field(&field, "ruby"), "[]");
        assert_eq!(default_value_for_field(&field, "csharp"), "[]");
        assert_eq!(default_value_for_field(&field, "go"), "nil");
        assert_eq!(default_value_for_field(&field, "java"), "List.of()");
        assert_eq!(default_value_for_field(&field, "php"), "[]");
        assert_eq!(default_value_for_field(&field, "r"), "c()");
        assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
    }

    #[test]
    fn test_default_value_empty_map_per_language() {
        let field = FieldDef {
            name: "meta".to_string(),
            ty: TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: Some(DefaultValue::Empty),
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        };
        assert_eq!(default_value_for_field(&field, "python"), "{}");
        assert_eq!(default_value_for_field(&field, "go"), "nil");
        assert_eq!(default_value_for_field(&field, "java"), "Map.of()");
        assert_eq!(default_value_for_field(&field, "rust"), "Default::default()");
    }

    #[test]
    fn test_default_value_empty_bool_primitive() {
        let field = FieldDef {
            name: "flag".to_string(),
            ty: TypeRef::Primitive(PrimitiveType::Bool),
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: Some(DefaultValue::Empty),
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        };
        assert_eq!(default_value_for_field(&field, "python"), "False");
        assert_eq!(default_value_for_field(&field, "ruby"), "false");
        assert_eq!(default_value_for_field(&field, "go"), "false");
    }

    #[test]
    fn test_default_value_empty_float_primitive() {
        let field = FieldDef {
            name: "ratio".to_string(),
            ty: TypeRef::Primitive(PrimitiveType::F64),
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: Some(DefaultValue::Empty),
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        };
        assert_eq!(default_value_for_field(&field, "python"), "0.0");
    }

    #[test]
    fn test_default_value_empty_string_type() {
        let field = FieldDef {
            name: "label".to_string(),
            ty: TypeRef::String,
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: Some(DefaultValue::Empty),
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        };
        assert_eq!(default_value_for_field(&field, "rust"), "String::new()");
        assert_eq!(default_value_for_field(&field, "python"), "\"\"");
    }

    #[test]
    fn test_default_value_empty_bytes_type() {
        let field = FieldDef {
            name: "data".to_string(),
            ty: TypeRef::Bytes,
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: Some(DefaultValue::Empty),
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        };
        assert_eq!(default_value_for_field(&field, "python"), "b\"\"");
        assert_eq!(default_value_for_field(&field, "go"), "[]byte{}");
        assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
    }

    #[test]
    fn test_default_value_empty_json_type() {
        let field = FieldDef {
            name: "payload".to_string(),
            ty: TypeRef::Json,
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: Some(DefaultValue::Empty),
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        };
        assert_eq!(default_value_for_field(&field, "python"), "{}");
        assert_eq!(default_value_for_field(&field, "ruby"), "{}");
        assert_eq!(default_value_for_field(&field, "go"), "json.RawMessage(nil)");
        assert_eq!(default_value_for_field(&field, "r"), "list()");
        assert_eq!(default_value_for_field(&field, "rust"), "serde_json::json!({})");
    }

    #[test]
    fn test_default_value_none_ruby_php_r() {
        let field = FieldDef {
            name: "maybe".to_string(),
            ty: TypeRef::Optional(Box::new(TypeRef::String)),
            optional: true,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: Some(DefaultValue::None),
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        };
        assert_eq!(default_value_for_field(&field, "ruby"), "nil");
        assert_eq!(default_value_for_field(&field, "php"), "null");
        assert_eq!(default_value_for_field(&field, "r"), "NULL");
        assert_eq!(default_value_for_field(&field, "rust"), "None");
    }

    // -------------------------------------------------------------------------
    // Fallback (no typed_default, no default) — type-based zero values
    // -------------------------------------------------------------------------

    #[test]
    fn test_default_value_fallback_bool_all_languages() {
        let field = make_field("flag", TypeRef::Primitive(PrimitiveType::Bool));
        assert_eq!(default_value_for_field(&field, "python"), "False");
        assert_eq!(default_value_for_field(&field, "ruby"), "false");
        assert_eq!(default_value_for_field(&field, "csharp"), "false");
        assert_eq!(default_value_for_field(&field, "java"), "false");
        assert_eq!(default_value_for_field(&field, "php"), "false");
        assert_eq!(default_value_for_field(&field, "r"), "FALSE");
        assert_eq!(default_value_for_field(&field, "rust"), "false");
    }

    #[test]
    fn test_default_value_fallback_float() {
        let field = make_field("ratio", TypeRef::Primitive(PrimitiveType::F64));
        assert_eq!(default_value_for_field(&field, "python"), "0.0");
        assert_eq!(default_value_for_field(&field, "rust"), "0.0");
    }

    #[test]
    fn test_default_value_fallback_string_all_languages() {
        let field = make_field("name", TypeRef::String);
        assert_eq!(default_value_for_field(&field, "python"), "\"\"");
        assert_eq!(default_value_for_field(&field, "ruby"), "\"\"");
        assert_eq!(default_value_for_field(&field, "go"), "\"\"");
        assert_eq!(default_value_for_field(&field, "java"), "\"\"");
        assert_eq!(default_value_for_field(&field, "csharp"), "\"\"");
        assert_eq!(default_value_for_field(&field, "php"), "\"\"");
        assert_eq!(default_value_for_field(&field, "r"), "\"\"");
        assert_eq!(default_value_for_field(&field, "rust"), "String::new()");
    }

    #[test]
    fn test_default_value_fallback_bytes_all_languages() {
        let field = make_field("data", TypeRef::Bytes);
        assert_eq!(default_value_for_field(&field, "python"), "b\"\"");
        assert_eq!(default_value_for_field(&field, "ruby"), "\"\"");
        assert_eq!(default_value_for_field(&field, "go"), "[]byte{}");
        assert_eq!(default_value_for_field(&field, "java"), "new byte[]{}");
        assert_eq!(default_value_for_field(&field, "csharp"), "new byte[]{}");
        assert_eq!(default_value_for_field(&field, "php"), "\"\"");
        assert_eq!(default_value_for_field(&field, "r"), "raw()");
        assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
    }

    #[test]
    fn test_default_value_fallback_optional() {
        let field = make_field("maybe", TypeRef::Optional(Box::new(TypeRef::String)));
        assert_eq!(default_value_for_field(&field, "python"), "None");
        assert_eq!(default_value_for_field(&field, "ruby"), "nil");
        assert_eq!(default_value_for_field(&field, "go"), "nil");
        assert_eq!(default_value_for_field(&field, "java"), "null");
        assert_eq!(default_value_for_field(&field, "csharp"), "null");
        assert_eq!(default_value_for_field(&field, "php"), "null");
        assert_eq!(default_value_for_field(&field, "r"), "NULL");
        assert_eq!(default_value_for_field(&field, "rust"), "None");
    }

    #[test]
    fn test_default_value_fallback_vec_all_languages() {
        let field = make_field("items", TypeRef::Vec(Box::new(TypeRef::String)));
        assert_eq!(default_value_for_field(&field, "python"), "[]");
        assert_eq!(default_value_for_field(&field, "ruby"), "[]");
        assert_eq!(default_value_for_field(&field, "go"), "[]interface{}{}");
        assert_eq!(default_value_for_field(&field, "java"), "new java.util.ArrayList<>()");
        assert_eq!(default_value_for_field(&field, "csharp"), "[]");
        assert_eq!(default_value_for_field(&field, "php"), "[]");
        assert_eq!(default_value_for_field(&field, "r"), "c()");
        assert_eq!(default_value_for_field(&field, "rust"), "vec![]");
    }

    #[test]
    fn test_default_value_fallback_map_all_languages() {
        let field = make_field(
            "meta",
            TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
        );
        assert_eq!(default_value_for_field(&field, "python"), "{}");
        assert_eq!(default_value_for_field(&field, "ruby"), "{}");
        assert_eq!(default_value_for_field(&field, "go"), "make(map[string]interface{})");
        assert_eq!(default_value_for_field(&field, "java"), "new java.util.HashMap<>()");
        assert_eq!(
            default_value_for_field(&field, "csharp"),
            "new Dictionary<string, object>()"
        );
        assert_eq!(default_value_for_field(&field, "php"), "[]");
        assert_eq!(default_value_for_field(&field, "r"), "list()");
        assert_eq!(
            default_value_for_field(&field, "rust"),
            "std::collections::HashMap::new()"
        );
    }

    #[test]
    fn test_default_value_fallback_json_all_languages() {
        let field = make_field("payload", TypeRef::Json);
        assert_eq!(default_value_for_field(&field, "python"), "{}");
        assert_eq!(default_value_for_field(&field, "ruby"), "{}");
        assert_eq!(default_value_for_field(&field, "go"), "json.RawMessage(nil)");
        assert_eq!(default_value_for_field(&field, "r"), "list()");
        assert_eq!(default_value_for_field(&field, "rust"), "serde_json::json!({})");
    }

    #[test]
    fn test_default_value_fallback_named_type() {
        let field = make_field("config", TypeRef::Named("MyConfig".to_string()));
        assert_eq!(default_value_for_field(&field, "rust"), "MyConfig::default()");
        assert_eq!(default_value_for_field(&field, "python"), "None");
        assert_eq!(default_value_for_field(&field, "ruby"), "nil");
        assert_eq!(default_value_for_field(&field, "go"), "nil");
        assert_eq!(default_value_for_field(&field, "java"), "null");
        assert_eq!(default_value_for_field(&field, "csharp"), "null");
        assert_eq!(default_value_for_field(&field, "php"), "null");
        assert_eq!(default_value_for_field(&field, "r"), "NULL");
    }

    #[test]
    fn test_default_value_fallback_duration() {
        // Duration falls through to the wildcard arm
        let field = make_field("timeout", TypeRef::Duration);
        assert_eq!(default_value_for_field(&field, "python"), "None");
        assert_eq!(default_value_for_field(&field, "rust"), "Default::default()");
    }

    #[test]
    fn test_gen_magnus_kwargs_constructor_hash_path_for_many_fields() {
        // Build a type with 16 fields (> MAGNUS_MAX_ARITY = 15) to force hash path
        let mut fields: Vec<FieldDef> = (0..16)
            .map(|i| FieldDef {
                name: format!("field_{i}"),
                ty: TypeRef::Primitive(PrimitiveType::U32),
                optional: false,
                default: None,
                doc: String::new(),
                sanitized: false,
                is_boxed: false,
                type_rust_path: None,
                cfg: None,
                typed_default: None,
                core_wrapper: CoreWrapper::None,
                vec_inner_core_wrapper: CoreWrapper::None,
                newtype_wrapper: None,
                serde_rename: None,
                serde_flatten: false,
                binding_excluded: false,
                binding_exclusion_reason: None,
                original_type: None,
            })
            .collect();
        // Make one field optional to exercise that branch in the hash constructor
        fields[0].optional = true;

        let typ = TypeDef {
            name: "BigConfig".to_string(),
            rust_path: "crate::BigConfig".to_string(),
            original_rust_path: String::new(),
            fields,
            methods: vec![],
            is_opaque: false,
            is_clone: true,
            is_copy: false,
            doc: String::new(),
            cfg: None,
            is_trait: false,
            has_default: true,
            has_stripped_cfg_fields: false,
            is_return_type: false,
            serde_rename_all: None,
            has_serde: false,
            super_traits: vec![],
            binding_excluded: false,
            binding_exclusion_reason: None,
        };
        let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);

        assert!(
            output.contains("Option<magnus::RHash>"),
            "should accept RHash via scan_args"
        );
        assert!(output.contains("ruby.to_symbol("), "should use symbol lookup");
        // Optional field uses and_then without unwrap_or
        assert!(
            output.contains("field_0: kwargs.get(ruby.to_symbol(\"field_0\")).and_then(|v|"),
            "optional field should use and_then"
        );
        assert!(
            output.contains("field_0:").then_some(()).is_some(),
            "field_0 should appear in output"
        );
    }

    // -------------------------------------------------------------------------
    // gen_php_kwargs_constructor
    // -------------------------------------------------------------------------

    #[test]
    fn test_gen_php_kwargs_constructor_basic() {
        let typ = make_test_type();
        let output = gen_php_kwargs_constructor(&typ, &simple_type_mapper);

        assert!(
            output.contains("pub fn __construct("),
            "should use PHP constructor name"
        );
        // All params are Option<T>
        assert!(
            output.contains("timeout: Option<u64>"),
            "timeout param should be Option<u64>"
        );
        assert!(
            output.contains("enabled: Option<bool>"),
            "enabled param should be Option<bool>"
        );
        assert!(
            output.contains("name: Option<String>"),
            "name param should be Option<String>"
        );
        assert!(output.contains("-> Self {"), "should return Self");
        assert!(
            output.contains("timeout: timeout.unwrap_or(30),"),
            "should apply int default for timeout"
        );
        assert!(
            output.contains("enabled: enabled.unwrap_or(true),"),
            "should apply bool default for enabled"
        );
        assert!(
            output.contains("name: name.unwrap_or(\"default\".to_string()),"),
            "should apply string default for name"
        );
    }

    #[test]
    fn test_gen_php_kwargs_constructor_optional_field_passthrough() {
        let mut typ = make_test_type();
        typ.fields.push(FieldDef {
            name: "tag".to_string(),
            ty: TypeRef::String,
            optional: true,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: None,
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        });
        let output = gen_php_kwargs_constructor(&typ, &simple_type_mapper);
        assert!(
            output.contains("tag,"),
            "optional field should be passed through directly"
        );
        assert!(!output.contains("tag.unwrap"), "optional field should not call unwrap");
    }

    #[test]
    fn test_gen_php_kwargs_constructor_unwrap_or_default_for_primitive() {
        let mut typ = make_test_type();
        typ.fields.push(FieldDef {
            name: "retries".to_string(),
            ty: TypeRef::Primitive(PrimitiveType::U32),
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: None,
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        });
        let output = gen_php_kwargs_constructor(&typ, &simple_type_mapper);
        assert!(
            output.contains("retries: retries.unwrap_or_default(),"),
            "primitive with no default should use unwrap_or_default"
        );
    }

    // -------------------------------------------------------------------------
    // gen_rustler_kwargs_constructor
    // -------------------------------------------------------------------------

    #[test]
    fn test_gen_rustler_kwargs_constructor_basic() {
        let typ = make_test_type();
        let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);

        assert!(
            output.contains("pub fn new(opts: std::collections::HashMap<String, rustler::Term>)"),
            "should accept HashMap of Terms"
        );
        assert!(output.contains("Self {"), "should construct Self");
        // timeout has IntLiteral(30) — explicit unwrap_or
        assert!(
            output.contains("timeout: opts.get(\"timeout\").and_then(|t| t.decode().ok()).unwrap_or(30),"),
            "should apply int default for timeout"
        );
        // enabled has BoolLiteral(true) — explicit unwrap_or
        assert!(
            output.contains("enabled: opts.get(\"enabled\").and_then(|t| t.decode().ok()).unwrap_or(true),"),
            "should apply bool default for enabled"
        );
    }

    #[test]
    fn test_gen_rustler_kwargs_constructor_optional_field() {
        let mut typ = make_test_type();
        typ.fields.push(FieldDef {
            name: "extra".to_string(),
            ty: TypeRef::String,
            optional: true,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: None,
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        });
        let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
        assert!(
            output.contains("extra: opts.get(\"extra\").and_then(|t| t.decode().ok()),"),
            "optional field should decode without unwrap"
        );
    }

    #[test]
    fn test_gen_rustler_kwargs_constructor_skips_binding_excluded_fields() {
        let mut typ = make_test_type();
        typ.fields.push(FieldDef {
            name: "internal_cache".to_string(),
            ty: TypeRef::String,
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: None,
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: true,
            binding_exclusion_reason: Some("internal implementation detail".to_string()),
            original_type: None,
        });

        let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);

        assert!(
            !output.contains("internal_cache"),
            "binding-excluded fields must not be exposed in Rustler constructors; got:\n{output}"
        );
    }

    #[test]
    fn test_gen_rustler_kwargs_constructor_named_type_uses_unwrap_or_default() {
        let mut typ = make_test_type();
        typ.fields.push(FieldDef {
            name: "inner".to_string(),
            ty: TypeRef::Named("InnerConfig".to_string()),
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: None,
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        });
        let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
        assert!(
            output.contains("inner: opts.get(\"inner\").and_then(|t| t.decode().ok()).unwrap_or_default(),"),
            "Named type with no default should use unwrap_or_default"
        );
    }

    #[test]
    fn test_gen_rustler_kwargs_constructor_string_field_uses_unwrap_or_default() {
        // A String field with a StringLiteral default contains "::", triggering the
        // is_enum_variant_default check — should fall back to unwrap_or_default().
        let mut typ = make_test_type();
        // 'name' field in make_test_type() has StringLiteral("default") — verify it
        let output = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
        assert!(
            output.contains("name: opts.get(\"name\").and_then(|t| t.decode().ok()).unwrap_or_default(),"),
            "String field with quoted default should use unwrap_or_default"
        );
        // Also verify a plain string field (no default) also falls through to unwrap_or_default
        typ.fields.push(FieldDef {
            name: "label".to_string(),
            ty: TypeRef::String,
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: None,
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        });
        let output2 = gen_rustler_kwargs_constructor(&typ, &simple_type_mapper);
        assert!(
            output2.contains("label: opts.get(\"label\").and_then(|t| t.decode().ok()).unwrap_or_default(),"),
            "String field with no default should use unwrap_or_default"
        );
    }

    // -------------------------------------------------------------------------
    // gen_extendr_kwargs_constructor
    // -------------------------------------------------------------------------

    #[test]
    fn test_gen_extendr_kwargs_constructor_basic() {
        let typ = make_test_type();
        let empty_enums = ahash::AHashSet::new();
        let output = gen_extendr_kwargs_constructor(&typ, &simple_type_mapper, &empty_enums);

        assert!(output.contains("#[extendr]"), "should have extendr attribute");
        assert!(
            output.contains("pub fn new_config("),
            "function name should be lowercase type name"
        );
        // Fields appear as Option<T> parameters — Rust does not support param defaults.
        assert!(
            output.contains("timeout: Option<u64>"),
            "should accept timeout as Option<u64>: {output}"
        );
        assert!(
            output.contains("enabled: Option<bool>"),
            "should accept enabled as Option<bool>: {output}"
        );
        assert!(
            output.contains("name: Option<String>"),
            "should accept name as Option<String>: {output}"
        );
        assert!(output.contains("-> Config {"), "should return Config");
        assert!(
            output.contains("let mut __out = <Config>::default();"),
            "should base on Default impl: {output}"
        );
        assert!(
            output.contains("if let Some(v) = timeout { __out.timeout = v; }"),
            "should overlay caller-provided timeout"
        );
        assert!(
            output.contains("if let Some(v) = enabled { __out.enabled = v; }"),
            "should overlay caller-provided enabled"
        );
        assert!(
            output.contains("if let Some(v) = name { __out.name = v; }"),
            "should overlay caller-provided name"
        );
    }

    #[test]
    fn test_gen_extendr_kwargs_constructor_uses_option_for_all_fields() {
        // Rust function-parameter defaults (`x: T = expr`) are a syntax error and
        // extendr 0.9 only supports defaults via the `#[extendr(default = "...")]`
        // attribute.  Verify that no field is emitted with a Rust-syntax default.
        let typ = make_test_type();
        let empty_enums = ahash::AHashSet::new();
        let output = gen_extendr_kwargs_constructor(&typ, &simple_type_mapper, &empty_enums);
        assert!(
            !output.contains("= TRUE") && !output.contains("= FALSE") && !output.contains("= \"default\""),
            "constructor must not use Rust-syntax param defaults: {output}"
        );
    }

    // -------------------------------------------------------------------------
    // gen_go_functional_options — tuple-field filtering
    // -------------------------------------------------------------------------

    #[test]
    fn test_gen_go_functional_options_skips_tuple_fields() {
        let mut typ = make_test_type();
        typ.fields.push(FieldDef {
            name: "_0".to_string(),
            ty: TypeRef::Primitive(PrimitiveType::U32),
            optional: false,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: None,
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        });
        let output = gen_go_functional_options(&typ, &simple_type_mapper);
        assert!(
            !output.contains("_0"),
            "tuple field _0 should be filtered out from Go output"
        );
    }

    // -------------------------------------------------------------------------
    // as_type_path_prefix — tested indirectly through hash constructor
    // -------------------------------------------------------------------------

    #[test]
    fn test_gen_magnus_hash_constructor_generic_type_prefix() {
        // A field with a Vec type should use <Vec<...>>::try_convert UFCS form
        let fields: Vec<FieldDef> = (0..16)
            .map(|i| FieldDef {
                name: format!("field_{i}"),
                ty: if i == 0 {
                    TypeRef::Vec(Box::new(TypeRef::String))
                } else {
                    TypeRef::Primitive(PrimitiveType::U32)
                },
                optional: false,
                default: None,
                doc: String::new(),
                sanitized: false,
                is_boxed: false,
                type_rust_path: None,
                cfg: None,
                typed_default: None,
                core_wrapper: CoreWrapper::None,
                vec_inner_core_wrapper: CoreWrapper::None,
                newtype_wrapper: None,
                serde_rename: None,
                serde_flatten: false,
                binding_excluded: false,
                binding_exclusion_reason: None,
                original_type: None,
            })
            .collect();
        let typ = TypeDef {
            name: "WideConfig".to_string(),
            rust_path: "crate::WideConfig".to_string(),
            original_rust_path: String::new(),
            fields,
            methods: vec![],
            is_opaque: false,
            is_clone: true,
            is_copy: false,
            doc: String::new(),
            cfg: None,
            is_trait: false,
            has_default: true,
            has_stripped_cfg_fields: false,
            is_return_type: false,
            serde_rename_all: None,
            has_serde: false,
            super_traits: vec![],
            binding_excluded: false,
            binding_exclusion_reason: None,
        };
        let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
        // Vec<String> is a generic type; must use <Vec<String>>::try_convert
        assert!(
            output.contains("<Vec<String>>::try_convert"),
            "generic types should use UFCS angle-bracket prefix: {output}"
        );
    }

    // -------------------------------------------------------------------------
    // Bug B regression: Option<Option<T>> must not appear when field.optional==true
    // and field.ty==Optional(T). This happens for "Update" structs where the core
    // field is Option<Option<T>> — the binding flattens to Option<T>.
    // -------------------------------------------------------------------------

    #[test]
    fn test_magnus_hash_constructor_no_double_option_when_ty_is_optional() {
        // field with optional=true AND ty=Optional(Usize) — represents a core Option<Option<usize>>
        // that should flatten to Option<usize> in the binding constructor.
        // simple_type_mapper maps Usize → "i64" (catch-all primitive arm).
        let field = FieldDef {
            name: "max_depth".to_string(),
            ty: TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::Usize))),
            optional: true,
            default: None,
            doc: String::new(),
            sanitized: false,
            is_boxed: false,
            type_rust_path: None,
            cfg: None,
            typed_default: None,
            core_wrapper: CoreWrapper::None,
            vec_inner_core_wrapper: CoreWrapper::None,
            newtype_wrapper: None,
            serde_rename: None,
            serde_flatten: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
            original_type: None,
        };
        // Build a large type (>15 fields) so the hash constructor is used
        let mut fields: Vec<FieldDef> = (0..15)
            .map(|i| FieldDef {
                name: format!("field_{i}"),
                ty: TypeRef::Primitive(PrimitiveType::U32),
                optional: false,
                default: None,
                doc: String::new(),
                sanitized: false,
                is_boxed: false,
                type_rust_path: None,
                cfg: None,
                typed_default: None,
                core_wrapper: CoreWrapper::None,
                vec_inner_core_wrapper: CoreWrapper::None,
                newtype_wrapper: None,
                serde_rename: None,
                serde_flatten: false,
                binding_excluded: false,
                binding_exclusion_reason: None,
                original_type: None,
            })
            .collect();
        fields.push(field);
        let typ = TypeDef {
            name: "UpdateConfig".to_string(),
            rust_path: "crate::UpdateConfig".to_string(),
            original_rust_path: String::new(),
            fields,
            methods: vec![],
            is_opaque: false,
            is_clone: true,
            is_copy: false,
            doc: String::new(),
            cfg: None,
            is_trait: false,
            has_default: true,
            has_stripped_cfg_fields: false,
            is_return_type: false,
            serde_rename_all: None,
            has_serde: false,
            super_traits: vec![],
            binding_excluded: false,
            binding_exclusion_reason: None,
        };
        let output = gen_magnus_kwargs_constructor(&typ, &simple_type_mapper);
        // The try_convert call must be for the inner type (i64, as mapped by simple_type_mapper),
        // not Option<i64> (which would yield Option<Option<i64>>).
        assert!(
            !output.contains("Option<Option<"),
            "hash constructor must not emit double Option: {output}"
        );
        assert!(
            output.contains("i64::try_convert"),
            "hash constructor should call inner-type::try_convert, not Option<T>::try_convert: {output}"
        );
    }
}
