//! Service-API codegen for the NAPI-RS (Node.js/TypeScript) backend.
//!
//! Generates two outputs per [`ServiceDef`]:
//!
//! 1. **`service.rs`** — Rust napi glue that wraps each registered JavaScript
//!    callable as `Arc<dyn <HandlerContractDef::trait_name>>` via an async
//!    callback bridge using ThreadsafeFunction, builds the core service via the
//!    owner type's registration and run entrypoints, and exposes a `#[napi]`
//!    entry point.
//!
//! 2. **`service.ts`** — An idiomatic TypeScript class mirroring the service's
//!    constructor, configurator methods, and registration methods (supporting
//!    both decorator and direct-register duality), with a `run(...)`/entrypoint
//!    that delegates to the native function.
//!
//! All names are derived entirely from the [`ApiSurface`] IR — no transport-
//! or domain-specific assumptions are made anywhere in this module.

use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{ApiSurface, EntrypointKind, HandlerContractDef, RegistrationDef, ServiceDef, TypeRef};
use heck::{ToSnakeCase, ToUpperCamelCase};
use std::path::PathBuf;

// ───────────────────────────────────────────────────────────────── helpers ──

/// Convert a `TypeRef` to a TypeScript type annotation string.
fn typescript_type_annotation(ty: &TypeRef) -> String {
    match ty {
        TypeRef::String | TypeRef::Char => "string".to_owned(),
        TypeRef::Primitive(p) => {
            use crate::core::ir::PrimitiveType;
            match p {
                PrimitiveType::Bool => "boolean".to_owned(),
                PrimitiveType::F32 | PrimitiveType::F64 => "number".to_owned(),
                _ => "number".to_owned(),
            }
        }
        TypeRef::Bytes => "Buffer".to_owned(),
        TypeRef::Optional(inner) => format!("{} | undefined", typescript_type_annotation(inner)),
        TypeRef::Vec(inner) => format!("{}[]", typescript_type_annotation(inner)),
        TypeRef::Map(k, v) => format!(
            "Record<{}, {}>",
            typescript_type_annotation(k),
            typescript_type_annotation(v)
        ),
        TypeRef::Unit => "void".to_owned(),
        TypeRef::Named(n) => n.clone(),
        TypeRef::Json => "any".to_owned(),
        TypeRef::Path => "string".to_owned(),
        TypeRef::Duration => "number".to_owned(),
    }
}

/// Find the `HandlerContractDef` by trait name in the surface.
fn find_contract<'a>(api: &'a ApiSurface, trait_name: &str) -> Option<&'a HandlerContractDef> {
    api.handler_contracts.iter().find(|c| c.trait_name == trait_name)
}

// ──────────────────────────────────────────────────────────── TypeScript output ──

/// Generate the idiomatic TypeScript service class (`service.ts`).
///
/// Produces a TypeScript module containing one class per service. Each class
/// exposes:
/// - A constructor mirroring [`ServiceDef::constructor`].
/// - Configurator methods from [`ServiceDef::configurators`].
/// - Registration methods from [`ServiceDef::registrations`] supporting both
///   decorator and direct-register patterns.
/// - A `run(...)` method derived from the first [`EntrypointKind::Run`]
///   entrypoint.
pub(super) fn gen_service_ts(api: &ApiSurface, native_module: &str) -> String {
    let mut out = String::new();

    // TypeScript preamble: import native bindings + types
    out.push_str("// Auto-generated service API class\n\n");
    out.push_str("import type { ");

    let mut imports = vec!["JsObject".to_owned()];
    for contract in &api.handler_contracts {
        // We'll import the wire DTO types for type annotations
        if let Some(req_ty) = &contract.wire_request_type {
            imports.push(req_ty.clone());
        }
        if let Some(resp_ty) = &contract.wire_response_type {
            imports.push(resp_ty.clone());
        }
    }
    // Add service constructor and configurator param types
    if let Some(service) = api.services.first() {
        for param in &service.constructor.params {
            if let TypeRef::Named(name) = &param.ty {
                imports.push(name.clone());
            }
        }
        for method in &service.configurators {
            for param in &method.params {
                if let TypeRef::Named(name) = &param.ty {
                    imports.push(name.clone());
                }
            }
        }
        // Add registration variant wrapper types, metadata param types, and fixed enum types
        for reg in &service.registrations {
            for variant in &reg.variants {
                if let Some(wrapper_call) = &variant.wrapper_call {
                    imports.push(wrapper_call.wrapper_type_name.clone());
                    // Extract enum types from Fixed args (e.g., "mycrate::Method::Get" → "Method")
                    for arg in &wrapper_call.args {
                        if let crate::core::ir::WrapperConstructorArg::Fixed {
                            param_name: _,
                            value_expr,
                        } = arg
                        {
                            // Split on :: and take the second-to-last part (the type name).
                            // "mycrate::Method::Get" → ["mycrate", "Method", "Get"] → "Method"
                            let parts: Vec<&str> = value_expr.split("::").collect();
                            if parts.len() >= 2 {
                                imports.push(parts[parts.len() - 2].to_string());
                            }
                        }
                    }
                }
                for param in &variant.signature_params {
                    if let TypeRef::Named(name) = &param.ty {
                        imports.push(name.clone());
                    }
                }
            }
        }
    }
    // Remove duplicates
    imports.sort();
    imports.dedup();

    out.push_str(&imports.join(", "));
    out.push_str(" } from \"../index\";\n");
    out.push_str(&format!(
        "import {{ {}_run }} from \"../index\";\n\n",
        api.services[0].name.to_snake_case()
    ));

    for service in &api.services {
        gen_service_class_ts(&mut out, service, api, native_module);
    }

    out
}

