use crate::backends::go::type_map::go_type;
use crate::core::config::TraitBridgeConfig;
use crate::core::hash::{self, CommentStyle};
use crate::core::ir::{ApiSurface, MethodDef, TypeDef, TypeRef};
use heck::ToPascalCase;
use std::collections::HashSet;

/// Recursively substitute `TypeRef::Named(n)` references where `n` is explicitly excluded
/// from the binding's public surface (collected from `ApiSurface::excluded_type_paths` and
/// any `binding_excluded` types) with `TypeRef::Json`. This lets trait-bridge interface
/// signatures and trampolines fall back to `json.RawMessage`, since the named Go type was
/// never emitted into `binding.go` and would otherwise produce `undefined: <Name>` build
/// errors.
fn substitute_excluded_types(ty: &TypeRef, excluded: &HashSet<&str>) -> TypeRef {
    match ty {
        TypeRef::Named(name) if excluded.contains(name.as_str()) => TypeRef::Json,
        TypeRef::Optional(inner) => TypeRef::Optional(Box::new(substitute_excluded_types(inner, excluded))),
        TypeRef::Vec(inner) => TypeRef::Vec(Box::new(substitute_excluded_types(inner, excluded))),
        TypeRef::Map(k, v) => TypeRef::Map(
            Box::new(substitute_excluded_types(k, excluded)),
            Box::new(substitute_excluded_types(v, excluded)),
        ),
        other => other.clone(),
    }
}

/// Clone a `MethodDef`, substituting any excluded named-type references in its
/// parameters and return type with `TypeRef::Json`. See [`substitute_excluded_types`].
fn method_with_excluded_substituted(method: &MethodDef, excluded: &HashSet<&str>) -> MethodDef {
    let mut m = method.clone();
    for p in &mut m.params {
        p.ty = substitute_excluded_types(&p.ty, excluded);
    }
    m.return_type = substitute_excluded_types(&m.return_type, excluded);
    m
}
/// Generate Go trait bridge interface, CGo callback trampolines, and registration functions.
///
/// # CGo callback strategy
///
/// CGo does not allow passing Go function values as C function pointers. The generated
/// code uses `cgo.Handle` to store the Go object and look it up in trampolines:
///
/// - A `//export go{Trait}{Method}` function receives `user_data unsafe.Pointer`.
/// - Inside the trampoline, `user_data` is converted to a `cgo.Handle` and dereferenced
///   to retrieve the Go object.
/// - The object's method is called, and the result is marshalled back to C.
/// - A static C helper in the CGo preamble constructs the vtable struct with trampoline
///   function pointers — this is valid because CGo compiles the preamble together with
///   the Go file that carries the `//export` declarations.
///
/// # Registration
///
/// `Register{Trait}(impl {Trait}) error` builds the C vtable, calls the C registration
/// function, and returns any error. The handle remains valid for the lifetime of the
/// plugin; a corresponding `Unregister{Trait}(name string) error` removes it.
///
/// Generate the complete trait_bridges.go file content for all configured trait bridges.
///
/// `pkg_name`: Go package name (e.g., `"sample_core"`).
/// `ffi_prefix`: C function prefix (e.g., `"sample_core"`).
/// `ffi_header`: C header filename (e.g., `"sample_core.h"`).
/// `ffi_crate_dir`: path from go output dir to the FFI crate dir.
/// `to_root`: relative path from go output dir to the repo root.
/// `crate_name`: Rust FFI crate name (e.g., `"sample_core"`), used to derive C type names.
#[allow(clippy::too_many_arguments)]
pub fn gen_trait_bridges_file(
    api: &ApiSurface,
    config: &crate::core::config::ResolvedCrateConfig,
    pkg_name: &str,
    ffi_prefix: &str,
    ffi_header: &str,
    ffi_crate_dir: &str,
    to_root: &str,
    crate_name: &str,
) -> String {
    let mut out = String::with_capacity(16_384);

    // Collect names of types that are present in the IR but explicitly excluded from the
    // public binding surface (typically via `#[cfg_attr(alef, alef(skip))]` or
    // type-level config exclusions). Trait-bridge interface signatures referencing any
    // such type must fall back to `json.RawMessage`: the corresponding Go type was never
    // emitted into binding.go and would otherwise produce `undefined: <Name>` build
    // errors. The IR exposes these names in `excluded_type_paths` — that includes both
    // types stripped from `api.types` entirely and types still present with
    // `binding_excluded = true`. We also union in any `binding_excluded` type names from
    // `api.types` defensively.
    let excluded_named_types: HashSet<&str> = api
        .excluded_type_paths
        .keys()
        .map(|s| s.as_str())
        .chain(api.types.iter().filter(|t| t.binding_excluded).map(|t| t.name.as_str()))
        .collect();

    out.push_str(&hash::header(CommentStyle::DoubleSlash));
    // NOTE: package_and_cgo.jinja already emits "package {name}\n\n/*\n#cgo..."
    // so we render it directly — do NOT push a separate "/*\n" before this call.
    out.push_str(&crate::backends::go::template_env::render(
        "package_and_cgo.jinja",
        minijinja::context! {
            pkg_name => pkg_name,
            to_root => to_root,
            ffi_crate_dir => ffi_crate_dir,
            ffi_header => ffi_header,
        },
    ));
    out.push('\n');

    // Forward-declare all exported Go trampolines in the CGO preamble so that
    // C code can reference them. These are Go functions with //export directives
    // that will be linked when the Go code is compiled.
    for bridge_cfg in &config.trait_bridges {
        if let Some(trait_def) = api.types.iter().find(|t| t.name == bridge_cfg.trait_name) {
            let pascal = bridge_cfg.trait_name.to_pascal_case();
            for method in trait_def
                .methods
                .iter()
                .filter(|m| !bridge_cfg.ffi_skip_methods.contains(&m.name))
            {
                let export_name = format!("go{}{}", &pascal, method.name.to_pascal_case());
                let method_substituted = method_with_excluded_substituted(method, &excluded_named_types);
                let c_sig = c_trampoline_signature(&export_name, &method_substituted);
                out.push_str(&crate::backends::go::template_env::render(
                    "extern_trampoline_decl.jinja",
                    minijinja::context! {
                        export_name => export_name,
                        c_sig => c_sig,
                    },
                ));
            }
            // Plugin lifecycle trampolines
            out.push_str(&crate::backends::go::template_env::render(
                "plugin_trampoline_decl.jinja",
                minijinja::context! {
                    pascal => pascal.clone(),
                    method => "Name",
                },
            ));
            out.push_str(&crate::backends::go::template_env::render(
                "plugin_trampoline_decl.jinja",
                minijinja::context! {
                    pascal => pascal.clone(),
                    method => "Version",
                },
            ));
            out.push_str(&crate::backends::go::template_env::render(
                "plugin_trampoline_decl.jinja",
                minijinja::context! {
                    pascal => pascal.clone(),
                    method => "Initialize",
                },
            ));
            out.push_str(&crate::backends::go::template_env::render(
                "plugin_trampoline_decl.jinja",
                minijinja::context! {
                    pascal => pascal.clone(),
                    method => "Shutdown",
                },
            ));
            out.push_str(&crate::backends::go::template_env::render(
                "plugin_free_user_data_extern.jinja",
                minijinja::context! {
                    pascal => &pascal,
                },
            ));
        }
    }

    out.push_str("*/\n");
    out.push_str("import \"C\"\n");
    out.push('\n');

    out.push_str("import (\n");
    out.push_str("\t\"encoding/json\"\n");
    out.push_str("\t\"fmt\"\n");
    out.push_str("\t\"runtime/cgo\"\n");
    out.push_str("\t\"sync\"\n");
    out.push_str("\t\"unsafe\"\n");
    out.push_str(")\n");
    out.push('\n');

    // Generate handle registry type and instances for all trait bridges.
    // Each trait needs a registry to track cgo.Handles by name for cleanup on unregister.
    let has_trait_bridges = config.trait_bridges.iter().any(|cfg| {
        !cfg.exclude_languages.iter().any(|lang| lang == "go") && api.types.iter().any(|t| t.name == cfg.trait_name)
    });

    if has_trait_bridges {
        out.push_str("// handleRegistry tracks cgo.Handles by name to ensure proper cleanup on unregister.\n");
        out.push_str("// Without this, unregistered plugins can cause use-after-free crashes when Rust\n");
        out.push_str("// still holds vtable pointers and tries to invoke callbacks on deleted handles.\n");
        out.push_str("type handleRegistry struct {\n");
        out.push_str("\tmu      sync.Mutex\n");
        out.push_str("\thandles map[string]cgo.Handle\n");
        out.push_str("}\n");
        out.push('\n');

        out.push_str("var (\n");
        for bridge_cfg in &config.trait_bridges {
            if !bridge_cfg.exclude_languages.iter().any(|lang| lang == "go")
                && api.types.iter().any(|t| t.name == bridge_cfg.trait_name)
            {
                let trait_snake = heck::AsSnakeCase(&bridge_cfg.trait_name).to_string();
                out.push_str(&format!(
                    "\t{trait_snake}Registry = &handleRegistry{{handles: make(map[string]cgo.Handle)}}\n"
                ));
            }
        }
        out.push_str(")\n");
        out.push('\n');

        // Generate handle registry methods.
        out.push_str("// store adds a handle to the registry, keyed by name.\n");
        out.push_str("func (reg *handleRegistry) store(name string, handle cgo.Handle) {\n");
        out.push_str("\treg.mu.Lock()\n");
        out.push_str("\tdefer reg.mu.Unlock()\n");
        out.push_str("\tif old, ok := reg.handles[name]; ok {\n");
        out.push_str("\t\told.Delete()\n");
        out.push_str("\t}\n");
        out.push_str("\treg.handles[name] = handle\n");
        out.push_str("}\n");
        out.push('\n');

        out.push_str("// delete removes and deletes a handle from the registry by name.\n");
        out.push_str("func (reg *handleRegistry) delete(name string) {\n");
        out.push_str("\treg.mu.Lock()\n");
        out.push_str("\tdefer reg.mu.Unlock()\n");
        out.push_str("\tif handle, ok := reg.handles[name]; ok {\n");
        out.push_str("\t\tdelete(reg.handles, name)\n");
        out.push_str("\t\thandle.Delete()\n");
        out.push_str("\t}\n");
        out.push_str("}\n");
        out.push('\n');

        out.push_str("// clear removes and deletes all handles from the registry.\n");
        out.push_str("func (reg *handleRegistry) clear() {\n");
        out.push_str("\treg.mu.Lock()\n");
        out.push_str("\tdefer reg.mu.Unlock()\n");
        out.push_str("\tfor _, handle := range reg.handles {\n");
        out.push_str("\t\thandle.Delete()\n");
        out.push_str("\t}\n");
        out.push_str("\treg.handles = make(map[string]cgo.Handle)\n");
        out.push_str("}\n");
        out.push('\n');
    }

    // Generate interfaces, trampolines, and registration functions for each bridge
    for bridge_cfg in &config.trait_bridges {
        // Skip trait bridges excluded for this language
        if bridge_cfg.exclude_languages.iter().any(|lang| lang == "go") {
            continue;
        }
        if let Some(trait_def) = api.types.iter().find(|t| t.name == bridge_cfg.trait_name) {
            let trait_snake = heck::AsSnakeCase(&trait_def.name).to_string();
            gen_trait_bridge(
                &mut out,
                trait_def,
                bridge_cfg,
                ffi_prefix,
                crate_name,
                &excluded_named_types,
                &trait_snake,
            );
            out.push('\n');
        }
    }

    out
}

