//! Service-API codegen for the Dart backend (FRB-based).
//!
//! Generates Rust source that declares FRB-opaque service owners and handler bridges.
//! The Dart side is auto-generated by flutter_rust_bridge from these declarations.
//!
//! **Output: Rust source** appended to `packages/dart/rust/src/lib.rs`:
//! - An `#[frb(opaque)]` owner struct per service
//! - Constructor and configurator methods (`#[frb(sync)]`)
//! - Registration methods accepting `DartFnFuture<String>` callbacks
//! - Entrypoint methods (async `run` or sync/async `finalize`)
//! - `DartHandler` adapter structs implementing handler contracts via manual `Pin<Box<dyn Future>>`

use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{ApiSurface, EntrypointKind, HandlerContractDef, ServiceDef, TypeRef};
use std::collections::BTreeSet;
use std::path::PathBuf;

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

/// Format a multi-line Rust doc as a Rust `///` block at the given column
/// indent. Every non-blank line is prefixed with `/// `; blank lines stay as
/// bare `///` so paragraph breaks survive. Includes the trailing newline.
fn format_dart_comment(text: &str, indent: usize) -> String {
    let trimmed = text.trim();
    if trimmed.is_empty() {
        return String::new();
    }
    let pad = " ".repeat(indent);
    let mut out = String::new();
    for line in trimmed.lines() {
        if line.trim().is_empty() {
            out.push_str(&pad);
            out.push_str("///\n");
        } else {
            out.push_str(&pad);
            out.push_str("/// ");
            out.push_str(line);
            out.push('\n');
        }
    }
    out
}

/// Whether an entrypoint's return type can be represented as a Rust type suitable for FRB.
///
/// Unit/primitive/string/bytes map to simple Rust types; a `Named` type is representable only
/// when this surface wraps it. Anything else (e.g. a foreign framework type a `finalize` converts into)
/// is not representable and the entrypoint is skipped from FRB emission.
///
/// Additionally, if the entrypoint's source method is marked as `sanitized` (e.g., method returns
/// an unknown type that was mapped to String), the entrypoint is skipped — the return type cannot
/// be safely used without the original type information.
fn entrypoint_return_representable(
    ep: &crate::core::ir::EntrypointDef,
    service: &ServiceDef,
    api: &ApiSurface,
) -> bool {
    // Check if the source method is sanitized; if so, the entrypoint is not representable.
    if let Some(svc_type) = api.types.iter().find(|t| t.name == service.name) {
        if let Some(method) = svc_type.methods.iter().find(|m| m.name == ep.method) {
            if method.sanitized {
                return false;
            }
        }
    }

    // Check if the return type itself is representable.
    match &ep.return_type {
        TypeRef::Unit | TypeRef::String | TypeRef::Char | TypeRef::Primitive(_) | TypeRef::Bytes => true,
        TypeRef::Named(n) => api.types.iter().any(|t| t.name == *n),
        _ => false,
    }
}

/// 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)
}

// ──────────────────────────────────────────────────────── Rust service codegen ──

/// Generate the Rust source for FRB-bridged service owners and handlers.
///
/// For each service this emits:
/// - An `#[frb(opaque)]` owner struct holding the inner service + registrations
/// - Constructor, configurators, registration methods, and entrypoints
/// - Handler adapter structs (`DartHandler*`) that bridge `DartFnFuture<String>` callbacks to trait impls
pub fn gen_service_rust(api: &ApiSurface, config: &ResolvedCrateConfig) -> String {
    let core_import = config.core_import_name();
    let mut out = String::new();

    out.push_str(&crate::backends::dart::template_env::render(
        "service_api/file_header.rs.jinja",
        minijinja::context! {},
    ));
    out.push_str("\n\n");

    // Emit handler bridges for all unique contracts referenced by registrations
    let referenced_contracts: Vec<&HandlerContractDef> = {
        let names: BTreeSet<&str> = api
            .services
            .iter()
            .flat_map(|s| s.registrations.iter())
            .map(|r| r.callback_contract.as_str())
            .collect();
        names.iter().filter_map(|n| find_contract(api, n)).collect()
    };

    for contract in &referenced_contracts {
        gen_handler_bridge(&mut out, contract, &core_import);
        out.push('\n');
    }

    // Emit one service owner per service definition
    for service in &api.services {
        gen_service_owner(&mut out, service, api, &core_import);
        out.push('\n');
    }

    out
}