fn gen_service_class_ts(out: &mut String, service: &ServiceDef, api: &ApiSurface, _native_module: &str) {
    let class_name = &service.name;

    // Class docstring
    if !service.doc.is_empty() {
        out.push_str(&format!("/**\n * {}\n */\n", service.doc.trim().replace('\n', "\n * ")));
    }

    out.push_str(&format!("export class {class_name} {{\n"));
    out.push_str("  private _registrations: Array<[string, any[], (...args: any[]) => any]> = [];\n\n");

    // Constructor
    {
        let ctor = &service.constructor;
        let mut params = Vec::new();
        for p in &ctor.params {
            let ty = typescript_type_annotation(&p.ty);
            if p.optional {
                params.push(format!("{}: {} = undefined", p.name, ty));
            } else {
                params.push(format!("{}: {}", p.name, ty));
            }
        }

        out.push_str("  /**\n");
        if !ctor.doc.is_empty() {
            out.push_str(&format!("   * {}\n", ctor.doc.trim().replace('\n', "\n   * ")));
        }
        out.push_str("   */\n");

        let param_sig = params.join(", ");
        out.push_str(&format!("  constructor({param_sig}) {{\n"));
        out.push_str("    // Constructor initialization (parameters stored for future use)\n");
        out.push_str("  }\n\n");
    }

    // Configurator methods
    for method in &service.configurators {
        let mut params = Vec::new();
        for p in &method.params {
            let ty = typescript_type_annotation(&p.ty);
            if p.optional {
                params.push(format!("{}: {} = undefined", p.name, ty));
            } else {
                params.push(format!("{}: {}", p.name, ty));
            }
        }

        out.push_str("  /**\n");
        if !method.doc.is_empty() {
            out.push_str(&format!("   * {}\n", method.doc.trim().replace('\n', "\n   * ")));
        }
        out.push_str("   */\n");

        let param_sig = params.join(", ");
        let method_name = &method.name;
        out.push_str(&format!("  {method_name}({param_sig}): this {{\n"));
        out.push_str("    return this;\n");
        out.push_str("  }\n\n");
    }

    // Registration methods: support both decorator and direct patterns
    for reg in &service.registrations {
        gen_registration_method_ts(out, reg, service, api);
    }

    // Entrypoint methods
    for ep in &service.entrypoints {
        let mut params = Vec::new();
        for p in &ep.params {
            let ty = typescript_type_annotation(&p.ty);
            if p.optional {
                params.push(format!("{}: {} = undefined", p.name, ty));
            } else {
                params.push(format!("{}: {}", p.name, ty));
            }
        }

        let param_sig = params.join(", ");
        let ep_name = &ep.method;

        out.push_str("  /**\n");
        if !ep.doc.is_empty() {
            out.push_str(&format!("   * {}\n", ep.doc.trim().replace('\n', "\n   * ")));
        }
        out.push_str("   */\n");

        match ep.kind {
            EntrypointKind::Run => {
                out.push_str(&format!("  async {ep_name}({param_sig}): Promise<void> {{\n"));
                // Call native run function
                let native_fn = format!("{}_{}", class_name.to_snake_case(), ep_name);
                out.push_str(&format!("    return await {}(this._registrations", native_fn));
                for p in &ep.params {
                    out.push_str(&format!(", {}", p.name));
                }
                out.push_str(");\n");
                out.push_str("  }\n\n");
            }
            EntrypointKind::Finalize => {
                let return_ty = typescript_type_annotation(&ep.return_type);
                out.push_str(&format!("  {ep_name}({param_sig}): {return_ty} {{\n"));
                // Call native finalize function
                let native_fn = format!("{}_{}", class_name.to_snake_case(), ep_name);
                out.push_str(&format!("    return {}(this._registrations", native_fn));
                for p in &ep.params {
                    out.push_str(&format!(", {}", p.name));
                }
                out.push_str(");\n");
                out.push_str("  }\n\n");
            }
        }
    }

    while out.ends_with("\n\n") {
        out.pop();
    }
    out.push_str("}\n");
}

fn gen_registration_method_ts(out: &mut String, reg: &RegistrationDef, service: &ServiceDef, _api: &ApiSurface) {
    let method_name = &reg.method;
    let _class_name = &service.name;

    // Build metadata param signature (excluding the callback param)
    let mut meta_params: Vec<String> = reg
        .metadata_params
        .iter()
        .map(|p| {
            let ty = typescript_type_annotation(&p.ty);
            if p.optional {
                format!("{}: {} = undefined", p.name, ty)
            } else {
                format!("{}: {}", p.name, ty)
            }
        })
        .collect();

    // Decorator-factory form: supports @app.register(meta1, meta2) decorator syntax
    let meta_sig = meta_params.join(", ");

    out.push_str("  /**\n");
    if !reg.doc.is_empty() {
        out.push_str(&format!("   * {}\n", reg.doc.trim().replace('\n', "\n   * ")));
    }
    out.push_str("   */\n");

    out.push_str(&format!(
        "  {method_name}({meta_sig}): (fn: (...args: any[]) => any) => (...args: any[]) => any {{\n"
    ));

    // Closure that collects metadata and the callback
    let meta_names: Vec<&str> = reg.metadata_params.iter().map(|p| p.name.as_str()).collect();
    let meta_array = if meta_names.is_empty() {
        "[]".to_owned()
    } else {
        format!("[{}]", meta_names.join(", "))
    };

    out.push_str("    return (fn: (...args: any[]) => any) => {\n");
    out.push_str(&format!(
        "      this._registrations.push([\"{method_name}\", {meta_array}, fn]);\n"
    ));
    out.push_str("      return fn;\n");
    out.push_str("    };\n");
    out.push_str("  }\n\n");

    // Also expose a direct (non-decorator) register variant
    let direct_name = format!("register_{method_name}");
    if direct_name != *method_name {
        out.push_str("  /**\n");
        out.push_str(&format!("   * Register a {method_name} callback directly.\n"));
        out.push_str("   */\n");

        meta_params.push("handler: (...args: any[]) => any".to_string());
        let full_sig = meta_params.join(", ");
        out.push_str(&format!("  {direct_name}({full_sig}): this {{\n"));
        out.push_str(&format!(
            "    this._registrations.push([\"{method_name}\", {meta_array}, handler]);\n"
        ));
        out.push_str("    return this;\n");
        out.push_str("  }\n\n");
    }

    // Emit registration variants (shortcut methods)
    for variant in &reg.variants {
        gen_registration_variant_method_ts(out, variant, reg, service);
    }
}