/// Generate one trait bridge: interface, trampolines, registration/unregistration functions.
fn gen_trait_bridge(
    out: &mut String,
    trait_def: &TypeDef,
    bridge_cfg: &TraitBridgeConfig,
    ffi_prefix: &str,
    crate_name: &str,
    excluded_named_types: &HashSet<&str>,
    #[allow(unused_variables)] trait_snake: &str,
) {
    let trait_name = &trait_def.name;
    let trait_snake = heck::AsSnakeCase(trait_name).to_string();
    let trait_pascal = trait_name.to_pascal_case();

    // Derive C VTable struct name: {CRATE_UPPER}{CratePascal}{TraitPascal}VTable
    // E.g., for crate="sample_core", trait="TextBackend": SAMPLE_CRATESampleCrateTextBackendVTable
    // Hyphens in crate names are not valid in C identifiers;
    // normalize the same way ffi_prefix does (`-` → `_`) before uppercasing.
    let crate_normalized = crate_name.replace('-', "_");
    let crate_upper = crate_normalized.to_uppercase();
    let crate_pascal = crate_normalized.to_pascal_case();
    let c_vtable_struct = format!("{}{}{}{}", crate_upper, crate_pascal, trait_pascal, "VTable");

    // =========================================================================
    // Go interface
    // =========================================================================
    out.push_str(&crate::backends::go::template_env::render(
        "trait_interface_header.jinja",
        minijinja::context! {
            name => trait_name,
        },
    ));

    // Plugin methods (name, version, initialize, shutdown)
    out.push_str(&crate::backends::go::template_env::render(
        "plugin_method_signature.jinja",
        minijinja::context! {
            doc => "Name returns the plugin name.",
            method => "Name",
            return_type => "string",
        },
    ));

    out.push_str(&crate::backends::go::template_env::render(
        "plugin_method_signature.jinja",
        minijinja::context! {
            doc => "Version returns the plugin version.",
            method => "Version",
            return_type => "string",
        },
    ));

    out.push_str(&crate::backends::go::template_env::render(
        "plugin_method_signature.jinja",
        minijinja::context! {
            doc => "Initialize is called when the plugin is loaded.",
            method => "Initialize",
            return_type => "error",
        },
    ));

    out.push_str(&crate::backends::go::template_env::render(
        "plugin_method_signature.jinja",
        minijinja::context! {
            doc => "Shutdown is called when the plugin is unloaded.",
            method => "Shutdown",
            return_type => "error",
        },
    ));

    // Trait methods (skip FFI-incompatible ones — they have no C VTable slot).
    for method in trait_def
        .methods
        .iter()
        .filter(|m| !bridge_cfg.ffi_skip_methods.contains(&m.name))
    {
        let method_substituted = method_with_excluded_substituted(method, excluded_named_types);
        gen_interface_method(out, &method_substituted);
    }

    out.push_str("}\n");
    out.push('\n');

    // =========================================================================
    // Exported trampolines
    // =========================================================================
    for method in trait_def
        .methods
        .iter()
        .filter(|m| !bridge_cfg.ffi_skip_methods.contains(&m.name))
    {
        let export_name = format!("go{}{}", &trait_pascal, method.name.to_pascal_case());
        out.push_str(&crate::backends::go::template_env::render(
            "export_marker.jinja",
            minijinja::context! {
                name => &export_name,
            },
        ));
        let method_substituted = method_with_excluded_substituted(method, excluded_named_types);
        gen_trampoline(out, trait_name, &trait_pascal, &method_substituted);
    }

    // Plugin method trampolines
    gen_plugin_trampolines(out, trait_name, &trait_pascal);

    // =========================================================================
    // Registration function
    // =========================================================================
    out.push_str(&crate::backends::go::template_env::render(
        "register_function_header.jinja",
        minijinja::context! {
            name => trait_name,
        },
    ));

    out.push_str(&crate::backends::go::template_env::render(
        "vtable_struct_init.jinja",
        minijinja::context! {
            c_vtable_struct => &c_vtable_struct,
        },
    ));

    // Set up vtable function pointers (via //export trampolines).
    // cgo declares function pointers as *[0]byte, so cast via unsafe.Pointer.
    // Skip FFI-incompatible methods — they have no C VTable slot.
    for method in trait_def
        .methods
        .iter()
        .filter(|m| !bridge_cfg.ffi_skip_methods.contains(&m.name))
    {
        let export_name = format!("go{}{}", &trait_pascal, method.name.to_pascal_case());
        out.push_str(&crate::backends::go::template_env::render(
            "register_vtable_method_field.jinja",
            minijinja::context! {
                method_name => &method.name,
                export_name => export_name,
            },
        ));
    }

    // Plugin method pointers (cbindgen suffixes lifecycle hooks with `_fn`).
    out.push_str(&crate::backends::go::template_env::render(
        "plugin_trampoline_lifecycle.jinja",
        minijinja::context! {
            field => "name_fn",
            pascal => &trait_pascal,
            method => "Name",
        },
    ));
    out.push_str(&crate::backends::go::template_env::render(
        "plugin_trampoline_lifecycle.jinja",
        minijinja::context! {
            field => "version_fn",
            pascal => &trait_pascal,
            method => "Version",
        },
    ));
    out.push_str(&crate::backends::go::template_env::render(
        "plugin_trampoline_lifecycle.jinja",
        minijinja::context! {
            field => "initialize_fn",
            pascal => &trait_pascal,
            method => "Initialize",
        },
    ));
    out.push_str(&crate::backends::go::template_env::render(
        "plugin_trampoline_lifecycle.jinja",
        minijinja::context! {
            field => "shutdown_fn",
            pascal => &trait_pascal,
            method => "Shutdown",
        },
    ));

    out.push_str(&format!(
        "\t\tfree_string: (*[0]byte)(unsafe.Pointer(C.go{trait_pascal}FreeString)),\n"
    ));

    // free_user_data deletes the cgo.Handle when the bridge is dropped by Rust
    out.push_str(&crate::backends::go::template_env::render(
        "vtable_free_user_data_field.jinja",
        minijinja::context! {
            pascal => &trait_pascal,
        },
    ));

    out.push_str("\t}\n");
    out.push('\n');

    out.push_str(&crate::backends::go::template_env::render(
        "register_c_call.jinja",
        minijinja::context! {
            c_function => format!("{}_register_{}", ffi_prefix, trait_snake),
            ffi_prefix => ffi_prefix,
            trait_name => trait_name,
            trait_snake => trait_snake,
        },
    ));
    out.push_str("}\n");
    out.push('\n');

    // =========================================================================
    // Unregistration function
    // =========================================================================
    out.push_str(&crate::backends::go::template_env::render(
        "unregister_function_header.jinja",
        minijinja::context! {
            name => trait_name,
        },
    ));

    out.push_str(&crate::backends::go::template_env::render(
        "unregister_c_call.jinja",
        minijinja::context! {
            c_function => format!("{}_unregister_{}", ffi_prefix, trait_snake),
            ffi_prefix => ffi_prefix,
            trait_name => trait_name,
            trait_snake => trait_snake,
        },
    ));
    out.push_str("}\n");

    // =========================================================================
    // Config-driven unregistration / clear functions (opt-in via bridge_cfg)
    // =========================================================================
    let unregister_block = gen_unregistration_fn(bridge_cfg, ffi_prefix, trait_name);
    if !unregister_block.is_empty() {
        out.push('\n');
        out.push_str(&unregister_block);
    }

    let clear_block = gen_clear_fn(bridge_cfg, ffi_prefix, trait_name);
    if !clear_block.is_empty() {
        out.push('\n');
        out.push_str(&clear_block);
    }
}