/// Emit a `DartHandler{ContractName}` struct + trait impl for an async handler contract.
///
/// The struct wraps a `DartFnFuture<String>` callback and implements the trait's dispatch method
/// using manual `Pin<Box<dyn Future>>` (not `#[async_trait]`). The dispatch:
/// 1. Serializes the wire request to JSON
/// 2. Calls the Dart handler closure with the JSON string
/// 3. Deserializes the JSON response back to the wire response type
/// 4. If a response_adapter is configured, applies it before returning
fn gen_handler_bridge(out: &mut String, contract: &HandlerContractDef, core_import: &str) {
    let trait_name = &contract.trait_name;
    let bridge_name = format!("DartHandler{}", trait_name);

    out.push_str(&crate::backends::dart::template_env::render(
        "service_api/handler_bridge_doc.rs.jinja",
        minijinja::context! {
            trait_name => trait_name.as_str(),
        },
    ));
    out.push('\n');

    out.push_str(&crate::backends::dart::template_env::render(
        "service_api/handler_bridge_struct.rs.jinja",
        minijinja::context! {
            bridge_name => bridge_name.as_str(),
        },
    ));
    out.push_str("\n\n");

    out.push_str(&crate::backends::dart::template_env::render(
        "service_api/handler_bridge_constructor.rs.jinja",
        minijinja::context! {
            bridge_name => bridge_name.as_str(),
        },
    ));
    out.push_str("\n\n");

    // Determine wire types — derive from the contract definition
    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");

    // Build the request and response type paths
    let req_path = if req_type.contains("::") {
        req_type.to_string()
    } else {
        format!("{core_import}::{req_type}")
    };
    let resp_path = if resp_type.contains("::") {
        resp_type.to_string()
    } else {
        format!("{core_import}::{resp_type}")
    };

    // Leading dispatch parameters (e.g. from dispatch_extra_params)
    let extra_params: String = contract
        .dispatch_extra_params
        .iter()
        .map(|p| format!(", {p}"))
        .collect();

    let wire_param_name = contract.wire_param_name.as_deref().unwrap_or("request");
    let dispatch_name = &contract.dispatch.name;

    // The future's Output type
    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());

    // Tail: apply response_adapter if configured
    let tail = match &contract.response_adapter {
        Some(adapter) => format!("{adapter}(outcome)"),
        None => "outcome".to_string(),
    };

    out.push_str(&crate::backends::dart::template_env::render(
        "service_api/handler_bridge_impl_open.rs.jinja",
        minijinja::context! {
            core_import => core_import,
            trait_name => trait_name.as_str(),
            bridge_name => bridge_name.as_str(),
            dispatch_name => dispatch_name.as_str(),
            extra_params => &extra_params,
            wire_param_name => wire_param_name,
            req_path => req_path.as_str(),
            resp_path => resp_path.as_str(),
            wire_output => wire_output.as_str(),
            box_err => box_err,
            output_type => output_type.as_str(),
            tail => tail.as_str(),
        },
    ));
}