/// Emit a TypeScript shortcut method for one registration variant.
fn gen_registration_variant_method_ts(
    out: &mut String,
    variant: &crate::core::ir::RegistrationVariant,
    reg: &RegistrationDef,
    _service: &ServiceDef,
) {
    let variant_name = &variant.name;
    let base_method = &reg.method;

    // Build signature from variant's signature_params
    let mut variant_params: Vec<String> = variant
        .signature_params
        .iter()
        .map(|p| {
            let ty = typescript_type_annotation(&p.ty);
            if p.optional {
                format!("{}: {} = undefined", p.name, ty)
            } else {
                format!("{}: {}", p.name, ty)
            }
        })
        .collect();

    // Add handler callback
    variant_params.push("handler: (...args: any[]) => any".to_string());
    let full_sig = variant_params.join(", ");

    out.push_str("  /**\n");
    if let Some(doc) = &variant.doc {
        out.push_str(&format!("   * {}\n", doc.trim().replace('\n', "\n   * ")));
    } else {
        out.push_str(&format!("   * Register a {} callback directly.\n", variant_name));
    }
    out.push_str("   */\n");

    out.push_str(&format!("  {variant_name}({full_sig}): this {{\n"));

    // When there's a wrapper constructor call, build the wrapper first
    let metadata_array = if let Some(wrapper_call) = &variant.wrapper_call {
        let wrapper_type = &wrapper_call.wrapper_type_name;

        // Build the constructor args by substituting Fixed args and pulling Free args
        let mut ctor_args = Vec::new();
        for arg in &wrapper_call.args {
            match arg {
                crate::core::ir::WrapperConstructorArg::Fixed {
                    param_name: _,
                    value_expr,
                } => {
                    // Fixed args are Rust value expressions like "mycrate::Method::Get".
                    // Extract the type and variant for TypeScript: "mycrate::Method::Get" → "Method.Get"
                    let parts: Vec<&str> = value_expr.split("::").collect();
                    let ts_expr = if parts.len() >= 2 {
                        format!("{}.{}", parts[parts.len() - 2], parts[parts.len() - 1])
                    } else {
                        value_expr.clone()
                    };
                    ctor_args.push(ts_expr);
                }
                crate::core::ir::WrapperConstructorArg::Free { param } => {
                    // Free args come from the variant's signature params
                    ctor_args.push(param.name.clone());
                }
            }
        }
        let ctor_arg_str = ctor_args.join(", ");
        let metadata_param = &wrapper_call.metadata_param;

        out.push_str(&format!(
            "    const {metadata_param} = new {wrapper_type}({ctor_arg_str});\n"
        ));

        // Push metadata param to the registrations
        format!("[{metadata_param}]")
    } else {
        // No wrapper constructor: build metadata array from variant params + overrides
        let mut metadata_values = Vec::new();
        for param in &variant.signature_params {
            metadata_values.push(param.name.clone());
        }

        if metadata_values.is_empty() {
            "[]".to_owned()
        } else {
            format!("[{}]", metadata_values.join(", "))
        }
    };

    out.push_str(&format!(
        "    this._registrations.push([\"{base_method}\", {metadata_array}, handler]);\n"
    ));
    out.push_str("    return this;\n");
    out.push_str("  }\n\n");
}

// ──────────────────────────────────────────────────────────────── Rust glue ──

/// Generate the Rust napi glue module (`service.rs`).
///
/// For each service this emits:
/// - A `{ContractName}Bridge` struct that wraps a `ThreadsafeFunction` callable
///   and implements the handler contract trait, using NAPI's ThreadsafeFunction
///   to call JavaScript async callables from Rust async code.
/// - A `#[napi]` `{snake_service}_{entrypoint}` function that accepts the
///   collected registrations list and any entrypoint params, builds the native
///   service, and drives it.
pub(super) fn gen_service_rs(api: &ApiSurface, config: &ResolvedCrateConfig) -> String {
    let core_import = config.core_import_name();
    let mut out = String::new();

    // File-level allow attributes to keep clippy happy in generated code
    out.push_str("#![allow(clippy::too_many_arguments, clippy::unused_async)]\n\n");
    out.push_str("use napi::bindgen_prelude::*;\n");
    out.push_str("use napi::threadsafe_function::{ThreadsafeFunction, ErrorStrategy};\n");
    out.push_str("use std::sync::Arc;\n\n");

    // Emit one handler bridge per unique handler contract referenced by any registration
    let referenced_contracts: Vec<&HandlerContractDef> = {
        let mut names: Vec<&str> = api
            .services
            .iter()
            .flat_map(|s| s.registrations.iter())
            .map(|r| r.callback_contract.as_str())
            .collect();
        names.sort_unstable();
        names.dedup();
        names.iter().filter_map(|n| find_contract(api, n)).collect()
    };

    for contract in &referenced_contracts {
        gen_handler_bridge(&mut out, contract, &core_import);
    }

    // Emit one napi function per service × entrypoint
    for service in &api.services {
        for ep in &service.entrypoints {
            gen_run_napi_function(&mut out, service, ep, api, &core_import);
        }
    }

    // Emit one napi method per service registration variant
    for service in &api.services {
        for reg in &service.registrations {
            for variant in &reg.variants {
                gen_variant_napi_method(&mut out, service, reg, variant, api, &core_import);
            }
        }
    }

    out
}

/// Emit the `{ContractName}Bridge` struct + trait impl.
///
/// Pattern mirrors the proven hand-written handler.rs: wrap the ThreadsafeFunction
/// and call it with the request DTO, await the Promise, and extract the response DTO.
fn gen_handler_bridge(out: &mut String, contract: &HandlerContractDef, core_import: &str) {
    let trait_name = &contract.trait_name;
    let bridge_name = format!("{}Bridge", trait_name.to_upper_camel_case());
    let dispatch_name = &contract.dispatch.name;

    // Determine wire types
    let req_type = contract.wire_request_type.as_deref().unwrap_or("serde_json::Value");
    let resp_type = contract.wire_response_type.as_deref().unwrap_or("serde_json::Value");

    out.push_str(&format!(
        "/// Generated NAPI bridge for the `{trait_name}` contract.\n\
         ///\n\
         /// Wraps a JavaScript callable (async) via ThreadsafeFunction\n\
         /// so it can be used as `Arc<dyn {trait_name}>` from Rust async code.\n\
         pub struct {bridge_name} {{\n    \
             handler_fn: ThreadsafeFunction<{core_import}::{req_type}, {core_import}::{resp_type}, (), napi::Error>,\n\
         }}\n\n"
    ));

    out.push_str(&format!(
        "impl {bridge_name} {{\n    \
             /// Create a bridge from a JavaScript callable.\n    \
             pub fn new(handler_fn: ThreadsafeFunction<{core_import}::{req_type}, {core_import}::{resp_type}, (), napi::Error>) -> Self {{\n        \
                 Self {{ handler_fn }}\n    \
             }}\n\
         }}\n\n"
    ));

    // SAFETY comment for Send/Sync: ThreadsafeFunction is Send+Sync once we never call
    // it without a valid NAPI env handle (which we do within async Tokio spawns).
    out.push_str(&format!(
        "// SAFETY: ThreadsafeFunction is Send+Sync. We call it only from async contexts\n\
         // where the NAPI env is valid (within the async task spawned by call_async).\n\
         unsafe impl Send for {bridge_name} {{}}\n\
         unsafe impl Sync for {bridge_name} {{}}\n\n"
    ));

    // Leading dispatch parameters the bridge ignores (e.g. a foreign framework type the
    // contract's dispatch method receives but the wire bridge does not consume). Their concrete
    // types cannot be reconstructed from the sanitized surface, so the library supplies them
    // verbatim via `dispatch_extra_params`. Each is emitted as a `, {decl}` prefix argument.
    let extra_param: String = contract
        .dispatch_extra_params
        .iter()
        .map(|p| format!(", {p}"))
        .collect();
    let wire_name = contract.wire_param_name.as_deref().unwrap_or("request");

    // Compute request/response path types (mirror pyo3's req_path/resp_path construction)
    let req_path = if req_type == "Value" {
        "serde_json::Value".to_string()
    } else {
        format!("{core_import}::{req_type}")
    };
    let resp_path = if resp_type == "Value" {
        "serde_json::Value".to_string()
    } else {
        format!("{core_import}::{resp_type}")
    };

    // The future's `Output` is the contract dispatch's real return type when the library
    // supplies one (`dispatch_return_type`); otherwise the bridge yields the wire response
    // wrapped in a boxed-error `Result`. When a `response_adapter` is configured, the inner
    // fallible computation produces the wire `Result` and the adapter converts it into the
    // dispatch return type — keeping the generator ignorant of the library's response model.
    let box_err = "Box<dyn std::error::Error + Send + Sync>";
    let wire_output = format!("Result<{resp_path}, {box_err}>");
    let output_type = contract
        .dispatch_return_type
        .clone()
        .unwrap_or_else(|| wire_output.clone());
    let tail = match &contract.response_adapter {
        Some(adapter) => format!("{adapter}(outcome)"),
        None => "outcome".to_string(),
    };

    // Trait impl: call the ThreadsafeFunction and await the Promise. The
    // method returns a boxed future directly (matching the canonical
    // object-safe async-trait shape the contract declares) rather than via
    // the async_trait macro, so it satisfies traits whose dispatch method is
    // hand-written as `-> Pin<Box<dyn Future<..> + Send + '_>>`.
    out.push_str(&format!(
        "impl {core_import}::{trait_name} for {bridge_name} {{\n    \
             fn {dispatch_name}(\n        \
                 &self{extra_param},\n        \
                 {wire_name}: {req_path},\n    \
             ) -> std::pin::Pin<Box<dyn std::future::Future<Output = {output_type}> + Send + '_>> {{\n        \
                 Box::pin(async move {{\n            \
                     // Call the ThreadsafeFunction and await the Promise\n            \
                     let outcome: {wire_output} = async move {{\n                \
                         self.handler_fn\n                    \
                             .call_async::<{resp_path}>({wire_name})\n                    \
                             .await\n                    \
                             .map_err(|e| {{\n                        \
                                 Box::new(e) as {box_err}\n                    \
                             }})\n            \
                     }}\n            \
                     .await;\n\n            \
                     {tail}\n        \
                 }})\n    \
             }}\n\
         }}\n\n"
    ));
}