/// Generate a config-driven unregistration wrapper.
///
/// Returns an empty string when `bridge_cfg.unregister_fn` is `None`.
/// Also returns empty string if the configured name is just the snake_case version of
/// the standard `Unregister{TraitName}` PascalCase function (to avoid duplicates).
/// Otherwise emits a Go function whose name is `bridge_cfg.unregister_fn`,
/// accepting a `name string` parameter and calling the C-exported
/// `{ffi_prefix}_unregister_{trait_snake}` function via cgo.
fn gen_unregistration_fn(bridge_cfg: &TraitBridgeConfig, ffi_prefix: &str, trait_name: &str) -> String {
    let Some(fn_name) = bridge_cfg.unregister_fn.as_deref() else {
        return String::new();
    };
    let trait_snake = heck::AsSnakeCase(trait_name).to_string();
    let standard_pascal_name = format!("Unregister{}", trait_name);
    let standard_snake_name = heck::AsSnakeCase(&standard_pascal_name).to_string();

    // Skip if the configured name is the snake_case version of the standard Unregister{TraitName}
    // Go convention is PascalCase only; the PascalCase version is always emitted, so don't duplicate
    if fn_name == standard_snake_name {
        return String::new();
    }

    let c_function = format!("{}_unregister_{}", ffi_prefix, trait_snake);
    // Convert fn_name to PascalCase for Go (e.g., "unregister_text_backend" → "UnregisterTextBackend")
    let go_fn_name = fn_name.to_pascal_case();

    let mut out = String::new();
    out.push_str(&crate::backends::go::template_env::render(
        "unregister_fn_header.jinja",
        minijinja::context! {
            fn_name => &go_fn_name,
            trait_name => trait_name,
        },
    ));
    out.push_str(&crate::backends::go::template_env::render(
        "unregister_c_call.jinja",
        minijinja::context! {
            c_function => c_function,
            ffi_prefix => ffi_prefix,
            trait_name => trait_name,
        },
    ));
    out.push_str("}\n");
    out
}

/// Generate a config-driven clear-all wrapper.
///
/// Returns an empty string when `bridge_cfg.clear_fn` is `None`.
/// Otherwise emits a Go function whose name is `bridge_cfg.clear_fn`,
/// taking no arguments and calling the C-exported
/// `{ffi_prefix}_clear_{trait_snake}` function via cgo.
fn gen_clear_fn(bridge_cfg: &TraitBridgeConfig, ffi_prefix: &str, trait_name: &str) -> String {
    let Some(fn_name) = bridge_cfg.clear_fn.as_deref() else {
        return String::new();
    };
    let trait_snake = heck::AsSnakeCase(trait_name).to_string();
    let c_function = format!("{}_clear_{}", ffi_prefix, trait_snake);
    // Convert fn_name to PascalCase for Go (e.g., "clear_text_backends" → "ClearTextBackends")
    let go_fn_name = fn_name.to_pascal_case();

    let mut out = String::new();
    out.push_str(&crate::backends::go::template_env::render(
        "clear_function_header.jinja",
        minijinja::context! {
            fn_name => &go_fn_name,
            name => trait_name,
        },
    ));
    out.push_str(&crate::backends::go::template_env::render(
        "clear_c_call.jinja",
        minijinja::context! {
            c_function => c_function,
            trait_name => trait_name,
            trait_snake => &trait_snake,
        },
    ));
    out.push_str("}\n");
    out
}