/// Emit an `#[frb(opaque)]` service owner struct and its impl block.
///
/// The struct holds:
/// - `inner`: `tokio::sync::Mutex<Option<service_type>>` — the actual service
/// - `registrations`: `tokio::sync::Mutex<Vec<DartRegistration>>` — pending registrations
///
/// Methods include constructor, configurators, registration, and entrypoints.
fn gen_service_owner(out: &mut String, service: &ServiceDef, api: &ApiSurface, _core_import: &str) {
    let service_name = &service.name;
    let owner_path = &service.rust_path;

    // Emit doc comment
    if !service.doc.is_empty() {
        let doc_lines = format_dart_comment(&service.doc, 0);
        out.push_str(&crate::backends::dart::template_env::render(
            "service_api/service_owner_doc.rs.jinja",
            minijinja::context! {
                service_name => service_name.as_str(),
                doc => service.doc.as_str(),
                doc_lines => doc_lines.as_str(),
            },
        ));
    } else {
        out.push_str(&format!("/// FRB-opaque service owner: {}.\n", service_name));
    }

    // Emit service owner struct
    out.push('\n');
    out.push_str(&crate::backends::dart::template_env::render(
        "service_api/service_owner_struct.rs.jinja",
        minijinja::context! {
            service_name => service_name.as_str(),
            owner_path => owner_path.as_str(),
        },
    ));
    out.push_str("\n\n");

    // Emit constructor and impl open
    let ctor_params = format_ctor_params(service);
    let ctor_call = format_ctor_call(service);

    out.push_str(&crate::backends::dart::template_env::render(
        "service_api/service_owner_impl_open.rs.jinja",
        minijinja::context! {
            service_name => service_name.as_str(),
            owner_path => owner_path.as_str(),
            ctor_params => ctor_params.as_str(),
            ctor_call => ctor_call.as_str(),
        },
    ));
    out.push('\n');

    // Emit configurator methods
    for config in &service.configurators {
        let method_params = format_method_params(config);
        let configurator_params: Vec<_> = config
            .params
            .iter()
            .map(|p| {
                // Named params resolving to opaque mirrors use `.inner`; other Named
                // params (non-opaque mirrors with derived `From<MirrorT> for SourceT`)
                // use `.into()`; primitives / strings pass through.
                let (is_opaque, is_named) = match &p.ty {
                    TypeRef::Named(n) => {
                        let opaque = api.types.iter().any(|t| t.name == *n && t.is_opaque);
                        (opaque, true)
                    }
                    _ => (false, false),
                };
                minijinja::context! {
                    name => p.name.as_str(),
                    rust_type => typeref_to_rust_type(&p.ty).as_str(),
                    is_opaque => is_opaque,
                    is_named => is_named,
                }
            })
            .collect();

        let is_owned = matches!(config.receiver, Some(crate::core::ir::ReceiverKind::Owned));

        out.push_str(&crate::backends::dart::template_env::render(
            "service_api/configurator_method.rs.jinja",
            minijinja::context! {
                config_name => config.name.as_str(),
                method_params => method_params.as_str(),
                configurator_params => configurator_params,
                is_owned => is_owned,
            },
        ));
        out.push('\n');
    }

    // Emit registration methods
    for reg in &service.registrations {
        let metadata_params: Vec<_> = reg
            .metadata_params
            .iter()
            .map(|p| {
                // Named params resolving to an opaque local mirror need `.inner`
                // to extract the wrapped core value before being passed to the
                // inner service method — FRB-opaque wrappers carry the core value
                // in a field named `inner`. Primitives / strings / non-opaque
                // mirrors pass through unchanged via `From<MirrorT> for SourceT`.
                let is_opaque = matches!(&p.ty, TypeRef::Named(n)
                    if api.types.iter().any(|t| t.name == *n && t.is_opaque));
                minijinja::context! {
                    name => p.name.as_str(),
                    rust_type => typeref_to_rust_type(&p.ty).as_str(),
                    is_opaque => is_opaque,
                }
            })
            .collect();

        let bridge_name = format!("DartHandler{}", reg.callback_contract);
        // Resolve the trait's full Rust path for the `Arc<dyn Trait>` cast in the
        // registration body. Falls back to the bare trait name when the contract
        // is missing a rust_path (defensive — the extractor populates it).
        let trait_path = find_contract(api, &reg.callback_contract)
            .map(|c| {
                if c.rust_path.is_empty() {
                    c.trait_name.clone()
                } else {
                    c.rust_path.clone()
                }
            })
            .unwrap_or_else(|| reg.callback_contract.clone());

        out.push_str(&crate::backends::dart::template_env::render(
            "service_api/registration_method.rs.jinja",
            minijinja::context! {
                method_name => reg.method.as_str(),
                callback_param => reg.callback_param.as_str(),
                metadata_params => metadata_params,
                bridge_name => bridge_name.as_str(),
                trait_path => trait_path.as_str(),
            },
        ));
        out.push('\n');

        // Emit registration variants
        for variant in &reg.variants {
            let signature_params: Vec<_> = variant
                .signature_params
                .iter()
                .map(|p| {
                    minijinja::context! {
                        name => p.name.as_str(),
                        rust_type => typeref_to_rust_type(&p.ty).as_str(),
                    }
                })
                .collect();

            let (wrapper_type_path, wrapper_local_type, constructor_method, wrapper_args) =
                if let Some(wc) = &variant.wrapper_call {
                    let args: Vec<_> = wc
                        .args
                        .iter()
                        .map(|arg| match arg {
                            crate::core::ir::WrapperConstructorArg::Fixed {
                                param_name: _,
                                value_expr,
                            } => minijinja::context! {
                                kind => "fixed",
                                value_expr => value_expr.as_str(),
                            },
                            crate::core::ir::WrapperConstructorArg::Free { param } => minijinja::context! {
                                kind => "free",
                                param => minijinja::context! {
                                    name => param.name.as_str(),
                                    rust_type => typeref_to_rust_type(&param.ty).as_str(),
                                },
                            },
                        })
                        .collect();
                    (
                        wc.wrapper_type_path.clone(),
                        wc.wrapper_type_name.clone(),
                        wc.constructor_method.clone(),
                        args,
                    )
                } else {
                    (String::new(), String::new(), String::new(), vec![])
                };

            out.push_str(&crate::backends::dart::template_env::render(
                "service_api/registration_variant.rs.jinja",
                minijinja::context! {
                    variant_name => variant.name.as_str(),
                    signature_params => signature_params,
                    base_method_name => reg.method.as_str(),
                    wrapper_call => variant.wrapper_call.is_some(),
                    wrapper_type_path => wrapper_type_path.as_str(),
                    wrapper_local_type => wrapper_local_type.as_str(),
                    constructor_method => constructor_method.as_str(),
                    wrapper_args => wrapper_args,
                },
            ));
            out.push('\n');
        }
    }

    // Emit entrypoint methods
    for ep in &service.entrypoints {
        gen_entrypoint_method(out, service, ep, api);
        out.push('\n');
    }

    // Close the impl block
    out.push_str(&crate::backends::dart::template_env::render(
        "service_api/service_owner_impl_close.rs.jinja",
        minijinja::context! {},
    ));
}

