//! Service-API codegen for the Dart backend.
//!
//! Generates Dart code that wraps the C FFI service contract (produced by the FFI backend).
//!
//! **`service.dart`** — Idiomatic Dart service class that:
//! - Uses `dart:ffi` to load the C shared library and lookup symbols for:
//!   - Constructor/destructor (`<prefix>_<service>_new`, `<prefix>_<service>_free`)
//!   - Registration functions (`<prefix>_<service>_register_<method>`)
//!   - Entrypoint runners (`<prefix>_<service>_ep_<method>`)
//! - Wraps a host Dart handler as a C callback via `NativeCallable.isolateLocal`
//! - Maintains a registry to map C context pointers back to Dart handlers
//! - Marshals request/response JSON via C char* FFI boundaries

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

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

// ──────────────────────────────────────────────────── Dart service generator ──

/// Generate the idiomatic Dart service class (`service.dart`).
///
/// Produces a Dart module containing one class per service. Each class:
/// - Uses `dart:ffi` to call C FFI symbols (constructor, registration, entrypoint)
/// - Wraps Dart handlers as C callbacks via `NativeCallable`
/// - Maintains a registry mapping context pointers to handlers
/// - Exposes registration and run methods matching the IR
pub(super) fn gen_service_dart(api: &ApiSurface, config: &ResolvedCrateConfig) -> String {
    let prefix = config.ffi_prefix();
    let mut out = String::new();

    out.push_str("// Generated by alef. Do not edit by hand.\n\n");
    out.push_str("import 'dart:ffi' as ffi;\n");
    out.push_str("import 'dart:io' show DynamicLibrary, Platform;\n\n");

    out.push_str("/// Callback typedef for C handler registration.\n");
    out.push_str("typedef _HandlerCallback = ffi.Pointer<ffi.Char> Function(\n");
    out.push_str("    ffi.Pointer<ffi.Void> context,\n");
    out.push_str("    ffi.Pointer<ffi.Char> requestJson\n");
    out.push_str(");\n\n");

    // Load the C library once
    out.push_str("late final ffi.DynamicLibrary _ffiBridge = _loadFFIBridge();\n\n");

    out.push_str("ffi.DynamicLibrary _loadFFIBridge() {\n");
    out.push_str("  if (Platform.isWindows) {\n");
    out.push_str(&format!(
        "    return DynamicLibrary.open('{}.dll');\n",
        config.name.replace('-', "_")
    ));
    out.push_str("  } else if (Platform.isMacOS) {\n");
    out.push_str(&format!(
        "    return DynamicLibrary.open('{}.dylib');\n",
        config.name.replace('-', "_")
    ));
    out.push_str("  } else if (Platform.isLinux || Platform.isAndroid) {\n");
    out.push_str(&format!(
        "    return DynamicLibrary.open('lib{}.so');\n",
        config.name.replace('-', "_")
    ));
    out.push_str("  } else {\n");
    out.push_str("    throw UnsupportedError('Unsupported platform');\n");
    out.push_str("  }\n");
    out.push_str("}\n\n");

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

    out
}