/// Generate the Go interface method signature for a trait method.
fn gen_interface_method(out: &mut String, method: &MethodDef) {
    let mut params = Vec::new();
    for p in &method.params {
        let go_type = rust_to_go_type(&p.ty);
        params.push(format!("{} {}", p.name, go_type));
    }

    let return_type = if method.error_type.is_some() {
        match &method.return_type {
            TypeRef::Unit => "error".to_string(),
            _ => {
                let ret = rust_to_go_type(&method.return_type);
                format!("({}, error)", ret)
            }
        }
    } else {
        rust_to_go_type(&method.return_type)
    };

    let params_str = params.join(", ");
    out.push_str(&crate::backends::go::template_env::render(
        "trait_interface_method.jinja",
        minijinja::context! {
            doc => &method.name,
            method_name => method.name.to_pascal_case(),
            params => params_str,
            return_type => return_type,
        },
    ));
    out.push('\n');
}

/// Generate one trampoline function (implementation without //export).
/// The //export declaration is in binding.go to avoid duplicate definitions.
fn gen_trampoline(out: &mut String, trait_name: &str, trait_pascal: &str, method: &MethodDef) {
    let export_name = format!("go{}{}", trait_pascal, method.name.to_pascal_case());

    let mut params = vec!["userData unsafe.Pointer".to_string()];
    for p in &method.params {
        let c_type = rust_to_c_type(&p.ty);
        params.push(format!("{} {}", p.name, c_type));
        // Bytes params carry a companion length so the trampoline can convert via
        // unsafe.Slice rather than C.GoString (which stops at NUL bytes).
        if matches!(p.ty, TypeRef::Bytes) {
            params.push(format!("{}Len C.size_t", p.name));
        }
    }
    if !matches!(method.return_type, TypeRef::Unit) {
        params.push("outResult **C.char".to_string());
    }
    params.push("outError **C.char".to_string());

    out.push_str(&crate::backends::go::template_env::render(
        "trampoline_signature.jinja",
        minijinja::context! {
            name => export_name,
            params => params,
        },
    ));
    out.push('\n');

    // Retrieve the Go object from the handle
    out.push_str("\thandle := cgo.Handle(uintptr(unsafe.Pointer(userData)))\n");
    out.push_str(&crate::backends::go::template_env::render(
        "handle_type_assertion.jinja",
        minijinja::context! {
            type_name => trait_name,
        },
    ));
    out.push('\n');
    out.push_str("\tif !ok {\n");
    out.push_str("\t\treturn 1  // error: invalid handle\n");
    out.push_str("\t}\n");
    out.push('\n');

    // Convert C parameters to Go
    for p in &method.params {
        gen_param_conversion(out, p);
    }

    // Call the method
    let mut call_args = Vec::new();
    for p in &method.params {
        call_args.push(format!("go{}", capitalize(&p.name)));
    }

    out.push_str("\t// Call the method\n");
    if method.error_type.is_some() {
        // Method returns (value?, error)
        match &method.return_type {
            TypeRef::Unit => {
                // Just returns error
                out.push_str(&crate::backends::go::template_env::render(
                    "impl_method_call_err.jinja",
                    minijinja::context! {
                        method => method.name.to_pascal_case(),
                        args => call_args.join(", "),
                    },
                ));
                out.push('\n');
            }
            _ => {
                // Returns (value, error)
                out.push_str(&crate::backends::go::template_env::render(
                    "impl_method_call_result_err.jinja",
                    minijinja::context! {
                        method => method.name.to_pascal_case(),
                        args => call_args.join(", "),
                    },
                ));
                out.push('\n');
            }
        }
        out.push_str("\tif err != nil {\n");
        out.push_str("\t\tcErr := C.CString(err.Error())\n");
        out.push_str("\t\t*outError = cErr\n");
        out.push_str("\t\treturn 1\n");
        out.push_str("\t}\n");

        // Encode result if not Unit
        if !matches!(&method.return_type, TypeRef::Unit) {
            gen_result_conversion(out, &method.return_type);
        }
    } else {
        // Method returns only value (no error)
        out.push_str(&crate::backends::go::template_env::render(
            "impl_method_call_result.jinja",
            minijinja::context! {
                method => method.name.to_pascal_case(),
                args => call_args.join(", "),
            },
        ));
        out.push('\n');

        // Encode result if not Unit
        if !matches!(&method.return_type, TypeRef::Unit) {
            gen_result_conversion(out, &method.return_type);
        }
    }

    out.push_str("\treturn 0  // success\n");
    out.push_str("}\n");
    out.push('\n');
}

/// Marshal a Go callback result into the C out-result slot.
///
/// The Rust FFI side decodes String/Path/Char callback returns as raw UTF-8 C strings,
/// while Json/Named/Vec/Map returns are parsed as JSON payloads. Keep those contracts
/// separate so string-like returns are not accidentally JSON-quoted and raw JSON payloads
/// are not double-encoded.
fn gen_result_conversion(out: &mut String, return_type: &TypeRef) {
    match return_type {
        TypeRef::String | TypeRef::Char | TypeRef::Path => {
            out.push_str("\tcResult := C.CString(result)\n");
            out.push_str("\t*outResult = cResult\n");
        }
        TypeRef::Json => {
            out.push_str("\tcResult := C.CString(string(result))\n");
            out.push_str("\t*outResult = cResult\n");
        }
        _ => {
            out.push_str("\tjsonBytes, _ := json.Marshal(result)\n");
            out.push_str("\tcResult := C.CString(string(jsonBytes))\n");
            out.push_str("\t*outResult = cResult\n");
        }
    }
}

