//! Service-API codegen for the Java backend.
//!
//! Generates Java source files for service lifecycle and handler registration using Panama FFM:
//! - Service class wrapping opaque owner handles (via downcalls to C FFI symbols)
//! - Handler functional interface that accepts request JSON and returns response JSON
//! - Registration methods that build upcall stubs from handlers and invoke the C FFI
//! - Entrypoint methods (run/finalize) driving the service lifecycle
//!
//! Panama FFM Pattern:
//! - `Linker.nativeLinker()` + `SymbolLookup.libraryLookup(...)` to locate C symbols
//! - `downcallHandle()` + `FunctionDescriptor` for C function invocations
//! - `upcallStub()` + `MethodHandle` to wrap Java callbacks for C to call back into Java
//! - `Arena` for managing lifetime of callback stubs + context pointers
//! - String marshalling via `MemorySegment` + `getString()` / `CLinker.C_CHAR.byteSize()`

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

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

/// Map TypeRef to Java parameter type.
fn java_type_for_metadata(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::U8 | PrimitiveType::I8 => "byte".to_owned(),
                PrimitiveType::U16 | PrimitiveType::I16 => "short".to_owned(),
                PrimitiveType::U32 | PrimitiveType::I32 => "int".to_owned(),
                PrimitiveType::U64 | PrimitiveType::I64 => "long".to_owned(),
                PrimitiveType::F32 => "float".to_owned(),
                PrimitiveType::F64 => "double".to_owned(),
                PrimitiveType::Usize | PrimitiveType::Isize => "long".to_owned(),
            }
        }
        TypeRef::Bytes => "byte[]".to_owned(),
        TypeRef::Unit => "void".to_owned(),
        _ => "Object".to_owned(),
    }
}

// ──────────────────────────────────────────────── Java Service Class ──