fn gen_service_class(out: &mut String, service: &ServiceDef, api: &ApiSurface, prefix: &str) {
    let class_name = &service.name;
    let service_snake = service.name.to_snake_case();
    let prefix_lower = prefix.to_lowercase();

    out.push_str(&format!("/// Service class for {}.\n", class_name));
    if !service.doc.is_empty() {
        out.push_str(&format!("///\n/// {}\n", service.doc.trim()));
    }
    out.push_str(&format!("class {class_name} {{\n"));

    // Private fields
    out.push_str("  late ffi.Pointer<ffi.Void> _owner;\n");
    out.push_str("  final Map<ffi.Pointer<ffi.Void>, _HandlerRegistry> _handlers = {};\n");
    out.push_str("  int _contextCounter = 0;\n\n");

    // Constructor
    out.push_str(&format!("  /// Create a new {} instance.\n", class_name));
    out.push_str(&format!(
        "  {class_name}() {{\n    \
         _owner = _ffiBridge.lookupFunction<\n      \
             ffi.Pointer<ffi.Void> Function(),\n      \
             ffi.Pointer<ffi.Void> Function()\n    \
         >('{prefix_lower}_{service_snake}_new')();\n  \
         }}\n\n"
    ));

    // Destructor-like method
    out.push_str(&format!("  /// Free the {} instance.\n", class_name));
    out.push_str("  void dispose() {\n");
    out.push_str(&format!(
        "    _ffiBridge.lookupFunction<void Function(ffi.Pointer<ffi.Void>), void Function(ffi.Pointer<ffi.Void>)>\n      \
         ('{prefix_lower}_{service_snake}_free')(_owner);\n"
    ));
    out.push_str("    _handlers.clear();\n");
    out.push_str("  }\n\n");

    // Registration methods
    for reg in &service.registrations {
        gen_registration_method(out, service, reg, api, prefix);
        out.push('\n');
    }

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

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

fn gen_registration_method(
    out: &mut String,
    service: &ServiceDef,
    reg: &RegistrationDef,
    _api: &ApiSurface,
    prefix: &str,
) {
    let service_snake = service.name.to_snake_case();
    let reg_method_snake = reg.method.to_snake_case();
    let prefix_lower = prefix.to_lowercase();
    let method_name = &reg.method;

    // Build parameter list (excluding the callback)
    let mut params = Vec::new();
    for meta_param in &reg.metadata_params {
        let dart_type = typeref_to_dart_type(&meta_param.ty);
        if meta_param.optional {
            params.push(format!("{}? {}", dart_type, meta_param.name));
        } else {
            params.push(format!("{} {}", dart_type, meta_param.name));
        }
    }
    params.push("_HandlerCallback? handler".to_string());
    let param_sig = params.join(", ");

    out.push_str(&format!("  /// Register a handler callback for '{}'.\n", method_name));
    if !reg.doc.is_empty() {
        out.push_str(&format!("  ///\n  /// {}\n", reg.doc.trim()));
    }
    out.push_str(&format!("  void {method_name}({param_sig}) {{\n"));
    out.push_str("    if (handler == null) return;\n\n");

    // Create a context pointer to store the handler
    out.push_str("    final contextPtr = ffi.calloc<ffi.Pointer<ffi.Void>>(1);\n");
    out.push_str("    contextPtr.value = ffi.Pointer<ffi.Void>.fromAddress(_contextCounter++);\n");
    out.push_str("    _handlers[contextPtr.value] = _HandlerRegistry(handler);\n\n");

    // Call the C registration function
    out.push_str(
        "    final registerFn = _ffiBridge.lookupFunction<\n      \
         ffi.Int Function(\n        \
           ffi.Pointer<ffi.Void>,\n        \
           ffi.Pointer<ffi.NativeFunction<_HandlerCallback>>,\n        \
           ffi.Pointer<ffi.Void>",
    );

    // Add metadata parameter types
    for meta_param in &reg.metadata_params {
        let c_type = typeref_to_c_ffi_type(&meta_param.ty);
        out.push_str(&format!(",\n        {c_type}"));
    }

    out.push_str(
        "\n      ),\n      \
         int Function(\n        \
           ffi.Pointer<ffi.Void>,\n        \
           ffi.Pointer<ffi.NativeFunction<_HandlerCallback>>,\n        \
           ffi.Pointer<ffi.Void>",
    );

    for meta_param in &reg.metadata_params {
        let dart_type = typeref_to_rust_ffi_type(&meta_param.ty);
        out.push_str(&format!(",\n        {dart_type}"));
    }

    out.push_str(&format!(
        "\n      )\n    \
         >('{prefix_lower}_{service_snake}_register_{reg_method_snake}');\n\n"
    ));

    // Build the C function pointer from the Dart handler
    out.push_str("    final nativeCallback = ffi.NativeCallable<_HandlerCallback>.isolateLocal(\n");
    out.push_str("      (contextPtr, requestJsonPtr) => _handleRequest(requestJsonPtr, handler),\n");
    out.push_str("    );\n\n");

    // Call registration with metadata
    out.push_str("    registerFn(\n");
    out.push_str("      _owner,\n");
    out.push_str("      nativeCallback.nativeFunction,\n");
    out.push_str("      contextPtr.value");
    for meta_param in &reg.metadata_params {
        out.push_str(&format!(",\n      {}", meta_param.name));
    }
    out.push_str("\n    );\n");
    out.push_str("  }\n");
}

fn gen_entrypoint_method(out: &mut String, service: &ServiceDef, ep: &crate::core::ir::EntrypointDef, prefix: &str) {
    let service_snake = service.name.to_snake_case();
    let ep_name_snake = ep.method.to_snake_case();
    let prefix_lower = prefix.to_lowercase();
    let ep_method = &ep.method;

    // Build parameter list
    let mut params = Vec::new();
    for p in &ep.params {
        let dart_type = typeref_to_dart_type(&p.ty);
        if p.optional {
            params.push(format!("{}? {}", dart_type, p.name));
        } else {
            params.push(format!("{} {}", dart_type, p.name));
        }
    }
    let param_sig = params.join(", ");

    let return_type = typeref_to_dart_type(&ep.return_type);

    out.push_str(&format!(
        "  /// {} the service.\n",
        if ep.kind == EntrypointKind::Run {
            "Run"
        } else {
            "Finalize"
        }
    ));
    if !ep.doc.is_empty() {
        out.push_str(&format!("  ///\n  /// {}\n", ep.doc.trim()));
    }

    if ep.is_async {
        out.push_str(&format!("  Future<{return_type}> {ep_method}({param_sig}) async {{\n"));
    } else {
        out.push_str(&format!("  {return_type} {ep_method}({param_sig}) {{\n"));
    }

    // Lookup and call the C entrypoint function
    let c_return_type = typeref_to_c_ffi_type(&ep.return_type);
    out.push_str(&format!(
        "    final epFn = _ffiBridge.lookupFunction<\n      \
         {c_return_type} Function(ffi.Pointer<ffi.Void>"
    ));

    for p in &ep.params {
        let c_type = typeref_to_c_ffi_type(&p.ty);
        out.push_str(&format!(",\n        {c_type}"));
    }

    out.push_str(&format!(
        "\n      ),\n      \
         {return_type} Function(ffi.Pointer<ffi.Void>"
    ));

    for p in &ep.params {
        let dart_type = typeref_to_rust_ffi_type(&p.ty);
        out.push_str(&format!(",\n        {dart_type}"));
    }

    out.push_str(&format!(
        "\n      )\n    \
         >('{prefix_lower}_{service_snake}_ep_{ep_name_snake}');\n\n"
    ));

    // Call the C function
    out.push_str("    return epFn(_owner");
    for p in &ep.params {
        out.push_str(&format!(", {}", p.name));
    }
    out.push_str(");\n");
    out.push_str("  }\n");
}

// ──────────────────────────────────────────────────────── C type conversion ──

/// Map a `TypeRef` to a Dart `dart:ffi` type annotation.
fn typeref_to_dart_type(ty: &TypeRef) -> String {
    match ty {
        TypeRef::String => "String".to_owned(),
        TypeRef::Char => "String".to_owned(),
        TypeRef::Primitive(p) => {
            use crate::core::ir::PrimitiveType;
            match p {
                PrimitiveType::Bool => "bool".to_owned(),
                PrimitiveType::U8 => "int".to_owned(),
                PrimitiveType::U16 => "int".to_owned(),
                PrimitiveType::U32 => "int".to_owned(),
                PrimitiveType::U64 => "int".to_owned(),
                PrimitiveType::I8 => "int".to_owned(),
                PrimitiveType::I16 => "int".to_owned(),
                PrimitiveType::I32 => "int".to_owned(),
                PrimitiveType::I64 => "int".to_owned(),
                PrimitiveType::F32 => "double".to_owned(),
                PrimitiveType::F64 => "double".to_owned(),
                PrimitiveType::Usize => "int".to_owned(),
                PrimitiveType::Isize => "int".to_owned(),
            }
        }
        TypeRef::Bytes => "List<int>".to_owned(),
        TypeRef::Unit => "void".to_owned(),
        TypeRef::Optional(inner) => format!("{}?", typeref_to_dart_type(inner)),
        TypeRef::Vec(inner) => format!("List<{}>", typeref_to_dart_type(inner)),
        TypeRef::Map(k, v) => format!("Map<{}, {}>", typeref_to_dart_type(k), typeref_to_dart_type(v)),
        TypeRef::Named(n) => n.clone(),
        TypeRef::Json => "Map<String, dynamic>".to_owned(),
        TypeRef::Path => "String".to_owned(),
        TypeRef::Duration => "Duration".to_owned(),
    }
}

/// Map a `TypeRef` to a C FFI type annotation (as it appears in `dart:ffi`).
fn typeref_to_c_ffi_type(ty: &TypeRef) -> String {
    match ty {
        TypeRef::String => "ffi.Pointer<ffi.Char>".to_owned(),
        TypeRef::Char => "ffi.Char".to_owned(),
        TypeRef::Primitive(p) => {
            use crate::core::ir::PrimitiveType;
            match p {
                PrimitiveType::Bool => "ffi.Bool".to_owned(),
                PrimitiveType::U8 => "ffi.Uint8".to_owned(),
                PrimitiveType::U16 => "ffi.Uint16".to_owned(),
                PrimitiveType::U32 => "ffi.Uint32".to_owned(),
                PrimitiveType::U64 => "ffi.Uint64".to_owned(),
                PrimitiveType::I8 => "ffi.Int8".to_owned(),
                PrimitiveType::I16 => "ffi.Int16".to_owned(),
                PrimitiveType::I32 => "ffi.Int32".to_owned(),
                PrimitiveType::I64 => "ffi.Int64".to_owned(),
                PrimitiveType::F32 => "ffi.Float".to_owned(),
                PrimitiveType::F64 => "ffi.Double".to_owned(),
                PrimitiveType::Usize => "ffi.UintPtr".to_owned(),
                PrimitiveType::Isize => "ffi.IntPtr".to_owned(),
            }
        }
        TypeRef::Bytes => "ffi.Pointer<ffi.Uint8>".to_owned(),
        TypeRef::Unit => "ffi.Void".to_owned(),
        _ => "ffi.Pointer<ffi.Void>".to_owned(),
    }
}

/// Map a `TypeRef` to a Rust FFI type annotation (Dart's representation of Rust types).
fn typeref_to_rust_ffi_type(ty: &TypeRef) -> String {
    match ty {
        TypeRef::String => "String".to_owned(),
        TypeRef::Char => "String".to_owned(),
        TypeRef::Primitive(p) => {
            use crate::core::ir::PrimitiveType;
            match p {
                PrimitiveType::Bool => "bool".to_owned(),
                PrimitiveType::U8 => "int".to_owned(),
                PrimitiveType::U16 => "int".to_owned(),
                PrimitiveType::U32 => "int".to_owned(),
                PrimitiveType::U64 => "int".to_owned(),
                PrimitiveType::I8 => "int".to_owned(),
                PrimitiveType::I16 => "int".to_owned(),
                PrimitiveType::I32 => "int".to_owned(),
                PrimitiveType::I64 => "int".to_owned(),
                PrimitiveType::F32 => "double".to_owned(),
                PrimitiveType::F64 => "double".to_owned(),
                PrimitiveType::Usize => "int".to_owned(),
                PrimitiveType::Isize => "int".to_owned(),
            }
        }
        TypeRef::Bytes => "List<int>".to_owned(),
        TypeRef::Unit => "void".to_owned(),
        TypeRef::Named(n) => n.clone(),
        _ => "Object?".to_owned(),
    }
}

// ──────────────────────────────────────────────────── handler registry ──

fn gen_handler_registry(out: &mut String) {
    out.push_str("/// Internal handler registry for mapping C context pointers to Dart callbacks.\n");
    out.push_str("class _HandlerRegistry {\n");
    out.push_str("  final _HandlerCallback callback;\n\n");
    out.push_str("  _HandlerRegistry(this.callback);\n");
    out.push_str("}\n\n");

    out.push_str("/// Delegate function for handling C callback requests.\n");
    out.push_str("ffi.Pointer<ffi.Char> _handleRequest(\n");
    out.push_str("  ffi.Pointer<ffi.Char> requestJsonPtr,\n");
    out.push_str("  _HandlerCallback callback,\n");
    out.push_str(") {\n");
    out.push_str("  try {\n");
    out.push_str("    // Extract the JSON string from the C pointer\n");
    out.push_str("    final requestJson = requestJsonPtr.cast<ffi.Utf8>().toDartString();\n\n");
    out.push_str("    // Call the Dart handler with the JSON request\n");
    out.push_str("    final responseJsonPtr = callback(ffi.nullptr, requestJsonPtr);\n\n");
    out.push_str("    // Return the response pointer (the callback is responsible for allocation)\n");
    out.push_str("    return responseJsonPtr;\n");
    out.push_str("  } catch (e) {\n");
    out.push_str("    // On error, return a JSON error response\n");
    out.push_str("    // This is a simplified implementation; in production you'd want\n");
    out.push_str("    // to properly marshal the error to JSON.\n");
    out.push_str("    return ffi.nullptr;\n");
    out.push_str("  }\n");
    out.push_str("}\n");
}

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

/// Generate all service-API files for the Dart backend.
///
/// Returns one `GeneratedFile` when services are present:
/// - `{output_dir}/service.dart` — Dart service class
pub fn generate(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
    if api.services.is_empty() {
        return Ok(vec![]);
    }

    let output_dir = "packages/dart/lib/src".to_string();

    // Dart service class
    let mut service_dart = gen_service_dart(api, config);
    gen_handler_registry(&mut service_dart);

    Ok(vec![GeneratedFile {
        path: PathBuf::from(&output_dir).join("service.dart"),
        content: service_dart,
        generated_header: false,
    }])
}

// ───────────────────────────────────────────────────────────────────── 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(),
        };

        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()),
            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_gen_service_dart_produces_valid_dart() {
        let api = make_fixture_surface();
        let config = ResolvedCrateConfig {
            name: "test_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };

        let dart = gen_service_dart(&api, &config);

        // Verify that the generated Dart contains expected FFI markers
        assert!(dart.contains("import 'dart:ffi'"));
        assert!(dart.contains("typedef _HandlerCallback"));
        assert!(dart.contains("class TestService"));
        assert!(dart.contains("_loadFFIBridge()"));
    }

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

        let dart = gen_service_dart(&api, &config);

        assert!(dart.contains("class TestService {"));
        assert!(dart.contains("late ffi.Pointer<ffi.Void> _owner;"));
        assert!(dart.contains("final Map<ffi.Pointer<ffi.Void>, _HandlerRegistry> _handlers = {};"));
    }

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

        let dart = gen_service_dart(&api, &config);

        assert!(dart.contains("TestService()"));
        assert!(dart.contains("_ffiBridge.lookupFunction"));
        assert!(dart.contains("test_crate_test_service_new"));
    }

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

        let dart = gen_service_dart(&api, &config);

        assert!(dart.contains("void dispose()"));
        assert!(dart.contains("test_crate_test_service_free"));
        assert!(dart.contains("_handlers.clear()"));
    }

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

        let dart = gen_service_dart(&api, &config);

        assert!(dart.contains("void add_handler(String path, _HandlerCallback? handler)"));
        assert!(dart.contains("test_crate_test_service_register_add_handler"));
        assert!(dart.contains("NativeCallable<_HandlerCallback>.isolateLocal"));
    }

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

        let dart = gen_service_dart(&api, &config);

        assert!(dart.contains("Future<void> run(String addr)"));
        assert!(dart.contains("test_crate_test_service_ep_run"));
    }

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

        let dart = gen_service_dart(&api, &config);

        assert!(dart.contains("_loadFFIBridge()"));
        assert!(dart.contains("Platform.isWindows"));
        assert!(dart.contains("Platform.isMacOS"));
        assert!(dart.contains("Platform.isLinux"));
    }

    #[test]
    fn test_handler_registry_is_generated() {
        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");
        let content = &files[0].content;

        assert!(content.contains("class _HandlerRegistry"));
        assert!(content.contains("final _HandlerCallback callback;"));
        assert!(content.contains("_handleRequest"));
    }

    #[test]
    fn generate_returns_one_file_for_non_empty_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, got {}", files.len());
        assert!(files[0].path.to_string_lossy().ends_with("service.dart"));
    }

    #[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");
    }
}