/// Generate trampolines for plugin methods: Name, Version, Initialize, Shutdown.
fn gen_plugin_trampolines(out: &mut String, trait_name: &str, trait_pascal: &str) {
    // Name trampoline
    out.push_str(&crate::backends::go::template_env::render(
        "export_marker.jinja",
        minijinja::context! {
            name => format!("go{trait_pascal}Name"),
        },
    ));
    out.push_str(&crate::backends::go::template_env::render(
        "plugin_method_trampoline_header.jinja",
        minijinja::context! {
            pascal => &trait_pascal,
            method => "Name",
            params => "userData unsafe.Pointer, outResult **C.char, outError **C.char",
        },
    ));
    out.push('\n');
    out.push_str("\thandle := cgo.Handle(uintptr(unsafe.Pointer(userData)))\n");
    out.push_str(&crate::backends::go::template_env::render(
        "handle_type_assertion.jinja",
        minijinja::context! {
            type_name => trait_name,
        },
    ));
    out.push('\n');
    out.push_str("\tif !ok {\n");
    out.push_str("\t\treturn 1\n");
    out.push_str("\t}\n");
    out.push_str("\tname := impl.Name()\n");
    out.push_str("\tcName := C.CString(name)\n");
    out.push_str("\t*outResult = cName\n");
    out.push_str("\treturn 0\n");
    out.push_str("}\n");
    out.push('\n');

    // Version trampoline
    out.push_str(&crate::backends::go::template_env::render(
        "export_marker.jinja",
        minijinja::context! {
            name => format!("go{trait_pascal}Version"),
        },
    ));
    out.push_str(&crate::backends::go::template_env::render(
        "plugin_method_trampoline_header.jinja",
        minijinja::context! {
            pascal => &trait_pascal,
            method => "Version",
            params => "userData unsafe.Pointer, outResult **C.char, outError **C.char",
        },
    ));
    out.push('\n');
    out.push_str("\thandle := cgo.Handle(uintptr(unsafe.Pointer(userData)))\n");
    out.push_str(&crate::backends::go::template_env::render(
        "handle_type_assertion.jinja",
        minijinja::context! {
            type_name => trait_name,
        },
    ));
    out.push('\n');
    out.push_str("\tif !ok {\n");
    out.push_str("\t\treturn 1\n");
    out.push_str("\t}\n");
    out.push_str("\tversion := impl.Version()\n");
    out.push_str("\tcVersion := C.CString(version)\n");
    out.push_str("\t*outResult = cVersion\n");
    out.push_str("\treturn 0\n");
    out.push_str("}\n");
    out.push('\n');

    // Initialize trampoline
    out.push_str(&crate::backends::go::template_env::render(
        "export_marker.jinja",
        minijinja::context! {
            name => format!("go{trait_pascal}Initialize"),
        },
    ));
    out.push_str(&crate::backends::go::template_env::render(
        "plugin_method_trampoline_header.jinja",
        minijinja::context! {
            pascal => &trait_pascal,
            method => "Initialize",
            params => "userData unsafe.Pointer, outError **C.char",
        },
    ));
    out.push('\n');
    out.push_str("\thandle := cgo.Handle(uintptr(unsafe.Pointer(userData)))\n");
    out.push_str(&crate::backends::go::template_env::render(
        "handle_type_assertion.jinja",
        minijinja::context! {
            type_name => trait_name,
        },
    ));
    out.push('\n');
    out.push_str("\tif !ok {\n");
    out.push_str("\t\treturn 1\n");
    out.push_str("\t}\n");
    out.push_str("\terr := impl.Initialize()\n");
    out.push_str("\tif err != nil {\n");
    out.push_str("\t\tcErr := C.CString(err.Error())\n");
    out.push_str("\t\t*outError = cErr\n");
    out.push_str("\t\treturn 1\n");
    out.push_str("\t}\n");
    out.push_str("\treturn 0\n");
    out.push_str("}\n");
    out.push('\n');

    // Shutdown trampoline
    out.push_str(&crate::backends::go::template_env::render(
        "export_marker.jinja",
        minijinja::context! {
            name => format!("go{trait_pascal}Shutdown"),
        },
    ));
    out.push_str(&crate::backends::go::template_env::render(
        "plugin_method_trampoline_header.jinja",
        minijinja::context! {
            pascal => &trait_pascal,
            method => "Shutdown",
            params => "userData unsafe.Pointer, outError **C.char",
        },
    ));
    out.push('\n');
    out.push_str("\thandle := cgo.Handle(uintptr(unsafe.Pointer(userData)))\n");
    out.push_str(&crate::backends::go::template_env::render(
        "handle_type_assertion.jinja",
        minijinja::context! {
            type_name => trait_name,
        },
    ));
    out.push('\n');
    out.push_str("\tif !ok {\n");
    out.push_str("\t\treturn 1\n");
    out.push_str("\t}\n");
    out.push_str("\terr := impl.Shutdown()\n");
    out.push_str("\tif err != nil {\n");
    out.push_str("\t\tcErr := C.CString(err.Error())\n");
    out.push_str("\t\t*outError = cErr\n");
    out.push_str("\t\treturn 1\n");
    out.push_str("\t}\n");
    out.push_str("\treturn 0\n");
    out.push_str("}\n");
    out.push('\n');

    // FreeUserData trampoline — called by Rust Drop (Go 1.26+ cleanup-queue).
    // DO NOT call cgo.Handle.Delete() here: Go's cleanup-queue runs finalizers in a
    // context where they may panic if the handle is invalid or already deleted.
    // Instead, rely on explicit Unregister() calls for proper cleanup.
    out.push_str(&crate::backends::go::template_env::render(
        "export_marker.jinja",
        minijinja::context! {
            name => format!("go{trait_pascal}FreeUserData"),
        },
    ));
    out.push_str(&crate::backends::go::template_env::render(
        "plugin_free_user_data_func.jinja",
        minijinja::context! {
            pascal => &trait_pascal,
        },
    ));
    out.push('\n');
    out.push_str("\t// No-op to avoid cleanup-queue panics. Handles cleaned in Unregister().\n");
    out.push_str("}\n");
    out.push('\n');

    out.push_str(&crate::backends::go::template_env::render(
        "export_marker.jinja",
        minijinja::context! {
            name => format!("go{trait_pascal}FreeString"),
        },
    ));
    out.push_str(&format!("func go{trait_pascal}FreeString(ptr *C.char) {{\n"));
    out.push_str("\tif ptr != nil {\n");
    out.push_str("\t\tC.free(unsafe.Pointer(ptr))\n");
    out.push_str("\t}\n");
    out.push_str("}\n");
    out.push('\n');
}

/// Build the C trampoline function signature for extern declaration in the CGo preamble.
/// Uses actual C types (not Go CGo types like `C.int32_t`).
#[allow(dead_code)]
fn c_trampoline_signature(_export_name: &str, method: &MethodDef) -> String {
    let mut params = vec!["void* user_data".to_string()];
    for p in &method.params {
        let cty = rust_to_plain_c_type(&p.ty);
        params.push(format!("{} {}", cty, p.name));
        // Bytes params carry a companion length so the trampoline can read the full
        // buffer without NUL-truncation (mirrors vtable.rs / call_body.rs pattern).
        if matches!(p.ty, TypeRef::Bytes) {
            params.push(format!("size_t {}_len", p.name));
        }
    }
    if !matches!(method.return_type, TypeRef::Unit) {
        params.push("char** out_result".to_string());
    }
    params.push("char** out_error".to_string());
    params.join(", ")
}

/// Convert a Rust TypeRef to a plain C type string (for CGo preamble extern declarations).
#[allow(dead_code)]
fn rust_to_plain_c_type(ty: &TypeRef) -> String {
    match ty {
        TypeRef::Primitive(p) => {
            use crate::core::ir::PrimitiveType::*;
            match p {
                Bool => "int32_t",
                U8 => "uint8_t",
                U16 => "uint16_t",
                U32 => "uint32_t",
                U64 => "uint64_t",
                I8 => "int8_t",
                I16 => "int16_t",
                I32 => "int32_t",
                I64 => "int64_t",
                F32 => "float",
                F64 => "double",
                Usize => "size_t",
                Isize => "intptr_t",
            }
            .to_string()
        }
        TypeRef::String | TypeRef::Char | TypeRef::Path => "char*".to_string(),
        TypeRef::Bytes => "uint8_t*".to_string(),
        TypeRef::Optional(_) | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) => "char*".to_string(),
        TypeRef::Unit => "void".to_string(),
        TypeRef::Duration => "uint64_t".to_string(),
        _ => "char*".to_string(),
    }
}