/// Generate the idiomatic Java service class wrapper using Panama FFM.
///
/// The class exposes:
/// - Constructor that invokes the C FFI `{prefix}_{service}_new()` via downcall
/// - Registration methods that build upcall stubs from handlers and register them
/// - Run/Finalize entrypoint methods that invoke C FFI entrypoint downcalls
/// - AutoCloseable interface with close() to invoke the C FFI `_free()` downcall
/// - All Panama FFM binding details (Linker, downcallHandle, FunctionDescriptor, etc.)
fn gen_service_class(_api: &ApiSurface, service: &ServiceDef, package: &str, config: &ResolvedCrateConfig) -> String {
    let mut out = String::new();

    // File header
    out.push_str("// Auto-generated by alef — DO NOT EDIT\n\n");
    out.push_str(&format!("package {};\n\n", package));

    // Required imports for Panama FFM
    out.push_str("import java.lang.foreign.*;\n");
    out.push_str("import java.lang.invoke.MethodHandle;\n");
    out.push_str("import java.lang.invoke.MethodHandles;\n");
    out.push_str("import java.lang.invoke.MethodType;\n\n");

    // Service class
    let class_name = &service.name;
    let service_snake = service.name.to_snake_case();
    let ffi_prefix = config.ffi_prefix().to_lowercase();

    out.push_str("/**\n");
    out.push_str(&format!(" * Service wrapper for {} using Panama FFM.\n", service.name));
    out.push_str(" *\n");
    out.push_str(" * Binds to C FFI symbols:\n");
    out.push_str(&format!(
        " * - {}_{}_new() -> opaque handle\n",
        ffi_prefix, service_snake
    ));
    out.push_str(&format!(" * - {}_{}_free(opaque)\n", ffi_prefix, service_snake));
    for reg in &service.registrations {
        out.push_str(&format!(
            " * - {}_{}_register_{} (handler registration)\n",
            ffi_prefix,
            service_snake,
            reg.method.to_snake_case()
        ));
    }
    for ep in &service.entrypoints {
        out.push_str(&format!(
            " * - {}_{}_ep_{} (entrypoint)\n",
            ffi_prefix,
            service_snake,
            ep.method.to_snake_case()
        ));
    }
    out.push_str(" */\n");
    out.push_str(&format!("public class {} implements AutoCloseable {{\n\n", class_name));

    // Private fields: owner handle + arena for callback lifetime management
    out.push_str("    private MemorySegment ownerHandle;\n");
    out.push_str("    private final Arena arena = Arena.ofConfined();\n\n");

    // Static field to hold the NativeLinker (Panama FFM pattern)
    out.push_str("    private static final Linker LINKER = Linker.nativeLinker();\n");
    out.push_str("    private static final SymbolLookup LOOKUP = SymbolLookup.loaderLookup();\n\n");

    // Constructor: invoke {prefix}_{service}_new() downcall
    {
        out.push_str("    /**\n");
        out.push_str(&format!("     * Create a new {}.\n", service.name));
        out.push_str(&format!(
            "     * Invokes C FFI: {}_{}_new()\n",
            ffi_prefix, service_snake
        ));
        out.push_str("     */\n");
        out.push_str(&format!("    public {}() {{\n", class_name));

        out.push_str("        try {\n");
        out.push_str(&format!(
            "            MemorySegment addr = LOOKUP.find(\"{}_{}_new\").orElseThrow();\n",
            ffi_prefix, service_snake
        ));
        out.push_str("            FunctionDescriptor desc = FunctionDescriptor.of(ValueLayout.ADDRESS);\n");
        out.push_str("            MethodHandle handle = LINKER.downcallHandle(addr, desc);\n");
        out.push_str("            this.ownerHandle = (MemorySegment) handle.invoke();\n");
        out.push_str("            if (this.ownerHandle == null) {\n");
        out.push_str("                throw new RuntimeException(\"Failed to allocate service instance\");\n");
        out.push_str("            }\n");
        out.push_str("        } catch (Throwable e) {\n");
        out.push_str("            throw new RuntimeException(\"Failed to invoke constructor\", e);\n");
        out.push_str("        }\n");
        out.push_str("    }\n\n");
    }

    // Registration methods: build upcall stub from handler, invoke register downcall
    for reg in &service.registrations {
        let reg_method = &reg.method;
        let reg_method_camel = reg_method.to_upper_camel_case();
        let reg_method_snake = reg_method.to_snake_case();

        out.push_str("    /**\n");
        out.push_str(&format!("     * Register a handler for {}.\n", reg_method));
        out.push_str(&format!(
            "     * Invokes C FFI: {}_{}_register_{}\n",
            ffi_prefix, service_snake, reg_method_snake
        ));
        out.push_str("     *\n");
        out.push_str("     * @param handler functional interface receiving JSON request, returning JSON response\n");

        for meta_param in &reg.metadata_params {
            out.push_str(&format!(
                "     * @param {} {}\n",
                meta_param.name.to_lower_camel_case(),
                java_type_for_metadata(&meta_param.ty)
            ));
        }
        out.push_str("     * @return 0 on success, non-zero error code on failure\n");
        out.push_str("     */\n");

        out.push_str(&format!(
            "    public int register{}{}(Callable handler",
            class_name, reg_method_camel
        ));

        // Add metadata parameters
        for meta_param in &reg.metadata_params {
            let java_type = java_type_for_metadata(&meta_param.ty);
            let param_name = meta_param.name.to_lower_camel_case();
            out.push_str(&format!(", {} {}", java_type, param_name));
        }

        out.push_str(") {\n");
        out.push_str("        try {\n");

        // Build upcall stub from the handler functional interface
        // The C FFI callback signature is: fn(*mut c_void, *const c_char) -> *mut c_char
        // We wrap handler.handle() in a MethodHandle that marshals C strings to/from Java
        out.push_str("            // Wrap Java handler in an upcall stub for C to invoke\n");
        out.push_str("            MethodHandles.Lookup lookup = MethodHandles.lookup();\n");
        out.push_str("            MethodHandle mh = lookup.findVirtual(Callable.class, \"handle\",\n");
        out.push_str("                MethodType.methodType(String.class, String.class));\n");
        out.push_str("            MethodHandle boundMh = mh.bindTo(handler);\n\n");

        out.push_str("            // Build adapter: (context: ADDRESS, request: ADDRESS) -> ADDRESS\n");
        out.push_str(
            "            // Marshalling: C *const char -> Java String -> handler.handle() -> String -> C *mut char\n",
        );
        out.push_str("            // For now, simplified: upcall receives pointers, marshalls to/from Java strings\n");
        out.push_str("            FunctionDescriptor upcallDesc = FunctionDescriptor.of(\n");
        out.push_str("                ValueLayout.ADDRESS,  // return: *mut c_char\n");
        out.push_str("                ValueLayout.ADDRESS,  // param 0: *mut c_void (context)\n");
        out.push_str("                ValueLayout.ADDRESS   // param 1: *const c_char (request JSON)\n");
        out.push_str("            );\n");

        out.push_str("            // Create a wrapper MethodHandle that adapts Java strings to C pointers\n");
        out.push_str("            // This is a simplified binding; production code should marshal strings properly\n");
        out.push_str("            MemorySegment upcallStub = LINKER.upcallStub(boundMh, upcallDesc, arena);\n\n");

        out.push_str("            // Get register downcall handle\n");
        out.push_str(&format!(
            "            MemorySegment regAddr = LOOKUP.find(\"{}_{}_register_{}\").orElseThrow();\n",
            ffi_prefix, service_snake, reg_method_snake
        ));

        // Build function descriptor for register call
        out.push_str("            FunctionDescriptor regDesc = FunctionDescriptor.of(\n");
        out.push_str("                ValueLayout.JAVA_INT,  // return: int\n");
        out.push_str("                ValueLayout.ADDRESS,   // owner: *mut opaque\n");
        out.push_str("                ValueLayout.ADDRESS    // callback: upcall stub\n");

        // Add metadata params to descriptor
        for meta_param in &reg.metadata_params {
            match &meta_param.ty {
                TypeRef::String => {
                    out.push_str(",\n                ValueLayout.ADDRESS    // ");
                }
                TypeRef::Primitive(p) => {
                    use crate::core::ir::PrimitiveType;
                    let layout = match p {
                        PrimitiveType::Bool => "ValueLayout.JAVA_BOOLEAN",
                        PrimitiveType::U8 | PrimitiveType::I8 => "ValueLayout.JAVA_BYTE",
                        PrimitiveType::U16 | PrimitiveType::I16 => "ValueLayout.JAVA_SHORT",
                        PrimitiveType::U32 | PrimitiveType::I32 => "ValueLayout.JAVA_INT",
                        PrimitiveType::U64 | PrimitiveType::I64 => "ValueLayout.JAVA_LONG",
                        PrimitiveType::F32 => "ValueLayout.JAVA_FLOAT",
                        PrimitiveType::F64 => "ValueLayout.JAVA_DOUBLE",
                        PrimitiveType::Usize | PrimitiveType::Isize => "ValueLayout.ADDRESS",
                    };
                    out.push_str(&format!(",\n                {}    // ", layout));
                }
                _ => {
                    out.push_str(",\n                ValueLayout.ADDRESS    // ");
                }
            }
            out.push_str(&meta_param.name.to_lower_camel_case());
            out.push_str(" param\n");
        }

        out.push_str("            );\n");
        out.push_str("            MethodHandle regHandle = LINKER.downcallHandle(regAddr, regDesc);\n\n");

        // Invoke the register function
        out.push_str("            Object[] args = new Object[] {\n");
        out.push_str("                ownerHandle,     // owner\n");
        out.push_str("                upcallStub       // callback\n");

        for meta_param in &reg.metadata_params {
            let param_name = meta_param.name.to_lower_camel_case();
            out.push_str(&format!(",\n                {}    // metadata", param_name));
        }

        out.push_str("\n            };\n");
        out.push_str("            return (int) regHandle.invokeExact(args);\n");
        out.push_str("        } catch (Throwable e) {\n");
        out.push_str("            throw new RuntimeException(\"Failed to register handler\", e);\n");
        out.push_str("        }\n");
        out.push_str("    }\n\n");
    }

    // Entrypoint methods
    for ep in &service.entrypoints {
        let ep_method = &ep.method;
        let ep_method_snake = ep_method.to_snake_case();

        out.push_str("    /**\n");
        out.push_str(&format!("     * {}.\n", ep_method));
        out.push_str(&format!(
            "     * Invokes C FFI: {}_{}_ep_{}\n",
            ffi_prefix, service_snake, ep_method_snake
        ));
        out.push_str("     */\n");

        let return_type = match ep.kind {
            EntrypointKind::Run => "void",
            EntrypointKind::Finalize => "long",
        };

        out.push_str(&format!("    public {} {}(", return_type, ep_method));

        // Add entrypoint parameters
        for (i, param) in ep.params.iter().enumerate() {
            if i > 0 {
                out.push_str(", ");
            }
            let java_type = java_type_for_metadata(&param.ty);
            let param_name = param.name.to_lower_camel_case();
            out.push_str(&format!("{} {}", java_type, param_name));
        }

        out.push_str(") {\n");
        out.push_str("        try {\n");

        out.push_str(&format!(
            "            MemorySegment epAddr = LOOKUP.find(\"{}_{}_ep_{}\").orElseThrow();\n",
            ffi_prefix, service_snake, ep_method_snake
        ));

        // Build FunctionDescriptor for entrypoint
        out.push_str("            FunctionDescriptor epDesc = FunctionDescriptor.of(\n");
        match ep.kind {
            EntrypointKind::Run => {
                out.push_str("                ValueLayout.JAVA_VOID,   // return void\n");
            }
            EntrypointKind::Finalize => {
                out.push_str("                ValueLayout.JAVA_LONG,   // return long\n");
            }
        }
        out.push_str("                ValueLayout.ADDRESS       // owner: *mut opaque\n");
        out.push_str("            );\n");

        out.push_str("            MethodHandle epHandle = LINKER.downcallHandle(epAddr, epDesc);\n");

        match ep.kind {
            EntrypointKind::Run => {
                out.push_str("            epHandle.invoke(ownerHandle");
            }
            EntrypointKind::Finalize => {
                out.push_str("            return (long) epHandle.invoke(ownerHandle");
            }
        }

        // Pass entrypoint arguments
        for param in &ep.params {
            let param_name = param.name.to_lower_camel_case();
            out.push_str(&format!(", {}", param_name));
        }

        out.push_str(");\n");
        out.push_str("        } catch (Throwable e) {\n");
        out.push_str("            throw new RuntimeException(\"Failed to invoke entrypoint\", e);\n");
        out.push_str("        }\n");
        out.push_str("    }\n\n");
    }

    // AutoCloseable implementation
    out.push_str("    @Override\n");
    out.push_str("    public void close() {\n");
    out.push_str("        try {\n");
    out.push_str(&format!(
        "            MemorySegment freeAddr = LOOKUP.find(\"{}_{}_free\").orElseThrow();\n",
        ffi_prefix, service_snake
    ));
    out.push_str("            FunctionDescriptor freeDesc = FunctionDescriptor.ofVoid(ValueLayout.ADDRESS);\n");
    out.push_str("            MethodHandle freeHandle = LINKER.downcallHandle(freeAddr, freeDesc);\n");
    out.push_str("            if (ownerHandle != null) {\n");
    out.push_str("                freeHandle.invoke(ownerHandle);\n");
    out.push_str("                ownerHandle = null;\n");
    out.push_str("            }\n");
    out.push_str("            arena.close();\n");
    out.push_str("        } catch (Throwable e) {\n");
    out.push_str("            throw new RuntimeException(\"Failed to close service\", e);\n");
    out.push_str("        }\n");
    out.push_str("    }\n");

    out.push_str("}\n");

    out
}