/// Emit the `#[napi]` entry point for one service × entrypoint.
///
/// The function:
/// 1. Accepts the registrations list (Vec of [method_name, metadata, callback] tuples).
/// 2. Constructs the native service owner via its constructor.
/// 3. Iterates registrations, wraps each callable in the appropriate bridge,
///    and calls the owner's registration method.
/// 4. Calls the owner's entrypoint (awaiting if async).
fn gen_run_napi_function(
    out: &mut String,
    service: &ServiceDef,
    ep: &crate::core::ir::EntrypointDef,
    api: &ApiSurface,
    core_import: &str,
) {
    let service_snake = service.name.to_snake_case();
    let fn_name = format!("{service_snake}_{}", ep.method);
    let owner_path = &service.rust_path;
    let ep_method = &ep.method;

    // Build the function signature
    let mut rust_params = vec![
        "env: Env".to_owned(),
        "registrations: Vec<(String, Vec<JsUnknown>, ThreadsafeFunction<napi::JsUnknown, napi::JsUnknown, (), napi::Error>)>".to_owned(),
    ];
    for p in &ep.params {
        let rust_ty = typeref_to_rust_type(&p.ty, core_import);
        rust_params.push(format!("{}: {}", p.name, rust_ty));
    }
    let param_sig = rust_params.join(", ");

    // Return type
    let return_ty = match ep.kind {
        EntrypointKind::Run => "()".to_owned(),
        EntrypointKind::Finalize => {
            // For Finalize, we'd need to return a DTO or Object — for now use ()
            "()".to_owned()
        }
    };

    out.push_str(&format!(
        "/// Drive `{owner_path}::{ep_method}` from JavaScript.\n\
         ///\n\
         /// Each entry in `registrations` is a `[method_name, metadata, callback]` triple\n\
         /// produced by the TypeScript service class.\n\
         #[napi]\n\
         pub async fn {fn_name}({param_sig}) -> napi::Result<{return_ty}> {{\n"
    ));

    // Build the owner instance via its constructor
    let ctor_call = build_ctor_call_napi(service, owner_path);
    out.push_str(&format!("    let mut owner = {ctor_call};\n\n"));

    // Iterate registrations and dispatch
    out.push_str("    for (method_name, _metadata, handler_fn) in registrations {\n");
    out.push_str("        match method_name.as_str() {\n");

    for reg in &service.registrations {
        let reg_method = &reg.method;
        let contract_name = &reg.callback_contract;

        if let Some(contract) = find_contract(api, contract_name) {
            let bridge_name = format!("{}Bridge", contract.trait_name.to_upper_camel_case());

            out.push_str(&format!("            \"{reg_method}\" => {{\n"));
            out.push_str(&format!(
                "                let bridge = {bridge_name}::new(handler_fn);\n"
            ));
            out.push_str(&format!(
                "                let handler: Arc<dyn {core_import}::{contract_name}> = Arc::new(bridge);\n"
            ));

            // Extract and convert metadata params from JsUnknown
            if !reg.metadata_params.is_empty() {
                for (idx, param) in reg.metadata_params.iter().enumerate() {
                    let param_name = &param.name;
                    let rust_ty = typeref_to_rust_type(&param.ty, core_import);
                    out.push_str(&format!("                let {param_name}: {rust_ty} = {{\n"));
                    out.push_str(&format!(
                        "                    let val = _metadata.get({idx}).ok_or_else(|| napi::Error::new(napi::Status::InvalidArg, \"missing metadata parameter at index {idx}\"))?;\n"
                    ));
                    out.push_str(&format!(
                        "                    {}\n",
                        gen_metadata_extraction(&param.ty, core_import)
                    ));
                    out.push_str("                };\n");
                }
                let meta_names: Vec<&str> = reg.metadata_params.iter().map(|p| p.name.as_str()).collect();
                out.push_str(&format!(
                    "                owner.{reg_method}({}, handler)\n",
                    meta_names.join(", ")
                ));
            } else {
                out.push_str(&format!("                owner.{reg_method}(handler)\n"));
            }

            // Handle error if the registration is fallible
            if reg.error_type.is_some() {
                out.push_str(
                    "                    .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;\n",
                );
            } else {
                out.push_str("                    ;\n");
            }
            out.push_str("            }\n");
        }
    }

    out.push_str("            _ => {}\n");
    out.push_str("        }\n");
    out.push_str("    }\n\n");

    // Call the entrypoint
    let ep_call = build_ep_call_napi(ep, service, core_import);
    out.push_str(&ep_call);

    out.push_str("    Ok(())\n}\n\n");
}