/// Convert a Rust TypeRef to a Go type string.
/// Uses the type_map module for consistent type resolution, which handles Named types correctly.
fn rust_to_go_type(ty: &TypeRef) -> String {
    go_type(ty).into_owned()
}

/// Convert a Rust TypeRef to a C type string.
fn rust_to_c_type(ty: &TypeRef) -> String {
    match ty {
        TypeRef::Primitive(p) => {
            use crate::core::ir::PrimitiveType::*;
            match p {
                Bool => "C.int32_t",
                U8 => "C.uint8_t",
                U16 => "C.uint16_t",
                U32 => "C.uint32_t",
                U64 => "C.uint64_t",
                I8 => "C.int8_t",
                I16 => "C.int16_t",
                I32 => "C.int32_t",
                I64 => "C.int64_t",
                F32 => "C.float",
                F64 => "C.double",
                Usize => "C.size_t",
                Isize => "C.intptr_t",
            }
            .to_string()
        }
        TypeRef::String | TypeRef::Char | TypeRef::Path => "*C.char".to_string(),
        TypeRef::Bytes => "*C.uint8_t".to_string(),
        TypeRef::Optional(_) => "*C.char".to_string(), // JSON-encoded
        TypeRef::Vec(_) => "*C.char".to_string(),      // JSON-encoded
        TypeRef::Map(_, _) => "*C.char".to_string(),   // JSON-encoded
        TypeRef::Unit => "C.void".to_string(),
        TypeRef::Duration => "C.uint64_t".to_string(),
        TypeRef::Named(_) => "*C.char".to_string(), // JSON-encoded
        _ => "*C.char".to_string(),
    }
}