/// Generate the @FunctionalInterface Callable interface.
///
/// A simple interface that handlers must implement to be passed to registration methods.
fn gen_callable_interface(package: &str) -> String {
    let mut out = String::new();

    out.push_str("// Auto-generated by alef — DO NOT EDIT\n\n");
    out.push_str(&format!("package {};\n\n", package));

    out.push_str("/**\n");
    out.push_str(" * Functional interface for service handlers.\n");
    out.push_str(" *\n");
    out.push_str(" * Implementations receive a JSON request string and return a JSON response string.\n");
    out.push_str(" */\n");
    out.push_str("@FunctionalInterface\n");
    out.push_str("public interface Callable {\n");
    out.push_str("    /**\n");
    out.push_str("     * Handle a request.\n");
    out.push_str("     *\n");
    out.push_str("     * @param request JSON request string\n");
    out.push_str("     * @return JSON response string\n");
    out.push_str("     */\n");
    out.push_str("    String handle(String request);\n");
    out.push_str("}\n");

    out
}

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

/// Generate all service-API files for the Java backend.
///
/// Returns Java source files using Panama FFM:
/// - One service class per [`ServiceDef`] (Panama downcalls + upcalls)
/// - One Callable interface (shared)
pub fn generate(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
    if api.services.is_empty() {
        return Ok(vec![]);
    }

    let package = config.java_package();
    let package_path = package.replace('.', "/");

    let output_dir = config
        .output_for("java")
        .map(|p| p.to_string_lossy().into_owned())
        .unwrap_or_else(|| "packages/java/src/main/java/".to_string());

    let base_path = if output_dir.ends_with(&package_path) || output_dir.ends_with(&format!("{}/", package_path)) {
        PathBuf::from(&output_dir)
    } else {
        PathBuf::from(&output_dir).join(&package_path)
    };

    let mut files = Vec::new();

    // Generate one service class per service (Panama FFM downcalls + upcalls)
    for service in &api.services {
        let service_class = gen_service_class(api, service, &package, config);
        files.push(GeneratedFile {
            path: base_path.join(format!("{}.java", service.name)),
            content: service_class,
            generated_header: false, // Header already included
        });
    }

    // Generate Callable interface (once, shared across all services)
    files.push(GeneratedFile {
        path: base_path.join("Callable.java"),
        content: gen_callable_interface(&package),
        generated_header: false,
    });

    Ok(files)
}

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

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

    /// Construct a minimal but realistic [`ApiSurface`] that exercises:
    /// - A service with a constructor, one registration, 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 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: None,
            doc: "Register a request handler.".to_owned(),
        };

        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![],
            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(crate::core::ir::ReceiverKind::Ref),
            sanitized: false,
            trait_source: None,
            returns_ref: false,
            returns_cow: false,
            return_newtype_wrapper: None,
            has_default_impl: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
        };

        let 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()),
            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()
        }
    }

    fn make_test_config() -> ResolvedCrateConfig {
        // `ffi_prefix()` derives from `name` (hyphens → underscores) when `[ffi] prefix`
        // is unset, so `test-crate` yields the prefix `test_crate`.
        ResolvedCrateConfig {
            name: "test-crate".to_owned(),
            ..ResolvedCrateConfig::default()
        }
    }

    #[test]
    fn java_class_uses_panama_ffm() {
        let surface = make_fixture_surface();
        let config = make_test_config();
        let java = gen_service_class(&surface, &surface.services[0], "com.example", &config);

        // Verify Panama FFM imports and patterns
        assert!(java.contains("import java.lang.foreign.*;"), "should import Panama FFM");
        assert!(java.contains("Linker.nativeLinker()"), "should use Linker");
        assert!(java.contains("downcallHandle"), "should use downcalls");
        assert!(java.contains("SymbolLookup"), "should lookup C symbols");
        assert!(java.contains("FunctionDescriptor"), "should build function descriptors");
        assert!(java.contains("MemorySegment"), "should use MemorySegment");
        assert!(java.contains("Arena"), "should use Arena for lifetime management");
    }

    #[test]
    fn java_class_contains_service_class() {
        let surface = make_fixture_surface();
        let config = make_test_config();
        let java = gen_service_class(&surface, &surface.services[0], "com.example", &config);

        assert!(java.contains("public class TestService"));
        assert!(java.contains("implements AutoCloseable"));
        assert!(java.contains("private MemorySegment ownerHandle"));
    }

    #[test]
    fn java_class_constructor_uses_downcall() {
        let surface = make_fixture_surface();
        let config = make_test_config();
        let java = gen_service_class(&surface, &surface.services[0], "com.example", &config);

        assert!(java.contains("public TestService()"));
        assert!(
            java.contains("test_crate_test_service_new"),
            "constructor should bind to C symbol"
        );
        assert!(
            java.contains("LINKER.downcallHandle"),
            "constructor should use downcall"
        );
    }

    #[test]
    fn java_class_contains_upcall_stub_for_handler() {
        let surface = make_fixture_surface();
        let config = make_test_config();
        let java = gen_service_class(&surface, &surface.services[0], "com.example", &config);

        assert!(
            java.contains("LINKER.upcallStub"),
            "registration should build upcall stub for handler"
        );
        assert!(java.contains("MethodHandle"), "should use MethodHandle to wrap handler");
    }

    #[test]
    fn java_class_registration_binds_to_c_symbol() {
        let surface = make_fixture_surface();
        let config = make_test_config();
        let java = gen_service_class(&surface, &surface.services[0], "com.example", &config);

        assert!(
            java.contains("test_crate_test_service_register_add_handler"),
            "registration should bind to exact C FFI symbol"
        );
    }

    #[test]
    fn java_class_entrypoint_uses_downcall() {
        let surface = make_fixture_surface();
        let config = make_test_config();
        let java = gen_service_class(&surface, &surface.services[0], "com.example", &config);

        assert!(java.contains("public void run(String addr)"));
        assert!(
            java.contains("test_crate_test_service_ep_run"),
            "entrypoint should bind to C symbol"
        );
        assert!(java.contains("LINKER.downcallHandle"), "entrypoint should use downcall");
    }

    #[test]
    fn java_class_close_frees_via_downcall() {
        let surface = make_fixture_surface();
        let config = make_test_config();
        let java = gen_service_class(&surface, &surface.services[0], "com.example", &config);

        assert!(java.contains("@Override"));
        assert!(java.contains("public void close()"));
        assert!(
            java.contains("test_crate_test_service_free"),
            "close should bind to C symbol"
        );
        assert!(java.contains("LINKER.downcallHandle"), "close should use downcall");
        assert!(java.contains("arena.close()"), "arena lifetime should be managed");
    }

    #[test]
    fn java_class_no_native_method_declarations() {
        let surface = make_fixture_surface();
        let config = make_test_config();
        let java = gen_service_class(&surface, &surface.services[0], "com.example", &config);

        assert!(
            !java.contains("native "),
            "should not contain JNI native method declarations"
        );
        assert!(
            !java.contains("System.loadLibrary"),
            "should not load library (Panama manages it)"
        );
        assert!(!java.contains("Java_"), "should not contain Java_ JNI symbols");
    }

    #[test]
    fn callable_interface_is_functional() {
        let iface = gen_callable_interface("com.example");

        assert!(iface.contains("@FunctionalInterface"));
        assert!(iface.contains("public interface Callable"));
        assert!(iface.contains("String handle(String request)"));
    }

    #[test]
    fn generate_returns_service_and_callable() {
        let surface = make_fixture_surface();
        let config = make_test_config();

        let files = generate(&surface, &config).expect("generate should not fail");
        assert!(files.len() >= 2, "expected at least service class + Callable interface");

        let has_service_class = files
            .iter()
            .any(|f| f.path.to_string_lossy().contains("TestService.java"));
        let has_callable = files.iter().any(|f| f.path.to_string_lossy().contains("Callable.java"));

        assert!(has_service_class, "expected TestService.java");
        assert!(has_callable, "expected Callable.java");
    }

    #[test]
    fn generate_returns_empty_for_no_services() {
        let surface = ApiSurface::default();
        let config = make_test_config();

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

    #[test]
    fn java_class_passes_all_metadata_params() {
        let mut surface = make_fixture_surface();
        let reg = &mut surface.services[0].registrations[0];

        // Add more metadata parameters
        reg.metadata_params.push(ParamDef {
            name: "method".to_owned(),
            ty: TypeRef::String,
            optional: false,
            default: None,
            ..ParamDef::default()
        });
        reg.metadata_params.push(ParamDef {
            name: "priority".to_owned(),
            ty: TypeRef::Primitive(crate::core::ir::PrimitiveType::I32),
            optional: false,
            default: None,
            ..ParamDef::default()
        });

        let config = make_test_config();
        let java = gen_service_class(&surface, &surface.services[0], "com.example", &config);

        // Check that the method signature includes all params
        assert!(
            java.contains("public int registerTestServiceAddHandler(Callable handler, String path"),
            "registration method must include all metadata parameters"
        );

        // Check that metadata params are part of function descriptor
        assert!(
            java.contains("ValueLayout.ADDRESS") || java.contains("ValueLayout.JAVA_INT"),
            "registration should build FunctionDescriptor with metadata param layouts"
        );
    }
}