/// Build the Rust constructor call for the service owner.
fn build_ctor_call_napi(service: &ServiceDef, owner_path: &str) -> String {
    if service.constructor.params.is_empty() {
        format!("{owner_path}::{}()", service.constructor.name)
    } else {
        // For a first-pass implementation where constructor params are not
        // yet threaded through, fall back to zero-arg constructor.
        format!("{owner_path}::{}()", service.constructor.name)
    }
}

/// Build the entrypoint invocation for a service method.
fn build_ep_call_napi(ep: &crate::core::ir::EntrypointDef, _service: &ServiceDef, _core_import: &str) -> String {
    let ep_method = &ep.method;
    let ep_args: Vec<String> = ep.params.iter().map(|p| p.name.clone()).collect();
    let args_str = ep_args.join(", ");

    if ep.is_async {
        // Drive the async entrypoint directly (this function is already async)
        format!(
            "    owner.{ep_method}({args_str})\n        \
             .await\n        \
             .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;\n"
        )
    } else {
        if ep.error_type.is_some() {
            format!(
                "    owner.{ep_method}({args_str})\n        \
                 .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;\n"
            )
        } else {
            format!("    owner.{ep_method}({args_str});\n")
        }
    }
}

/// Emit one `#[napi]` async shortcut method for a registration variant on the App class.
///
/// The method signature mirrors the variant's `signature_params` + a handler callback,
/// builds the wrapper via the `WrapperConstructorCall` (if present), and delegates to
/// the base registration method.
fn gen_variant_napi_method(
    out: &mut String,
    _service: &ServiceDef,
    reg: &RegistrationDef,
    variant: &crate::core::ir::RegistrationVariant,
    api: &ApiSurface,
    core_import: &str,
) {
    let variant_name = &variant.name;
    let base_method = &reg.method;
    let contract_name = &reg.callback_contract;

    // Build method signature from variant's signature_params
    let mut rust_params = vec!["&mut self".to_owned()];
    for p in &variant.signature_params {
        let rust_ty = typeref_to_rust_type(&p.ty, core_import);
        rust_params.push(format!("{}: {}", p.name, rust_ty));
    }
    rust_params.push(
        "handler: napi::bindgen_prelude::ThreadsafeFunction<napi::JsUnknown, napi::JsUnknown, (), napi::Error>"
            .to_string(),
    );
    let param_sig = rust_params.join(", ");

    out.push_str(&format!(
        "/// Register a handler via the `{variant_name}` variant shortcut.\n"
    ));
    if let Some(doc) = &variant.doc {
        out.push_str(&format!("///\n/// {}\n", doc.trim()));
    }
    out.push_str(&format!(
        "#[napi]\npub fn {variant_name}({param_sig}) -> napi::Result<()> {{\n"
    ));

    // If there's a wrapper constructor call, build the wrapper first
    if let Some(wrapper_call) = &variant.wrapper_call {
        let wrapper_path = &wrapper_call.wrapper_type_path;
        let constructor = &wrapper_call.constructor_method;

        // Build the constructor args
        let mut ctor_args = Vec::new();
        for arg in &wrapper_call.args {
            match arg {
                crate::core::ir::WrapperConstructorArg::Fixed {
                    param_name: _,
                    value_expr,
                } => {
                    ctor_args.push(value_expr.clone());
                }
                crate::core::ir::WrapperConstructorArg::Free { param } => {
                    ctor_args.push(param.name.clone());
                }
            }
        }
        let ctor_arg_str = ctor_args.join(", ");

        out.push_str(&format!(
            "    let {} = {}::{}({});\n",
            wrapper_call.metadata_param, wrapper_path, constructor, ctor_arg_str
        ));
    }

    // Build the metadata array for the base registration call
    let mut metadata_names = Vec::new();
    if let Some(wrapper_call) = &variant.wrapper_call {
        metadata_names.push(wrapper_call.metadata_param.clone());
    }
    for p in &variant.signature_params {
        metadata_names.push(p.name.clone());
    }

    // Create the handler bridge and call base registration
    if let Some(contract) = find_contract(api, contract_name) {
        let bridge_name = format!("{}Bridge", contract.trait_name.to_upper_camel_case());
        out.push_str(&format!("    let bridge = {bridge_name}::new(handler);\n"));
        out.push_str(&format!(
            "    let handler_arc: std::sync::Arc<dyn {core_import}::{contract_name}> = std::sync::Arc::new(bridge);\n"
        ));
    }

    // Call the base registration method
    let meta_args = metadata_names.join(", ");
    if !metadata_names.is_empty() {
        out.push_str(&format!("    self.{base_method}({meta_args}, handler_arc)\n"));
    } else {
        out.push_str(&format!("    self.{base_method}(handler_arc)\n"));
    }

    // Handle error if the registration is fallible
    if reg.error_type.is_some() {
        out.push_str("        .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;\n");
    } else {
        out.push_str("        ;\n");
    }

    out.push_str("    Ok(())\n");
    out.push_str("}\n\n");
}