/// Emit an entrypoint method (run or finalize).
///
/// For `Run` entrypoints: async method that drains registrations, wraps handlers,
/// registers them on the inner service, and calls the run method.
///
/// For representable `Finalize` entrypoints: sync/async method that does similar but returns the result.
fn gen_entrypoint_method(
    out: &mut String,
    service: &ServiceDef,
    ep: &crate::core::ir::EntrypointDef,
    api: &ApiSurface,
) {
    // Skip non-representable finalizers
    if matches!(ep.kind, EntrypointKind::Finalize) && !entrypoint_return_representable(ep, service, api) {
        return;
    }

    let method_name = &ep.method;
    let is_async = ep.is_async;
    let return_type = typeref_to_rust_type(&ep.return_type);

    let params: Vec<_> = ep
        .params
        .iter()
        .map(|p| {
            minijinja::context! {
                name => p.name.as_str(),
                rust_type => typeref_to_rust_type(&p.ty).as_str(),
            }
        })
        .collect();

    let registrations: Vec<_> = service
        .registrations
        .iter()
        .map(|r| {
            minijinja::context! {
                method => r.method.as_str(),
            }
        })
        .collect();

    out.push_str(&crate::backends::dart::template_env::render(
        "service_api/entrypoint_method.rs.jinja",
        minijinja::context! {
            method_name => method_name.as_str(),
            is_async => is_async,
            return_type => return_type.as_str(),
            params => params,
            registrations => registrations,
        },
    ));
}

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

/// Format constructor parameters into a Rust param list.
fn format_ctor_params(service: &ServiceDef) -> String {
    service
        .constructor
        .params
        .iter()
        .map(|p| {
            let ty = typeref_to_rust_type(&p.ty);
            format!("{}: {}", p.name, ty)
        })
        .collect::<Vec<_>>()
        .join(", ")
}

/// Format the constructor call expression.
fn format_ctor_call(service: &ServiceDef) -> String {
    if service.constructor.params.is_empty() {
        "::new()".to_string()
    } else {
        let args = service
            .constructor
            .params
            .iter()
            .map(|p| p.name.clone())
            .collect::<Vec<_>>()
            .join(", ");
        format!("::new({})", args)
    }
}

/// Format configurator method parameters.
fn format_method_params(method: &crate::core::ir::MethodDef) -> String {
    let mut out = String::new();
    for param in &method.params {
        let ty = typeref_to_rust_type(&param.ty);
        out.push_str(&format!(", {}: {}", param.name, ty));
    }
    out
}