/// Generate parameter conversion code (C to Go).
fn gen_param_conversion(out: &mut String, param: &crate::core::ir::ParamDef) {
    let var_name = format!("go{}", capitalize(&param.name));
    match &param.ty {
        TypeRef::String | TypeRef::Char | TypeRef::Path => {
            out.push_str(&crate::backends::go::template_env::render(
                "go_string_cast.jinja",
                minijinja::context! {
                    name => capitalize(&param.name),
                    param => param.name.as_str(),
                },
            ));
            out.push('\n');
        }
        TypeRef::Bytes => {
            // Use unsafe.Slice(ptr, len) so the full byte slice is available even
            // when the data contains embedded NUL bytes (fixes issue #114).
            // The companion {name}Len parameter is emitted alongside the pointer.
            let name = &param.name;
            let len_name = format!("{name}Len");
            out.push_str(&format!(
                "\tvar {var_name} []byte\n\
                 \tif {name} != nil {{\n\
                 \t\t{var_name} = unsafe.Slice((*byte)(unsafe.Pointer({name})), int({len_name}))\n\
                 \t}}\n\n"
            ));
        }
        TypeRef::Vec(_) => {
            // Vec types unmarshal directly from JSON array
            let go_type = rust_to_go_type(&param.ty);
            out.push_str(&crate::backends::go::template_env::render(
                "var_type_decl.jinja",
                minijinja::context! {
                    var_name => &var_name,
                    type_name => &go_type,
                },
            ));
            out.push_str(&crate::backends::go::template_env::render(
                "if_nil_check.jinja",
                minijinja::context! {
                    param => param.name.as_str(),
                },
            ));
            out.push_str(&crate::backends::go::template_env::render(
                "json_unmarshal_simple.jinja",
                minijinja::context! {
                    param => param.name.as_str(),
                    var_name => &var_name,
                },
            ));
            out.push('\n');
            out.push_str("\t}\n");
            out.push('\n');
        }
        TypeRef::Named(_) => {
            // Named types (structs/config types) unmarshal directly into concrete type
            let go_type = rust_to_go_type(&param.ty);
            out.push_str(&crate::backends::go::template_env::render(
                "var_type_decl.jinja",
                minijinja::context! {
                    var_name => &var_name,
                    type_name => &go_type,
                },
            ));
            out.push_str(&crate::backends::go::template_env::render(
                "if_nil_check.jinja",
                minijinja::context! {
                    param => param.name.as_str(),
                },
            ));
            // Unmarshal directly into the concrete Go type
            out.push_str(&crate::backends::go::template_env::render(
                "json_unmarshal_simple.jinja",
                minijinja::context! {
                    param => param.name.as_str(),
                    var_name => &var_name,
                },
            ));
            out.push('\n');
            out.push_str("\t}\n");
            out.push('\n');
        }
        TypeRef::Map(_, _) => {
            // Map types unmarshal as map[string]interface{}
            let go_type = rust_to_go_type(&param.ty);
            out.push_str(&crate::backends::go::template_env::render(
                "var_type_decl.jinja",
                minijinja::context! {
                    var_name => &var_name,
                    type_name => &go_type,
                },
            ));
            out.push_str(&crate::backends::go::template_env::render(
                "if_nil_check.jinja",
                minijinja::context! {
                    param => param.name.as_str(),
                },
            ));
            out.push_str("\t\tvar rawData interface{}\n");
            out.push_str(&crate::backends::go::template_env::render(
                "json_unmarshal_rawdata.jinja",
                minijinja::context! {
                    param => param.name.as_str(),
                },
            ));
            out.push('\n');
            out.push_str("\t\tif m, ok := rawData.(map[string]interface{}); ok {\n");
            out.push_str(&crate::backends::go::template_env::render(
                "var_assign_m.jinja",
                minijinja::context! {
                    var => &var_name,
                },
            ));
            out.push('\n');
            out.push_str("\t\t}\n");
            out.push_str("\t}\n");
            out.push('\n');
        }
        TypeRef::Optional(_) => {
            // Optional types
            let go_type = rust_to_go_type(&param.ty);
            out.push_str(&crate::backends::go::template_env::render(
                "var_type_decl.jinja",
                minijinja::context! {
                    var_name => &var_name,
                    type_name => &go_type,
                },
            ));
            out.push_str(&crate::backends::go::template_env::render(
                "if_nil_check.jinja",
                minijinja::context! {
                    param => param.name.as_str(),
                },
            ));
            out.push_str("\t\tvar rawData interface{}\n");
            out.push_str(&crate::backends::go::template_env::render(
                "json_unmarshal_rawdata.jinja",
                minijinja::context! {
                    param => param.name.as_str(),
                },
            ));
            out.push('\n');
            out.push_str("\t\tif m, ok := rawData.(map[string]interface{}); ok {\n");
            out.push_str(&crate::backends::go::template_env::render(
                "var_assign_m.jinja",
                minijinja::context! {
                    var => &var_name,
                },
            ));
            out.push('\n');
            out.push_str("\t\t}\n");
            out.push_str("\t}\n");
            out.push('\n');
        }
        TypeRef::Json => {
            // Trait-bridge fallback for binding-excluded named types. The Go-side type is
            // `json.RawMessage`; the C-side carries the JSON payload as a NUL-terminated
            // string. Copy the bytes through so the user gets the raw JSON document
            // without forcing them to unmarshal into a Go type that was never emitted.
            out.push_str(&format!("\tvar {var_name} json.RawMessage\n"));
            out.push_str(&crate::backends::go::template_env::render(
                "if_nil_check.jinja",
                minijinja::context! {
                    param => param.name.as_str(),
                },
            ));
            out.push_str(&format!(
                "\t\t{var_name} = json.RawMessage(C.GoString({}))\n",
                param.name
            ));
            out.push_str("\t}\n");
            out.push('\n');
        }
        TypeRef::Primitive(p) => {
            use crate::core::ir::PrimitiveType::*;
            let cast = match p {
                Bool => format!("{} != 0", param.name),
                _ => {
                    // Get the Go type for this primitive
                    let go_type = match p {
                        U8 => "uint8",
                        U16 => "uint16",
                        U32 => "uint32",
                        U64 => "uint64",
                        I8 => "int8",
                        I16 => "int16",
                        I32 => "int32",
                        I64 => "int64",
                        F32 => "float32",
                        F64 => "float64",
                        Usize => "uint",
                        Isize => "int",
                        _ => "",
                    };
                    format!("{}({})", go_type, param.name)
                }
            };
            out.push_str(&crate::backends::go::template_env::render(
                "var_assign_cast.jinja",
                minijinja::context! {
                    var_name => &var_name,
                    cast => &cast,
                },
            ));
            out.push('\n');
            out.push('\n');
        }
        _ => {
            out.push_str(&crate::backends::go::template_env::render(
                "var_assign_cast.jinja",
                minijinja::context! {
                    var_name => &var_name,
                    cast => param.name.as_str(),
                },
            ));
            out.push('\n');
            out.push('\n');
        }
    }
}

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

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_vtable_struct_name_derivation() {
        // Test the pattern: {CRATE_UPPER}{CratePascal}{TraitPascal}VTable
        let crate_name = "sample_crate";
        let crate_upper = crate_name.to_uppercase();
        let crate_pascal = crate_name.to_pascal_case();
        let trait_name = "OcrBackend";
        let trait_pascal = trait_name.to_pascal_case();

        let c_vtable_struct = format!("{}{}{}{}", crate_upper, crate_pascal, trait_pascal, "VTable");

        assert_eq!(c_vtable_struct, "SAMPLE_CRATESampleCrateOcrBackendVTable");
    }

    #[test]
    fn test_register_function_name_format() {
        // Test the pattern: {ffi_prefix}_register_{trait_snake}
        let ffi_prefix = "sample_crate";
        let trait_name = "OcrBackend";
        let trait_snake = heck::AsSnakeCase(trait_name).to_string();

        let register_fn = format!("{}_register_{}", ffi_prefix, trait_snake);
        assert_eq!(register_fn, "sample_crate_register_ocr_backend");
    }

    #[test]
    fn test_unregister_function_name_format() {
        // Test the pattern: {ffi_prefix}_unregister_{trait_snake}
        let ffi_prefix = "sample_crate";
        let trait_name = "PostProcessor";
        let trait_snake = heck::AsSnakeCase(trait_name).to_string();

        let unregister_fn = format!("{}_unregister_{}", ffi_prefix, trait_snake);
        assert_eq!(unregister_fn, "sample_crate_unregister_post_processor");
    }

    #[test]
    fn test_vtable_struct_name_multiple_traits() {
        // Verify correct naming for multiple traits
        let test_cases = vec![
            ("sample_crate", "OcrBackend", "SAMPLE_CRATESampleCrateOcrBackendVTable"),
            (
                "sample_crate",
                "PostProcessor",
                "SAMPLE_CRATESampleCratePostProcessorVTable",
            ),
            ("sample_crate", "Validator", "SAMPLE_CRATESampleCrateValidatorVTable"),
            (
                "sample_crate",
                "EmbeddingBackend",
                "SAMPLE_CRATESampleCrateEmbeddingBackendVTable",
            ),
        ];

        for (crate_name, trait_name, expected_struct) in test_cases {
            let crate_upper = crate_name.to_uppercase();
            let crate_pascal = crate_name.to_pascal_case();
            let trait_pascal = trait_name.to_pascal_case();
            let c_vtable_struct = format!("{}{}{}{}", crate_upper, crate_pascal, trait_pascal, "VTable");

            assert_eq!(
                c_vtable_struct, expected_struct,
                "Mismatch for crate={}, trait={}",
                crate_name, trait_name
            );
        }
    }

    #[test]
    fn gen_unregistration_fn_returns_empty_when_none() {
        let cfg = crate::core::config::TraitBridgeConfig {
            trait_name: "OcrBackend".to_string(),
            unregister_fn: None,
            clear_fn: None,
            ..Default::default()
        };
        let result = gen_unregistration_fn(&cfg, "sample_crate", "OcrBackend");
        assert!(result.is_empty(), "expected empty output when unregister_fn is None");
    }

    #[test]
    fn gen_unregistration_fn_emits_wrapper_when_set() {
        let cfg = crate::core::config::TraitBridgeConfig {
            trait_name: "OcrBackend".to_string(),
            unregister_fn: Some("remove_ocr_backend".to_string()),
            clear_fn: None,
            ..Default::default()
        };
        let result = gen_unregistration_fn(&cfg, "sample_crate", "OcrBackend");
        assert!(
            !result.is_empty(),
            "expected non-empty output when unregister_fn is set"
        );
        assert!(
            result.contains("func RemoveOcrBackend(name string) error"),
            "generated function signature not found in:\n{result}"
        );
        assert!(
            result.contains("C.sample_crate_unregister_ocr_backend"),
            "C call not found in:\n{result}"
        );
    }

    #[test]
    fn gen_clear_fn_returns_empty_when_none() {
        let cfg = crate::core::config::TraitBridgeConfig {
            trait_name: "OcrBackend".to_string(),
            unregister_fn: None,
            clear_fn: None,
            ..Default::default()
        };
        let result = gen_clear_fn(&cfg, "sample_crate", "OcrBackend");
        assert!(result.is_empty(), "expected empty output when clear_fn is None");
    }

    #[test]
    fn gen_clear_fn_emits_wrapper_when_set() {
        let cfg = crate::core::config::TraitBridgeConfig {
            trait_name: "OcrBackend".to_string(),
            unregister_fn: None,
            clear_fn: Some("clear_ocr_backends".to_string()),
            ..Default::default()
        };
        let result = gen_clear_fn(&cfg, "sample_crate", "OcrBackend");
        assert!(!result.is_empty(), "expected non-empty output when clear_fn is set");
        assert!(
            result.contains("func ClearOcrBackends() error"),
            "generated function signature not found in:\n{result}"
        );
        assert!(
            result.contains("C.sample_crate_clear_ocr_backend"),
            "C call not found in:\n{result}"
        );
    }

    #[test]
    fn substitute_excluded_types_replaces_excluded_named_with_json() {
        let mut excluded = HashSet::new();
        excluded.insert("InternalDocument");
        // Excluded named type collapses to Json so the Go trait-bridge interface
        // can fall back to `json.RawMessage`.
        let result = substitute_excluded_types(&TypeRef::Named("InternalDocument".to_string()), &excluded);
        assert!(matches!(result, TypeRef::Json), "expected Json, got {:?}", result);
    }

    #[test]
    fn substitute_excluded_types_leaves_non_excluded_named_intact() {
        let excluded: HashSet<&str> = HashSet::new();
        let result = substitute_excluded_types(&TypeRef::Named("ExtractionConfig".to_string()), &excluded);
        match result {
            TypeRef::Named(ref n) => assert_eq!(n, "ExtractionConfig"),
            other => panic!("expected Named, got {:?}", other),
        }
    }

    #[test]
    fn substitute_excluded_types_recurses_into_optional_vec_map() {
        let mut excluded = HashSet::new();
        excluded.insert("X");
        excluded.insert("Y");
        excluded.insert("Z");
        // Optional<Named("X")> → Optional<Json>
        let opt = TypeRef::Optional(Box::new(TypeRef::Named("X".to_string())));
        match substitute_excluded_types(&opt, &excluded) {
            TypeRef::Optional(inner) => assert!(matches!(*inner, TypeRef::Json)),
            other => panic!("expected Optional<Json>, got {:?}", other),
        }
        // Vec<Named("Y")> → Vec<Json>
        let v = TypeRef::Vec(Box::new(TypeRef::Named("Y".to_string())));
        match substitute_excluded_types(&v, &excluded) {
            TypeRef::Vec(inner) => assert!(matches!(*inner, TypeRef::Json)),
            other => panic!("expected Vec<Json>, got {:?}", other),
        }
        // Map<String, Named("Z")> → Map<String, Json>
        let m = TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::Named("Z".to_string())));
        match substitute_excluded_types(&m, &excluded) {
            TypeRef::Map(k, v) => {
                assert!(matches!(*k, TypeRef::String));
                assert!(matches!(*v, TypeRef::Json));
            }
            other => panic!("expected Map<String, Json>, got {:?}", other),
        }
    }

    /// Regression (#114): the CGo trampoline signature for a Bytes parameter must include
    /// a companion `{name}Len C.size_t` parameter.  Without it the trampoline has no way
    /// to bound the read when the payload contains embedded NUL bytes (0x00).
    #[test]
    fn trampoline_bytes_param_includes_len_companion() {
        let method = crate::core::ir::MethodDef {
            name: "process".to_string(),
            params: vec![crate::core::ir::ParamDef {
                name: "payload".to_string(),
                ty: TypeRef::Bytes,
                optional: false,
                default: None,
                sanitized: false,
                typed_default: None,
                is_ref: true,
                is_mut: false,
                newtype_wrapper: None,
                original_type: None,
                map_is_ahash: false,
                map_key_is_cow: false,
                vec_inner_is_ref: false,
                    map_is_btree: false,
                    core_wrapper: crate::core::ir::CoreWrapper::None,
            }],
            return_type: TypeRef::Unit,
            is_async: false,
            is_static: false,
            error_type: Some("Error".to_string()),
            doc: String::new(),
            receiver: Some(crate::core::ir::ReceiverKind::Ref),
            sanitized: false,
            trait_source: None,
            returns_ref: false,
            returns_cow: false,
            return_newtype_wrapper: None,
            has_default_impl: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
        };
        let mut out = String::new();
        gen_trampoline(&mut out, "Ingester", "Ingester", &method);

        // The CGo trampoline must declare the length companion so the Go body can
        // use unsafe.Slice(ptr, len) rather than C.GoString() which stops at 0x00.
        assert!(
            out.contains("payloadLen C.size_t"),
            "trampoline must include `payloadLen C.size_t` for Bytes param;\nactual:\n{out}"
        );
        // The conversion body must use unsafe.Slice, not GoString (which NUL-truncates).
        assert!(
            out.contains("unsafe.Slice"),
            "trampoline conversion must use unsafe.Slice for Bytes param;\nactual:\n{out}"
        );
        // Must NOT fall back to the old base64 roundtrip.
        assert!(
            !out.contains("base64"),
            "trampoline must not use base64 encoding for Bytes param;\nactual:\n{out}"
        );
    }

    /// Regression (#114): the C preamble extern declaration for a Bytes parameter
    /// must include `size_t {name}_len` so the linker resolves the trampoline correctly.
    #[test]
    fn c_trampoline_signature_bytes_param_includes_len_companion() {
        let method = crate::core::ir::MethodDef {
            name: "process".to_string(),
            params: vec![crate::core::ir::ParamDef {
                name: "payload".to_string(),
                ty: TypeRef::Bytes,
                optional: false,
                default: None,
                sanitized: false,
                typed_default: None,
                is_ref: true,
                is_mut: false,
                newtype_wrapper: None,
                original_type: None,
                map_is_ahash: false,
                map_key_is_cow: false,
                vec_inner_is_ref: false,
                    map_is_btree: false,
                    core_wrapper: crate::core::ir::CoreWrapper::None,
            }],
            return_type: TypeRef::Unit,
            is_async: false,
            is_static: false,
            error_type: None,
            doc: String::new(),
            receiver: Some(crate::core::ir::ReceiverKind::Ref),
            sanitized: false,
            trait_source: None,
            returns_ref: false,
            returns_cow: false,
            return_newtype_wrapper: None,
            has_default_impl: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
        };
        let sig = c_trampoline_signature("goIngesterProcess", &method);
        assert!(
            sig.contains("size_t payload_len"),
            "C preamble sig must include `size_t payload_len`;\nactual:\n{sig}"
        );
    }

    #[test]
    fn trait_bridge_vtable_includes_free_string_callback() {
        let trait_def = TypeDef {
            name: "OcrBackend".to_string(),
            rust_path: "sample_crate::OcrBackend".to_string(),
            original_rust_path: String::new(),
            fields: vec![],
            methods: vec![],
            is_opaque: false,
            is_clone: false,
            is_copy: false,
            is_trait: true,
            has_default: false,
            has_stripped_cfg_fields: false,
            is_return_type: false,
            serde_rename_all: None,
            has_serde: false,
            super_traits: vec![],
            doc: String::new(),
            cfg: None,
            binding_excluded: false,
            binding_exclusion_reason: None,
            is_variant_wrapper: false,
            has_lifetime_params: false,
        };
        let bridge_cfg = TraitBridgeConfig {
            trait_name: "OcrBackend".to_string(),
            super_trait: Some("Plugin".to_string()),
            registry_getter: None,
            register_fn: Some("register_ocr_backend".to_string()),
            unregister_fn: None,
            clear_fn: None,
            type_alias: None,
            param_name: None,
            register_extra_args: None,
            exclude_languages: Vec::new(),
            bind_via: crate::core::config::BridgeBinding::FunctionParam,
            options_type: None,
            options_field: None,
            context_type: None,
            result_type: None,
            ffi_skip_methods: Vec::new(),
        };
        let mut out = String::new();
        let excluded = HashSet::new();

        gen_trait_bridge(
            &mut out,
            &trait_def,
            &bridge_cfg,
            "sample_crate",
            "sample_crate",
            &excluded,
            "ocr_backend",
        );

        assert!(out.contains("free_string: (*[0]byte)(unsafe.Pointer(C.goOcrBackendFreeString))"));
        assert!(out.contains("func goOcrBackendFreeString(ptr *C.char)"));
        assert!(out.contains("C.free(unsafe.Pointer(ptr))"));
    }

    #[test]
    fn substitute_excluded_types_passes_through_primitives_and_other_atoms() {
        let excluded: HashSet<&str> = HashSet::new();
        assert!(matches!(
            substitute_excluded_types(&TypeRef::String, &excluded),
            TypeRef::String
        ));
        assert!(matches!(
            substitute_excluded_types(&TypeRef::Bytes, &excluded),
            TypeRef::Bytes
        ));
        assert!(matches!(
            substitute_excluded_types(&TypeRef::Unit, &excluded),
            TypeRef::Unit
        ));
    }
}