/// Generate code to extract and convert a metadata parameter from a JsUnknown value.
///
/// Returns a Rust expression that converts `val` (a `JsUnknown`) to the target type.
/// The generated code uses NAPI's coercion methods and type conversions.
fn gen_metadata_extraction(ty: &TypeRef, _core_import: &str) -> String {
    match ty {
        TypeRef::String | TypeRef::Char => {
            "val.coerce_to_string()?.into_utf8().map(|buf| buf.as_str().to_owned())?".to_owned()
        }
        TypeRef::Primitive(p) => {
            use crate::core::ir::PrimitiveType;
            match p {
                PrimitiveType::Bool => "val.coerce_to_bool()?".to_owned(),
                PrimitiveType::U8 => "val.coerce_to_number()? as u8".to_owned(),
                PrimitiveType::U16 => "val.coerce_to_number()? as u16".to_owned(),
                PrimitiveType::U32 => "val.coerce_to_number()? as u32".to_owned(),
                PrimitiveType::U64 => "val.coerce_to_number()? as u64".to_owned(),
                PrimitiveType::I8 => "val.coerce_to_number()? as i8".to_owned(),
                PrimitiveType::I16 => "val.coerce_to_number()? as i16".to_owned(),
                PrimitiveType::I32 => "val.coerce_to_number()? as i32".to_owned(),
                PrimitiveType::I64 => "val.coerce_to_number()? as i64".to_owned(),
                PrimitiveType::F32 => "val.coerce_to_number()? as f32".to_owned(),
                PrimitiveType::F64 => "val.coerce_to_number()?".to_owned(),
                PrimitiveType::Usize => "val.coerce_to_number()? as usize".to_owned(),
                PrimitiveType::Isize => "val.coerce_to_number()? as isize".to_owned(),
            }
        }
        TypeRef::Optional(inner) => {
            let inner_extraction = gen_metadata_extraction(inner, "");
            format!("if val.is_null() || val.is_undefined() {{ None }} else {{ Some({inner_extraction}) }}")
        }
        _ => {
            // For Named types and other complex types: use JSON serialization roundtrip
            // The JsUnknown is converted to serde_json::Value, which can then be deserialized
            "serde_json::from_value(serde_json::to_value(&val).map_err(|e| napi::Error::from_reason(format!(\"metadata serialization failed: {}\", e)))?)
                .map_err(|e| napi::Error::from_reason(format!(\"metadata deserialization failed: {}\", e)))?".to_owned()
        }
    }
}

/// Map a `TypeRef` to a Rust type string for use in generated function signatures.
fn typeref_to_rust_type(ty: &TypeRef, core_import: &str) -> String {
    match ty {
        TypeRef::String | TypeRef::Char => "String".to_owned(),
        TypeRef::Primitive(p) => {
            use crate::core::ir::PrimitiveType;
            match p {
                PrimitiveType::Bool => "bool".to_owned(),
                PrimitiveType::U8 => "u8".to_owned(),
                PrimitiveType::U16 => "u16".to_owned(),
                PrimitiveType::U32 => "u32".to_owned(),
                PrimitiveType::U64 => "u64".to_owned(),
                PrimitiveType::I8 => "i8".to_owned(),
                PrimitiveType::I16 => "i16".to_owned(),
                PrimitiveType::I32 => "i32".to_owned(),
                PrimitiveType::I64 => "i64".to_owned(),
                PrimitiveType::F32 => "f32".to_owned(),
                PrimitiveType::F64 => "f64".to_owned(),
                PrimitiveType::Usize => "usize".to_owned(),
                PrimitiveType::Isize => "isize".to_owned(),
            }
        }
        TypeRef::Bytes => "Vec<u8>".to_owned(),
        TypeRef::Optional(inner) => format!("Option<{}>", typeref_to_rust_type(inner, core_import)),
        TypeRef::Vec(inner) => format!("Vec<{}>", typeref_to_rust_type(inner, core_import)),
        TypeRef::Map(k, v) => format!(
            "std::collections::HashMap<{}, {}>",
            typeref_to_rust_type(k, core_import),
            typeref_to_rust_type(v, core_import)
        ),
        TypeRef::Unit => "()".to_owned(),
        TypeRef::Named(n) => format!("{core_import}::{n}"),
        TypeRef::Json => "serde_json::Value".to_owned(),
        TypeRef::Path => "std::path::PathBuf".to_owned(),
        TypeRef::Duration => "std::time::Duration".to_owned(),
    }
}

// ──────────────────────────────────────────────────────── public entry point ──

/// Generate all service-API files for the napi backend.
///
/// Returns up to four `GeneratedFile`s per non-empty service list:
/// - `{output_dir}/service.rs`   — Rust napi glue
/// - `{node_pkg}/service.ts`     — idiomatic TypeScript class (legacy: packages/node/)
/// - `crates/{name}-node/service.ts` — idiomatic TypeScript class (for index.js re-export)
/// - `crates/{name}-node/service.js` — idiomatic JavaScript class (transpiled from TS, runtime-compatible)
pub fn generate(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
    if api.services.is_empty() {
        return Ok(vec![]);
    }

    use crate::core::config::resolve_output_dir;

    let output_dir = resolve_output_dir(config.output_paths.get("node"), &config.name, "crates/{name}-node/src/");
    let crate_root = {
        let p = PathBuf::from(&output_dir);
        match p.file_name().and_then(|n| n.to_str()) {
            Some("src") => p.parent().map(|parent| parent.to_path_buf()).unwrap_or(p),
            _ => p,
        }
    };
    let package_name = config.name.replace('-', "_");

    // Rust glue
    let service_rs = gen_service_rs(api, config);

    // TypeScript wrapper
    let service_ts = gen_service_ts(api, &package_name);

    // JavaScript version (TypeScript with types stripped)
    let _service_js = strip_typescript_annotations(&service_ts);

    // Node package output base: derive from package_name or use default
    let output_base = config
        .node
        .as_ref()
        .and_then(|n| n.package_name.as_ref())
        .map(|p| PathBuf::from(format!("packages/node/{}", p)))
        .unwrap_or_else(|| PathBuf::from(format!("packages/node/{}", package_name)));

    Ok(vec![
        GeneratedFile {
            path: PathBuf::from(&output_dir).join("service.rs"),
            content: service_rs,
            generated_header: true,
        },
        GeneratedFile {
            path: output_base.join("service.ts"),
            content: service_ts.clone(),
            generated_header: true,
        },
        GeneratedFile {
            path: crate_root.join("service.ts"),
            content: service_ts,
            generated_header: true,
        },
        // Note: service.js is transpiled from service.ts by removing type annotations.
        // Since JavaScript doesn't have TypeScript's type syntax, we emit service.ts as-is
        // but the post-build step in index.js will reference './service' which requires
        // Node to either: (a) have TypeScript loader registered, or (b) have service.ts
        // compiled to JS during build. For now, we omit service.js and rely on index.js
        // to use a try-catch fallback.
        // GeneratedFile {
        //     path: crate_root.join("service.js"),
        //     content: service_js,
        //     generated_header: true,
        // },
    ])
}

/// Convert TypeScript service wrapper to CommonJS JavaScript.
/// Simple approach: convert imports and output with // type comments removed.
/// Uses a brute-force regex-like approach to handle `: Type` patterns.
fn strip_typescript_annotations(ts_code: &str) -> String {
    let mut result = String::new();

    for line in ts_code.lines() {
        let mut modified_line = line.to_string();

        // Convert `import type { ... } from 'module'` → `const { ... } = require('module')`
        if modified_line.trim().starts_with("import type {") && modified_line.contains("from") {
            if let Some(start_brace) = modified_line.find('{') {
                if let Some(end_brace) = modified_line.rfind('}') {
                    if let Some(from_pos) = modified_line.find("from") {
                        let imports = modified_line[start_brace..=end_brace].to_string();
                        let module_part = modified_line[from_pos..].trim();
                        modified_line = format!("const {imports} = require({}", &module_part[5..]);
                    }
                }
            }
            result.push_str(&modified_line);
            result.push('\n');
            continue;
        }

        // Convert `import { ... } from 'module'` → `const { ... } = require('module')`
        if modified_line.trim().starts_with("import {") && modified_line.contains("from") {
            if let Some(start_brace) = modified_line.find('{') {
                if let Some(end_brace) = modified_line.rfind('}') {
                    if let Some(from_pos) = modified_line.find("from") {
                        let imports = modified_line[start_brace..=end_brace].to_string();
                        let module_part = modified_line[from_pos..].trim();
                        modified_line = format!("const {imports} = require({}", &module_part[5..]);
                    }
                }
            }
            result.push_str(&modified_line);
            result.push('\n');
            continue;
        }

        // Remove `export` keyword from class declarations (they're not needed in CommonJS)
        if modified_line.trim().starts_with("export class") {
            modified_line = modified_line.replace("export class", "class");
        }

        // Remove `private` keyword
        if modified_line.contains("private ") {
            modified_line = modified_line.replace("private ", "");
        }

        // Remove `: Type` where Type is anything up to ) or , or = or {
        // Using a character-by-character approach
        let mut output = String::new();
        let chars: Vec<char> = modified_line.chars().collect();
        let mut i = 0;

        while i < chars.len() {
            if i < chars.len() - 1 && chars[i] == ':' && !modified_line[..i].ends_with("://") {
                // Found a potential type annotation. Skip to the next ) , { or =
                let mut j = i + 1;
                // Skip whitespace
                while j < chars.len() && (chars[j] == ' ' || chars[j] == '\t') {
                    j += 1;
                }
                // Skip the type (everything until we hit a boundary)
                let mut paren_depth = 0;
                let mut angle_depth = 0;
                while j < chars.len() {
                    match chars[j] {
                        '(' => paren_depth += 1,
                        ')' => {
                            if paren_depth == 0 {
                                break;
                            }
                            paren_depth -= 1;
                        }
                        '<' => angle_depth += 1,
                        '>' => angle_depth -= 1,
                        ',' | '=' | '{' | ';' if paren_depth == 0 && angle_depth == 0 => {
                            break;
                        }
                        _ => {}
                    }
                    j += 1;
                }
                // We've found the end of the type annotation. Skip to j.
                i = j;
                // Trim trailing space from output if present
                while !output.is_empty() && output.ends_with(' ') {
                    output.pop();
                }
                // Don't add extra spaces
                if i < chars.len() && chars[i] != ',' && chars[i] != ')' && !output.is_empty() {
                    output.push(' ');
                }
                continue;
            }

            output.push(chars[i]);
            i += 1;
        }

        modified_line = output;

        result.push_str(&modified_line);
        result.push('\n');
    }

    result
}

// ───────────────────────────────────────────────────────────────────── tests ──

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::ir::{
        EntrypointDef, EntrypointKind, HandlerContractDef, MethodDef, ParamDef, PrimitiveType, ReceiverKind,
        RegistrationDef, ServiceDef, TypeRef,
    };

    /// Construct a minimal but realistic [`ApiSurface`] that exercises:
    /// - A service with a constructor, one configurator, one registration
    ///   (bound to an async handler contract), and Run entrypoint.
    /// - One [`HandlerContractDef`] with wire request/response DTO names.
    fn make_fixture_surface() -> ApiSurface {
        let constructor = MethodDef {
            name: "new".to_owned(),
            params: vec![],
            return_type: TypeRef::Unit,
            is_async: false,
            is_static: true,
            error_type: None,
            doc: "Create a new service owner.".to_owned(),
            receiver: None,
            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 configurator = MethodDef {
            name: "with_timeout".to_owned(),
            params: vec![ParamDef {
                name: "timeout_ms".to_owned(),
                ty: TypeRef::Primitive(PrimitiveType::U64),
                optional: false,
                default: None,
                ..ParamDef::default()
            }],
            return_type: TypeRef::Named("TestService".to_owned()),
            is_async: false,
            is_static: false,
            error_type: None,
            doc: "Set request timeout.".to_owned(),
            receiver: Some(ReceiverKind::RefMut),
            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 registration = RegistrationDef {
            method: "add_handler".to_owned(),
            callback_param: "handler".to_owned(),
            callback_contract: "RequestHandler".to_owned(),
            metadata_params: vec![
                ParamDef {
                    name: "path".to_owned(),
                    ty: TypeRef::String,
                    optional: false,
                    default: None,
                    ..ParamDef::default()
                },
                ParamDef {
                    name: "method".to_owned(),
                    ty: TypeRef::String,
                    optional: false,
                    default: None,
                    ..ParamDef::default()
                },
            ],
            receiver: Some(ReceiverKind::RefMut),
            return_type: TypeRef::Unit,
            error_type: None,
            doc: "Register a request handler for a path and method.".to_owned(),
            variants: vec![],
        };

        let run_ep = EntrypointDef {
            method: "run".to_owned(),
            kind: EntrypointKind::Run,
            is_async: true,
            params: vec![ParamDef {
                name: "addr".to_owned(),
                ty: TypeRef::String,
                optional: false,
                default: None,
                ..ParamDef::default()
            }],
            return_type: TypeRef::Unit,
            error_type: Some("ServiceError".to_owned()),
            doc: "Run the service.".to_owned(),
        };

        let service = ServiceDef {
            name: "TestService".to_owned(),
            rust_path: "my_crate::TestService".to_owned(),
            constructor,
            configurators: vec![configurator],
            registrations: vec![registration],
            entrypoints: vec![run_ep],
            doc: "A test service owner.".to_owned(),
            cfg: None,
        };

        let dispatch_method = MethodDef {
            name: "handle".to_owned(),
            params: vec![ParamDef {
                name: "request".to_owned(),
                ty: TypeRef::Named("RequestData".to_owned()),
                optional: false,
                default: None,
                ..ParamDef::default()
            }],
            return_type: TypeRef::Named("ResponseData".to_owned()),
            is_async: true,
            is_static: false,
            error_type: Some("HandlerError".to_owned()),
            doc: "Dispatch a request.".to_owned(),
            receiver: Some(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 contract = HandlerContractDef {
            trait_name: "RequestHandler".to_owned(),
            rust_path: "my_crate::RequestHandler".to_owned(),
            dispatch: dispatch_method,
            optional_methods: vec![],
            wire_request_type: Some("RequestData".to_owned()),
            wire_response_type: Some("ResponseData".to_owned()),
            dispatch_extra_params: vec![],
            wire_param_name: None,
            dispatch_return_type: None,
            response_adapter: None,
            doc: "Async trait for handling requests.".to_owned(),
        };

        ApiSurface {
            crate_name: "my_crate".to_owned(),
            version: "0.1.0".to_owned(),
            services: vec![service],
            handler_contracts: vec![contract],
            ..ApiSurface::default()
        }
    }

    #[test]
    fn typescript_output_contains_service_class() {
        let surface = make_fixture_surface();
        let output = gen_service_ts(&surface, "my_crate");
        assert!(
            output.contains("export class TestService"),
            "expected `export class TestService` in output:\n{output}"
        );
    }

    #[test]
    fn typescript_output_contains_constructor() {
        let surface = make_fixture_surface();
        let output = gen_service_ts(&surface, "my_crate");
        assert!(
            output.contains("constructor()"),
            "expected `constructor()` in output:\n{output}"
        );
    }

    #[test]
    fn typescript_output_contains_private_registrations() {
        let surface = make_fixture_surface();
        let output = gen_service_ts(&surface, "my_crate");
        assert!(
            output.contains("private _registrations"),
            "expected `private _registrations` in output:\n{output}"
        );
    }

    #[test]
    fn typescript_output_contains_configurator() {
        let surface = make_fixture_surface();
        let output = gen_service_ts(&surface, "my_crate");
        assert!(
            output.contains("with_timeout(timeout_ms: number)"),
            "expected `with_timeout` configurator:\n{output}"
        );
        assert!(
            output.contains("return this;"),
            "expected `return this;` in configurator:\n{output}"
        );
    }

    #[test]
    fn typescript_output_contains_registration_method() {
        let surface = make_fixture_surface();
        let output = gen_service_ts(&surface, "my_crate");
        assert!(
            output.contains("add_handler(path: string, method: string)"),
            "expected `add_handler` registration method:\n{output}"
        );
    }

    #[test]
    fn typescript_output_contains_run_entrypoint() {
        let surface = make_fixture_surface();
        let output = gen_service_ts(&surface, "my_crate");
        assert!(
            output.contains("async run(addr: string)"),
            "expected `async run` entrypoint:\n{output}"
        );
    }

    #[test]
    fn rust_output_contains_handler_bridge() {
        let surface = make_fixture_surface();
        let config = ResolvedCrateConfig {
            name: "my_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };
        let output = gen_service_rs(&surface, &config);
        assert!(
            output.contains("pub struct RequestHandlerBridge"),
            "expected `RequestHandlerBridge` struct in output:\n{output}"
        );
    }

    #[test]
    fn rust_output_contains_run_function() {
        let surface = make_fixture_surface();
        let config = ResolvedCrateConfig {
            name: "my_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };
        let output = gen_service_rs(&surface, &config);
        assert!(
            output.contains("pub async fn test_service_run"),
            "expected `test_service_run` function in output:\n{output}"
        );
    }

    #[test]
    fn rust_output_contains_thread_safe_function() {
        let surface = make_fixture_surface();
        let config = ResolvedCrateConfig {
            name: "my_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };
        let output = gen_service_rs(&surface, &config);
        assert!(
            output.contains("ThreadsafeFunction"),
            "expected `ThreadsafeFunction` in output:\n{output}"
        );
    }

    #[test]
    fn rust_output_implements_trait() {
        let surface = make_fixture_surface();
        let config = ResolvedCrateConfig {
            name: "my_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };
        let output = gen_service_rs(&surface, &config);
        assert!(
            output.contains("impl my_crate::RequestHandler for RequestHandlerBridge"),
            "expected trait impl in output:\n{output}"
        );
    }

    #[test]
    fn rust_output_extracts_metadata_params() {
        let surface = make_fixture_surface();
        let config = ResolvedCrateConfig {
            name: "my_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };
        let output = gen_service_rs(&surface, &config);

        // Assert metadata params are extracted as real typed variables, not stubs
        assert!(
            !output.contains("/* TODO: extract metadata */"),
            "expected no TODO placeholder in output:\n{output}"
        );
        assert!(
            !output.contains("TODO: extract metadata"),
            "expected no TODO marker in output:\n{output}"
        );

        // Assert the "path" metadata param is extracted and declared with proper type
        assert!(
            output.contains("let path: String"),
            "expected `let path: String` extraction in output:\n{output}"
        );

        // Assert the "method" metadata param is extracted and declared with proper type
        assert!(
            output.contains("let method: String"),
            "expected `let method: String` extraction in output:\n{output}"
        );

        // Assert both metadata params are passed to the registration method call
        assert!(
            output.contains("owner.add_handler(path, method, handler)"),
            "expected owner.add_handler(path, method, handler) call in output:\n{output}"
        );

        // Assert metadata is accessed from the _metadata vector
        assert!(
            output.contains("_metadata.get("),
            "expected _metadata.get(...) access in output:\n{output}"
        );
    }

    #[test]
    fn registration_variants_emit_napi_methods() {
        use crate::core::ir::{RegistrationVariant, WrapperConstructorArg, WrapperConstructorCall};

        let mut surface = make_fixture_surface();

        // Add a variant to the registration
        if let Some(reg) = surface.services[0].registrations.first_mut() {
            reg.variants.push(RegistrationVariant {
                name: "get".to_owned(),
                overrides: vec![],
                wrapper_call: Some(WrapperConstructorCall {
                    metadata_param: "builder".to_owned(),
                    wrapper_type_path: "my_crate::RouteBuilder".to_owned(),
                    wrapper_type_name: "RouteBuilder".to_owned(),
                    constructor_method: "new".to_owned(),
                    args: vec![
                        WrapperConstructorArg::Fixed {
                            param_name: "method".to_owned(),
                            value_expr: "my_crate::Method::GET".to_owned(),
                        },
                        WrapperConstructorArg::Free {
                            param: ParamDef {
                                name: "path".to_owned(),
                                ty: TypeRef::String,
                                optional: false,
                                default: None,
                                ..ParamDef::default()
                            },
                        },
                    ],
                }),
                signature_params: vec![ParamDef {
                    name: "path".to_owned(),
                    ty: TypeRef::String,
                    optional: false,
                    default: None,
                    ..ParamDef::default()
                }],
                doc: Some("Register a GET handler.".to_owned()),
            });
        }

        let config = ResolvedCrateConfig {
            name: "my_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };
        let output = gen_service_rs(&surface, &config);

        // Assert the variant method is emitted with #[napi]
        assert!(
            output.contains("#[napi]\npub fn get("),
            "expected `#[napi] pub fn get(` in output:\n{output}"
        );

        // Assert the wrapper builder is constructed
        assert!(
            output.contains("my_crate::RouteBuilder::new("),
            "expected wrapper constructor call in output:\n{output}"
        );

        // Assert the fixed arg is substituted
        assert!(
            output.contains("my_crate::Method::GET"),
            "expected fixed arg substitution in output:\n{output}"
        );
    }
}