/// Convert a TypeRef to a Rust type annotation.
fn typeref_to_rust_type(ty: &TypeRef) -> String {
    match ty {
        TypeRef::String => "String".to_owned(),
        TypeRef::Char => "char".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::Unit => "()".to_owned(),
        TypeRef::Optional(inner) => format!("Option<{}>", typeref_to_rust_type(inner)),
        TypeRef::Vec(inner) => format!("Vec<{}>", typeref_to_rust_type(inner)),
        TypeRef::Map(k, v) => format!(
            "std::collections::HashMap<{}, {}>",
            typeref_to_rust_type(k),
            typeref_to_rust_type(v)
        ),
        TypeRef::Named(n) => n.clone(),
        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 Dart backend.
///
/// Returns one `GeneratedFile` when services are present:
/// - Rust source destined for `packages/dart/rust/src/lib.rs`
pub fn generate(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
    if api.services.is_empty() {
        return Ok(vec![]);
    }

    let rust_source = gen_service_rust(api, config);

    Ok(vec![GeneratedFile {
        path: PathBuf::from("packages/dart/rust/src/service_api.rs"),
        content: rust_source,
        generated_header: true,
    }])
}

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

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

    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 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()
            }],
            receiver: Some(crate::core::ir::ReceiverKind::RefMut),
            return_type: TypeRef::Unit,
            error_type: Some("HandlerError".to_owned()),
            doc: "Register a request handler.".to_owned(),
            variants: vec![],
        };

        let run_entrypoint = 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("IoError".to_owned()),
            doc: "Start the service.".to_owned(),
        };

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

        ApiSurface {
            crate_name: "test_crate".to_owned(),
            version: "1.0.0".to_owned(),
            services: vec![ServiceDef {
                name: "TestService".to_owned(),
                rust_path: "my_crate::TestService".to_owned(),
                constructor,
                configurators: vec![],
                registrations: vec![registration],
                entrypoints: vec![run_entrypoint],
                doc: "Test service.".to_owned(),
                cfg: None,
            }],
            handler_contracts: vec![handler_contract],
            ..ApiSurface::default()
        }
    }

    #[test]
    fn test_emit_service_owner_with_frb_opaque() {
        let api = make_fixture_surface();
        let config = ResolvedCrateConfig {
            name: "test_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };

        let rust = gen_service_rust(&api, &config);

        // Verify FRB opaque marker
        assert!(
            rust.contains("#[frb(opaque)]"),
            "expected `#[frb(opaque)]` marker in:\n{rust}"
        );

        // Verify service owner struct
        assert!(
            rust.contains("pub struct TestService"),
            "expected service owner struct in:\n{rust}"
        );

        // Verify no registrations field (handlers registered immediately)
        assert!(
            !rust.contains("registrations: tokio::sync::Mutex"),
            "should not have registrations field in:\n{rust}"
        );
    }

    #[test]
    fn test_emit_registration_with_dartfnfuture() {
        let api = make_fixture_surface();
        let config = ResolvedCrateConfig {
            name: "test_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };

        let rust = gen_service_rust(&api, &config);

        // Verify DartFnFuture parameter
        assert!(
            rust.contains("DartFnFuture<String>"),
            "expected `DartFnFuture<String>` in registration method:\n{rust}"
        );
    }

    #[test]
    fn test_emit_handler_bridge_with_manual_pin_box() {
        let api = make_fixture_surface();
        let config = ResolvedCrateConfig {
            name: "test_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };

        let rust = gen_service_rust(&api, &config);

        // Verify manual Pin<Box<dyn Future>> form (not #[async_trait])
        assert!(
            rust.contains("Pin<Box<dyn std::future::Future"),
            "expected manual `Pin<Box<dyn Future>>` form in handler bridge:\n{rust}"
        );

        // Verify NOT async_trait
        assert!(
            !rust.contains("#[async_trait]"),
            "should NOT emit #[async_trait] in:\n{rust}"
        );
    }

    #[test]
    fn test_no_dart_ffi_symbols() {
        let api = make_fixture_surface();
        let config = ResolvedCrateConfig {
            name: "test_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };

        let rust = gen_service_rust(&api, &config);

        // Verify NO dart:ffi symbols
        assert!(!rust.contains("dart:ffi"), "should not contain dart:ffi");
        assert!(!rust.contains("NativeCallable"), "should not contain NativeCallable");
        assert!(!rust.contains("lookupFunction"), "should not contain lookupFunction");
        assert!(!rust.contains("ffi.Pointer"), "should not contain ffi.Pointer");
    }

    #[test]
    fn test_skip_unrepresentable_finalize() {
        let constructor = MethodDef {
            name: "new".to_owned(),
            params: vec![],
            return_type: TypeRef::Unit,
            is_async: false,
            is_static: true,
            error_type: None,
            doc: String::new(),
            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,
        };

        // Finalize with unrepresentable Named return type
        let finalize_ep = EntrypointDef {
            method: "into_router".to_owned(),
            kind: EntrypointKind::Finalize,
            is_async: false,
            params: vec![],
            return_type: TypeRef::Named("ExternalRouter".to_owned()), // NOT in types
            error_type: None,
            doc: String::new(),
        };

        let api = ApiSurface {
            crate_name: "test_crate".to_owned(),
            version: "1.0.0".to_owned(),
            services: vec![ServiceDef {
                name: "TestService".to_owned(),
                rust_path: "my_crate::TestService".to_owned(),
                constructor,
                configurators: vec![],
                registrations: vec![],
                entrypoints: vec![finalize_ep],
                doc: String::new(),
                cfg: None,
            }],
            ..ApiSurface::default()
        };

        let config = ResolvedCrateConfig {
            name: "test_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };

        let rust = gen_service_rust(&api, &config);

        // Verify that the unrepresentable finalize method is NOT emitted
        assert!(
            !rust.contains("into_router"),
            "should not emit unrepresentable finalize method:\n{rust}"
        );
    }

    #[test]
    fn test_skip_sanitized_finalize() {
        let constructor = MethodDef {
            name: "new".to_owned(),
            params: vec![],
            return_type: TypeRef::Unit,
            is_async: false,
            is_static: true,
            error_type: None,
            doc: String::new(),
            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,
        };

        // Finalize method marked as sanitized (returns an unknown type mapped to String)
        let finalize_method = MethodDef {
            name: "into_router".to_owned(),
            params: vec![],
            return_type: TypeRef::String, // sanitized from unknown Router type
            is_async: false,
            is_static: false,
            error_type: None,
            doc: String::new(),
            receiver: Some(crate::core::ir::ReceiverKind::RefMut),
            sanitized: true, // <-- KEY: method is marked as sanitized
            trait_source: None,
            returns_ref: false,
            returns_cow: false,
            return_newtype_wrapper: None,
            has_default_impl: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
        };

        // Finalize entrypoint that references the sanitized method
        let finalize_ep = EntrypointDef {
            method: "into_router".to_owned(),
            kind: EntrypointKind::Finalize,
            is_async: false,
            params: vec![],
            return_type: TypeRef::String, // representable as String, but source is sanitized
            error_type: None,
            doc: String::new(),
        };

        // Service type that owns the sanitized method
        use crate::core::ir::TypeDef;
        let service_type = TypeDef {
            name: "TestService".to_owned(),
            rust_path: "my_crate::TestService".to_owned(),
            original_rust_path: String::new(),
            fields: vec![],
            methods: vec![finalize_method],
            is_opaque: false,
            is_clone: false,
            is_copy: false,
            doc: String::new(),
            cfg: None,
            is_trait: false,
            has_default: false,
            has_stripped_cfg_fields: false,
            is_return_type: false,
            serde_rename_all: None,
            has_serde: false,
            super_traits: vec![],
            binding_excluded: false,
            binding_exclusion_reason: None,
            is_variant_wrapper: false,
        };

        let api = ApiSurface {
            crate_name: "test_crate".to_owned(),
            version: "1.0.0".to_owned(),
            types: vec![service_type],
            services: vec![ServiceDef {
                name: "TestService".to_owned(),
                rust_path: "my_crate::TestService".to_owned(),
                constructor,
                configurators: vec![],
                registrations: vec![],
                entrypoints: vec![finalize_ep],
                doc: String::new(),
                cfg: None,
            }],
            ..ApiSurface::default()
        };

        let config = ResolvedCrateConfig {
            name: "test_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };

        let rust = gen_service_rust(&api, &config);

        // Verify that the sanitized finalize method is NOT emitted
        assert!(
            !rust.contains("into_router"),
            "should not emit finalize method when source is sanitized:\n{rust}"
        );
    }

    #[test]
    fn generate_returns_empty_for_no_services() {
        let api = ApiSurface::default();
        let config = ResolvedCrateConfig {
            name: "test_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };

        let files = generate(&api, &config).expect("generate should not fail");
        assert!(files.is_empty(), "expected no files for surface without services");
    }

    #[test]
    fn generate_returns_one_file_for_services() {
        let api = make_fixture_surface();
        let config = ResolvedCrateConfig {
            name: "test_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };

        let files = generate(&api, &config).expect("generate should not fail");
        assert_eq!(files.len(), 1, "expected 1 generated file");
        assert!(
            files[0].path.to_string_lossy().ends_with("service_api.rs"),
            "expected service_api.rs file"
        );
    }

    #[test]
    fn test_registration_calls_inner_directly() {
        let api = make_fixture_surface();
        let config = ResolvedCrateConfig {
            name: "test_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };

        let rust = gen_service_rust(&api, &config);

        // Verify that registration methods call inner.<method_name> directly
        // (not deferred dispatch logic)
        assert!(
            rust.contains("inner.add_handler("),
            "expected immediate inner.add_handler() call in:\n{rust}"
        );

        // Verify NO draining logic or match arms in registrations
        assert!(
            !rust.contains("for reg in registrations"),
            "should not have registration draining loop in:\n{rust}"
        );

        assert!(
            !rust.contains("match reg.method.as_str()"),
            "should not have method dispatch match in:\n{rust}"
        );
    }

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

        let constructor = MethodDef {
            name: "new".to_owned(),
            params: vec![],
            return_type: TypeRef::Unit,
            is_async: false,
            is_static: true,
            error_type: None,
            doc: String::new(),
            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 variant_with_wrapper = 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 registration = RegistrationDef {
            method: "route".to_owned(),
            callback_param: "handler".to_owned(),
            callback_contract: "RequestHandler".to_owned(),
            metadata_params: vec![ParamDef {
                name: "builder".to_owned(),
                ty: TypeRef::Named("RouteBuilder".to_owned()),
                optional: false,
                default: None,
                ..ParamDef::default()
            }],
            receiver: Some(crate::core::ir::ReceiverKind::RefMut),
            return_type: TypeRef::Unit,
            error_type: Some("HandlerError".to_owned()),
            doc: "Register a handler with a route builder.".to_owned(),
            variants: vec![variant_with_wrapper],
        };

        let handler_contract = HandlerContractDef {
            trait_name: "RequestHandler".to_owned(),
            rust_path: "my_crate::RequestHandler".to_owned(),
            dispatch: MethodDef {
                name: "handle".to_owned(),
                params: vec![ParamDef {
                    name: "req".to_owned(),
                    ty: TypeRef::Named("RequestData".to_owned()),
                    optional: false,
                    default: None,
                    ..ParamDef::default()
                }],
                return_type: TypeRef::Named("Response".to_owned()),
                is_async: true,
                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,
            },
            optional_methods: vec![],
            wire_request_type: Some("RequestData".to_owned()),
            wire_response_type: Some("Response".to_owned()),
            dispatch_extra_params: vec![],
            wire_param_name: None,
            dispatch_return_type: None,
            response_adapter: None,
            doc: String::new(),
        };

        let api = ApiSurface {
            crate_name: "test_crate".to_owned(),
            version: "1.0.0".to_owned(),
            services: vec![ServiceDef {
                name: "TestService".to_owned(),
                rust_path: "my_crate::TestService".to_owned(),
                constructor,
                configurators: vec![],
                registrations: vec![registration],
                entrypoints: vec![],
                doc: String::new(),
                cfg: None,
            }],
            handler_contracts: vec![handler_contract],
            ..ApiSurface::default()
        };

        let config = ResolvedCrateConfig {
            name: "test_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };

        let rust = gen_service_rust(&api, &config);

        // Verify variant method is emitted
        assert!(
            rust.contains("pub fn get("),
            "expected registration variant 'get' method in:\n{rust}"
        );

        // Verify wrapper constructor is called
        assert!(
            rust.contains("my_crate::RouteBuilder::new("),
            "expected wrapper constructor call in:\n{rust}"
        );

        // Verify fixed arg is substituted
        assert!(
            rust.contains("my_crate::Method::GET"),
            "expected fixed wrapper arg in:\n{rust}"
        );

        // Verify calls back to base registration method, wrapping the constructed
        // inner value in the local Dart wrapper newtype before forwarding.
        assert!(
            rust.contains("self.route(RouteBuilder { inner }"),
            "expected call to base route method with local newtype wrapper in:\n{rust}"
        );
    }
}
