Skip to main content

alef_e2e/codegen/
c.rs

1//! C e2e test generator using assert.h and a Makefile.
2//!
3//! Generates `e2e/c/Makefile`, per-category `test_{category}.c` files,
4//! a `main.c` test runner, a `test_runner.h` header, and a
5//! `download_ffi.sh` script for downloading prebuilt FFI libraries from
6//! GitHub releases.
7
8use crate::config::{CallConfig, E2eConfig};
9use crate::escape::{escape_c, sanitize_filename, sanitize_ident};
10use crate::field_access::FieldResolver;
11use crate::fixture::{Assertion, Fixture, FixtureGroup};
12use alef_core::backend::GeneratedFile;
13use alef_core::config::ResolvedCrateConfig;
14use alef_core::hash::{self, CommentStyle};
15use anyhow::Result;
16use heck::{ToPascalCase, ToSnakeCase};
17use std::collections::{HashMap, HashSet};
18use std::fmt::Write as FmtWrite;
19use std::path::PathBuf;
20
21use super::E2eCodegen;
22
23/// C e2e code generator.
24pub struct CCodegen;
25
26/// Returns true when `t` is a primitive C scalar type (uint64_t, int32_t, double,
27/// etc.) that should be emitted as a typed local variable rather than a heap
28/// `char*` accessor result.
29fn is_primitive_c_type(t: &str) -> bool {
30    matches!(
31        t,
32        "uint8_t"
33            | "uint16_t"
34            | "uint32_t"
35            | "uint64_t"
36            | "int8_t"
37            | "int16_t"
38            | "int32_t"
39            | "int64_t"
40            | "uintptr_t"
41            | "intptr_t"
42            | "size_t"
43            | "ssize_t"
44            | "double"
45            | "float"
46            | "bool"
47            | "int"
48    )
49}
50
51/// Infer the opaque-handle PascalCase return type for a bare-field accessor.
52///
53/// Returns `Some(pascal_type)` when the accessor `{prefix}_{parent}_{field}`
54/// returns a pointer to an opaque struct (e.g. `LITERLLMUsage*`) rather than
55/// a `char*` or primitive scalar.
56///
57/// Detection strategy:
58/// 1. Direct lookup `fields_c_types["{parent}.{field}"]` — if present and
59///    NOT a primitive AND NOT `char*`, treat as an opaque handle of that
60///    PascalCase type.
61/// 2. Inferred lookup — when ANY key in `fields_c_types` starts with
62///    `"{field}."` (the snake_case of `field` as a parent type), the field
63///    must be a struct whose nested fields are mapped. Default the struct
64///    type to `field.to_pascal_case()`. This mirrors the fallback used by
65///    `emit_nested_accessor` for intermediate segments.
66///
67/// Returns `None` when the field looks like a `char*` string accessor.
68fn infer_opaque_handle_type(
69    fields_c_types: &HashMap<String, String>,
70    parent_snake_type: &str,
71    field_snake: &str,
72) -> Option<String> {
73    let lookup_key = format!("{parent_snake_type}.{field_snake}");
74    if let Some(t) = fields_c_types.get(&lookup_key) {
75        if !is_primitive_c_type(t) && t != "char*" {
76            return Some(t.clone());
77        }
78        // Primitive or explicit char* — caller handles those paths.
79        return None;
80    }
81    // Inferred: nested keys exist with `field_snake` as the parent type prefix.
82    let nested_prefix = format!("{field_snake}.");
83    if fields_c_types.keys().any(|k| k.starts_with(&nested_prefix)) {
84        return Some(field_snake.to_pascal_case());
85    }
86    None
87}
88
89/// Try to emit an enum-aware field accessor: when `raw_field`/`resolved_field`
90/// is registered in `fields_enum` AND `fields_c_types[parent.field]` resolves
91/// to a non-primitive PascalCase type name, treat the accessor return as an
92/// opaque enum pointer and convert it to `char*` via the FFI's
93/// `{prefix}_{enum_snake}_to_string` accessor.
94///
95/// Without this, the C codegen would default-declare the accessor result as
96/// `char* status = {prefix}_batch_object_status(result);` and string-compare
97/// it — but the FFI returns `LITERLLMBatchStatus*` (an opaque enum struct
98/// pointer), not a C string. The mismatch causes immediate `Abort trap: 6` /
99/// `strcmp(NULL,...)` failures in every assertion that targets an enum field.
100///
101/// Returns `true` when an accessor was emitted (caller must NOT emit the
102/// default `char*` declaration). When emitted, the opaque-enum handle is
103/// pushed to `intermediate_handles` so the existing cleanup loop frees it via
104/// `{prefix}_{enum_snake}_free(...)` after the test body runs.
105#[allow(clippy::too_many_arguments)]
106fn try_emit_enum_accessor(
107    out: &mut String,
108    prefix: &str,
109    prefix_upper: &str,
110    raw_field: &str,
111    resolved_field: &str,
112    parent_snake_type: &str,
113    accessor_fn: &str,
114    parent_handle: &str,
115    local_var: &str,
116    fields_c_types: &HashMap<String, String>,
117    fields_enum: &HashSet<String>,
118    intermediate_handles: &mut Vec<(String, String)>,
119) -> bool {
120    if !(fields_enum.contains(raw_field) || fields_enum.contains(resolved_field)) {
121        return false;
122    }
123    let lookup_key = format!("{parent_snake_type}.{resolved_field}");
124    let Some(enum_pascal) = fields_c_types.get(&lookup_key) else {
125        return false;
126    };
127    if is_primitive_c_type(enum_pascal) || enum_pascal == "char*" {
128        return false;
129    }
130    let enum_snake = enum_pascal.to_snake_case();
131    let handle_var = format!("{local_var}_handle");
132    let _ = writeln!(
133        out,
134        "    {prefix_upper}{enum_pascal}* {handle_var} = {accessor_fn}({parent_handle});"
135    );
136    let _ = writeln!(out, "    assert({handle_var} != NULL);");
137    let _ = writeln!(
138        out,
139        "    char* {local_var} = {prefix}_{enum_snake}_to_string({handle_var});"
140    );
141    intermediate_handles.push((handle_var, enum_snake));
142    true
143}
144
145impl E2eCodegen for CCodegen {
146    fn generate(
147        &self,
148        groups: &[FixtureGroup],
149        e2e_config: &E2eConfig,
150        config: &ResolvedCrateConfig,
151        _type_defs: &[alef_core::ir::TypeDef],
152        _enums: &[alef_core::ir::EnumDef],
153    ) -> Result<Vec<GeneratedFile>> {
154        let lang = self.language_name();
155        let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
156
157        let mut files = Vec::new();
158
159        // Resolve default call config with overrides.
160        let call = &e2e_config.call;
161        let overrides = call.overrides.get(lang);
162        let result_var = &call.result_var;
163        let prefix = overrides
164            .and_then(|o| o.prefix.as_ref())
165            .cloned()
166            .or_else(|| config.ffi.as_ref().and_then(|ffi| ffi.prefix.as_ref()).cloned())
167            .unwrap_or_default();
168        let header = overrides
169            .and_then(|o| o.header.as_ref())
170            .cloned()
171            .unwrap_or_else(|| config.ffi_header_name());
172
173        // Resolve package config.
174        let c_pkg = e2e_config.resolve_package("c");
175        let lib_name = c_pkg
176            .as_ref()
177            .and_then(|p| p.name.as_ref())
178            .cloned()
179            .unwrap_or_else(|| config.ffi_lib_name());
180
181        // Filter active groups (with non-skipped fixtures).
182        let active_groups: Vec<(&FixtureGroup, Vec<&Fixture>)> = groups
183            .iter()
184            .filter_map(|group| {
185                let active: Vec<&Fixture> = group
186                    .fixtures
187                    .iter()
188                    .filter(|f| super::should_include_fixture(f, lang, e2e_config))
189                    .filter(|f| f.visitor.is_none())
190                    .collect();
191                if active.is_empty() { None } else { Some((group, active)) }
192            })
193            .collect();
194
195        // Resolve FFI crate path for local repo builds.
196        // Default to `../../crates/{name}-ffi` derived from the crate name so that
197        // projects like `liter-llm` resolve to `../../crates/liter-llm-ffi/include/`
198        // rather than the generic (incorrect) `../../crates/ffi`.
199        // When `[crates.output] ffi` is set explicitly, derive the crate path from
200        // that value so that renamed FFI crates (e.g. `ts-pack-core-ffi`) resolve
201        // correctly without any hardcoded special cases.
202        let ffi_crate_path = c_pkg
203            .as_ref()
204            .and_then(|p| p.path.as_ref())
205            .cloned()
206            .unwrap_or_else(|| config.ffi_crate_path());
207
208        // Generate Makefile.
209        let category_names: Vec<String> = active_groups
210            .iter()
211            .map(|(g, _)| sanitize_filename(&g.category))
212            .collect();
213        let needs_mock_server = active_groups
214            .iter()
215            .flat_map(|(_, fixtures)| fixtures.iter())
216            .any(|f| f.needs_mock_server());
217        files.push(GeneratedFile {
218            path: output_base.join("Makefile"),
219            content: render_makefile(&category_names, &header, &ffi_crate_path, &lib_name, needs_mock_server),
220            generated_header: true,
221        });
222
223        // Generate download_ffi.sh for downloading prebuilt FFI from GitHub releases.
224        let github_repo = config.github_repo();
225        let version = config.resolved_version().unwrap_or_else(|| "0.0.0".to_string());
226        let ffi_pkg_name = e2e_config
227            .registry
228            .packages
229            .get("c")
230            .and_then(|p| p.name.as_ref())
231            .cloned()
232            .unwrap_or_else(|| lib_name.clone());
233        files.push(GeneratedFile {
234            path: output_base.join("download_ffi.sh"),
235            content: render_download_script(&github_repo, &version, &ffi_pkg_name),
236            generated_header: true,
237        });
238
239        // Generate test_runner.h.
240        files.push(GeneratedFile {
241            path: output_base.join("test_runner.h"),
242            content: render_test_runner_header(&active_groups),
243            generated_header: true,
244        });
245
246        // Generate main.c.
247        files.push(GeneratedFile {
248            path: output_base.join("main.c"),
249            content: render_main_c(&active_groups),
250            generated_header: true,
251        });
252
253        let field_resolver = FieldResolver::new(
254            &e2e_config.fields,
255            &e2e_config.fields_optional,
256            &e2e_config.result_fields,
257            &e2e_config.fields_array,
258            &std::collections::HashSet::new(),
259        );
260
261        // Generate per-category test files.
262        // Each fixture may reference a named call config (fixture.call), so we pass
263        // e2e_config to render_test_file so it can resolve per-fixture call settings.
264        for (group, active) in &active_groups {
265            let filename = format!("test_{}.c", sanitize_filename(&group.category));
266            let content = render_test_file(
267                &group.category,
268                active,
269                &header,
270                &prefix,
271                result_var,
272                e2e_config,
273                lang,
274                &field_resolver,
275            );
276            files.push(GeneratedFile {
277                path: output_base.join(filename),
278                content,
279                generated_header: true,
280            });
281        }
282
283        Ok(files)
284    }
285
286    fn language_name(&self) -> &'static str {
287        "c"
288    }
289}
290
291/// Resolve per-call-config C-specific settings for a given call config and lang.
292struct ResolvedCallInfo {
293    function_name: String,
294    result_type_name: String,
295    options_type_name: String,
296    client_factory: Option<String>,
297    args: Vec<crate::config::ArgMapping>,
298    raw_c_result_type: Option<String>,
299    c_free_fn: Option<String>,
300    c_engine_factory: Option<String>,
301    result_is_option: bool,
302    /// When `true`, the FFI signature for this method follows the byte-buffer
303    /// out-pointer pattern: `int32_t fn(this, req, uint8_t** out_ptr,
304    /// uintptr_t* out_len, uintptr_t* out_cap)`. The C codegen emits out-param
305    /// declarations, a status-code check, and `<prefix>_free_bytes` rather
306    /// than treating the result as an opaque response handle.
307    result_is_bytes: bool,
308    /// Per-language `extra_args` from call overrides — verbatim trailing
309    /// arguments appended after the configured `args`. The C codegen passes
310    /// `NULL` for absent optional pointers via this mechanism.
311    extra_args: Vec<String>,
312}
313
314fn resolve_call_info(call: &CallConfig, lang: &str) -> ResolvedCallInfo {
315    let overrides = call.overrides.get(lang);
316    let function_name = overrides
317        .and_then(|o| o.function.as_ref())
318        .cloned()
319        .unwrap_or_else(|| call.function.clone());
320    // Fall back to the *base* (non-C-overridden) function name when no explicit
321    // result_type is set.  Using the C-overridden name (e.g. "htm_convert") would
322    // produce a doubled-prefix type like `HTMHtmConvert*`; the base name
323    // ("convert") yields the correct `HTMConvert*` shape.
324    let result_type_name = overrides
325        .and_then(|o| o.result_type.as_ref())
326        .cloned()
327        .unwrap_or_else(|| call.function.to_pascal_case());
328    let options_type_name = overrides
329        .and_then(|o| o.options_type.as_deref())
330        .unwrap_or("ConversionOptions")
331        .to_string();
332    let client_factory = overrides.and_then(|o| o.client_factory.as_ref()).cloned();
333    let raw_c_result_type = overrides.and_then(|o| o.raw_c_result_type.clone());
334    let c_free_fn = overrides.and_then(|o| o.c_free_fn.clone());
335    let c_engine_factory = overrides.and_then(|o| o.c_engine_factory.clone());
336    let result_is_option = overrides
337        .and_then(|o| if o.result_is_option { Some(true) } else { None })
338        .unwrap_or(call.result_is_option);
339    // result_is_bytes is read from either the call-level config (preferred —
340    // the byte-buffer FFI shape is identical across languages that use the
341    // same FFI crate) or the per-language override (back-compat with the
342    // pattern used by Java / PHP / etc.).
343    let result_is_bytes = call.result_is_bytes || overrides.is_some_and(|o| o.result_is_bytes);
344    let extra_args = overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
345    ResolvedCallInfo {
346        function_name,
347        result_type_name,
348        options_type_name,
349        client_factory,
350        args: call.args.clone(),
351        raw_c_result_type,
352        c_free_fn,
353        c_engine_factory,
354        result_is_option,
355        result_is_bytes,
356        extra_args,
357    }
358}
359
360/// Resolve call info for a fixture, with fallback to default call's client_factory.
361///
362/// Named call configs (e.g. `[e2e.calls.embed]`) may not repeat the `client_factory`
363/// setting. We fall back to the default `[e2e.call]` override's client_factory so that
364/// all methods on the same client use the same pattern.
365fn resolve_fixture_call_info(fixture: &Fixture, e2e_config: &E2eConfig, lang: &str) -> ResolvedCallInfo {
366    let call = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
367    let mut info = resolve_call_info(call, lang);
368
369    let default_overrides = e2e_config.call.overrides.get(lang);
370
371    // Fallback: if the named call has no client_factory override, inherit from the
372    // default call config so all calls use the same client pattern.
373    if info.client_factory.is_none() {
374        if let Some(factory) = default_overrides.and_then(|o| o.client_factory.as_ref()) {
375            info.client_factory = Some(factory.clone());
376        }
377    }
378
379    // Fallback: if the named call has no c_engine_factory override, inherit from the
380    // default call config so all calls use the same engine pattern.
381    if info.c_engine_factory.is_none() {
382        if let Some(factory) = default_overrides.and_then(|o| o.c_engine_factory.as_ref()) {
383            info.c_engine_factory = Some(factory.clone());
384        }
385    }
386
387    info
388}
389
390fn render_makefile(
391    categories: &[String],
392    header_name: &str,
393    ffi_crate_path: &str,
394    lib_name: &str,
395    needs_mock_server: bool,
396) -> String {
397    let mut out = String::new();
398    out.push_str(&hash::header(CommentStyle::Hash));
399    let _ = writeln!(out, "CC = gcc");
400    let _ = writeln!(out, "FFI_DIR = ffi");
401    let _ = writeln!(out);
402
403    // Rust's cdylib output normalizes hyphens to underscores in the filename
404    // (e.g. crate "html-to-markdown-ffi" → "libhtml_to_markdown_ffi.dylib").
405    // The -l linker flag must therefore use the underscore form, while the
406    // pkg-config package name retains the original form (as declared in the .pc file).
407    let link_lib_name = lib_name.replace('-', "_");
408
409    // 3-path fallback: ffi/ (download script) -> local repo build -> pkg-config.
410    let _ = writeln!(out, "ifneq ($(wildcard $(FFI_DIR)/include/{header_name}),)");
411    let _ = writeln!(out, "    CFLAGS = -Wall -Wextra -I. -I$(FFI_DIR)/include");
412    let _ = writeln!(
413        out,
414        "    LDFLAGS = -L$(FFI_DIR)/lib -l{link_lib_name} -Wl,-rpath,$(FFI_DIR)/lib"
415    );
416    let _ = writeln!(out, "else ifneq ($(wildcard {ffi_crate_path}/include/{header_name}),)");
417    let _ = writeln!(out, "    CFLAGS = -Wall -Wextra -I. -I{ffi_crate_path}/include");
418    let _ = writeln!(
419        out,
420        "    LDFLAGS = -L../../target/release -l{link_lib_name} -Wl,-rpath,../../target/release"
421    );
422    let _ = writeln!(out, "else");
423    let _ = writeln!(
424        out,
425        "    CFLAGS = -Wall -Wextra -I. $(shell pkg-config --cflags {lib_name} 2>/dev/null)"
426    );
427    let _ = writeln!(out, "    LDFLAGS = $(shell pkg-config --libs {lib_name} 2>/dev/null)");
428    let _ = writeln!(out, "endif");
429    let _ = writeln!(out);
430
431    let src_files: Vec<String> = categories.iter().map(|c| format!("test_{c}.c")).collect();
432    let srcs = src_files.join(" ");
433
434    let _ = writeln!(out, "SRCS = main.c {srcs}");
435    let _ = writeln!(out, "TARGET = run_tests");
436    let _ = writeln!(out);
437    let _ = writeln!(out, ".PHONY: all clean test");
438    let _ = writeln!(out);
439    let _ = writeln!(out, "all: $(TARGET)");
440    let _ = writeln!(out);
441    let _ = writeln!(out, "$(TARGET): $(SRCS)");
442    let _ = writeln!(out, "\t$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)");
443    let _ = writeln!(out);
444
445    if !needs_mock_server {
446        // No fixtures require an HTTP mock backend; run the test binary directly.
447        let _ = writeln!(out, "test: $(TARGET)");
448        let _ = writeln!(out, "\t./$(TARGET)");
449        let _ = writeln!(out);
450        let _ = writeln!(out, "clean:");
451        let _ = writeln!(out, "\trm -f $(TARGET)");
452        return out;
453    }
454
455    // The `test:` target spawns the e2e mock-server binary, captures its
456    // assigned MOCK_SERVER_URL line on stdout, exports it for the test process,
457    // runs the suite, then tears the server down. This mirrors the per-language
458    // conftest/setup machinery used by Python, Ruby, Java, etc.
459    //
460    // The mock-server also emits MOCK_SERVERS={...json...} mapping fixture IDs to
461    // their per-fixture listener URLs (needed for fixtures like robots/sitemap that
462    // require host-root routes). We parse this with python3 and export
463    // MOCK_SERVER_<UPPER_ID> env vars so the test binary can look them up.
464    let _ = writeln!(out, "MOCK_SERVER_BIN ?= ../rust/target/release/mock-server");
465    let _ = writeln!(out, "FIXTURES_DIR ?= ../../fixtures");
466    let _ = writeln!(out);
467    let _ = writeln!(out, "test: $(TARGET)");
468    let _ = writeln!(out, "\t@if [ -n \"$$MOCK_SERVER_URL\" ]; then \\");
469    // When MOCK_SERVER_URL is already set (e.g. run under an existing mock-server
470    // process), also parse MOCK_SERVERS env var and export per-fixture vars.
471    let _ = writeln!(out, "\t\tif [ -n \"$$MOCK_SERVERS\" ]; then \\");
472    let _ = writeln!(
473        out,
474        "\t\t\teval $$(python3 -c \"import json,os; d=json.loads(os.environ.get('MOCK_SERVERS','{{}}')); print(' '.join('export MOCK_SERVER_'+k.upper()+'='+v for k,v in d.items()))\"); \\"
475    );
476    let _ = writeln!(out, "\t\tfi; \\");
477    let _ = writeln!(out, "\t\t./$(TARGET); \\");
478    let _ = writeln!(out, "\telse \\");
479    let _ = writeln!(out, "\t\tif [ ! -x \"$(MOCK_SERVER_BIN)\" ]; then \\");
480    let _ = writeln!(
481        out,
482        "\t\t\techo \"mock-server binary not found at $(MOCK_SERVER_BIN); run: cargo build -p mock-server --release\" >&2; \\"
483    );
484    let _ = writeln!(out, "\t\t\texit 1; \\");
485    let _ = writeln!(out, "\t\tfi; \\");
486    let _ = writeln!(out, "\t\trm -f mock_server.stdout mock_server.stdin; \\");
487    let _ = writeln!(out, "\t\tmkfifo mock_server.stdin; \\");
488    let _ = writeln!(
489        out,
490        "\t\t\"$(MOCK_SERVER_BIN)\" \"$(FIXTURES_DIR)\" <mock_server.stdin >mock_server.stdout 2>&1 & \\"
491    );
492    let _ = writeln!(out, "\t\tMOCK_PID=$$!; \\");
493    let _ = writeln!(out, "\t\texec 9>mock_server.stdin; \\");
494    let _ = writeln!(out, "\t\tMOCK_URL=\"\"; MOCK_SERVERS_JSON=\"\"; \\");
495    // Wait until MOCK_SERVER_URL appears in stdout (server is ready), bail after 5 s.
496    let _ = writeln!(out, "\t\tfor _ in $$(seq 1 100); do \\");
497    let _ = writeln!(out, "\t\t\tif [ -s mock_server.stdout ]; then \\");
498    let _ = writeln!(
499        out,
500        "\t\t\t\tMOCK_URL=$$(grep -o 'MOCK_SERVER_URL=[^ ]*' mock_server.stdout | head -1 | cut -d= -f2); \\"
501    );
502    let _ = writeln!(out, "\t\t\t\tif [ -n \"$$MOCK_URL\" ]; then break; fi; \\");
503    let _ = writeln!(out, "\t\t\tfi; \\");
504    let _ = writeln!(out, "\t\t\tsleep 0.05; \\");
505    let _ = writeln!(out, "\t\tdone; \\");
506    // MOCK_SERVERS line is printed after MOCK_SERVER_URL; give it a short extra read.
507    let _ = writeln!(
508        out,
509        "\t\tMOCK_SERVERS_JSON=$$(grep -o 'MOCK_SERVERS={{.*}}' mock_server.stdout | head -1 | cut -d= -f2-); \\"
510    );
511    let _ = writeln!(
512        out,
513        "\t\tif [ -z \"$$MOCK_URL\" ]; then echo 'failed to start mock-server' >&2; cat mock_server.stdout >&2; kill $$MOCK_PID 2>/dev/null || true; exit 1; fi; \\"
514    );
515    // Export per-fixture MOCK_SERVER_<UPPER_ID> env vars from the JSON map.
516    let _ = writeln!(
517        out,
518        "\t\tif [ -n \"$$MOCK_SERVERS_JSON\" ] && command -v python3 >/dev/null 2>&1; then \\"
519    );
520    let _ = writeln!(
521        out,
522        "\t\t\teval $$(python3 -c \"import json,sys; d=json.loads(sys.argv[1]); print(' '.join('export MOCK_SERVER_{{}}={{}}'.format(k.upper(),v) for k,v in d.items()))\" \"$$MOCK_SERVERS_JSON\"); \\"
523    );
524    let _ = writeln!(out, "\t\tfi; \\");
525    let _ = writeln!(out, "\t\tMOCK_SERVER_URL=\"$$MOCK_URL\" ./$(TARGET); STATUS=$$?; \\");
526    let _ = writeln!(out, "\t\texec 9>&-; \\");
527    let _ = writeln!(out, "\t\tkill $$MOCK_PID 2>/dev/null || true; \\");
528    let _ = writeln!(out, "\t\trm -f mock_server.stdout mock_server.stdin; \\");
529    let _ = writeln!(out, "\t\texit $$STATUS; \\");
530    let _ = writeln!(out, "\tfi");
531    let _ = writeln!(out);
532    let _ = writeln!(out, "clean:");
533    let _ = writeln!(out, "\trm -f $(TARGET) mock_server.stdout mock_server.stdin");
534    out
535}
536
537fn render_download_script(github_repo: &str, version: &str, ffi_pkg_name: &str) -> String {
538    let mut out = String::new();
539    let _ = writeln!(out, "#!/usr/bin/env bash");
540    out.push_str(&hash::header(CommentStyle::Hash));
541    let _ = writeln!(out, "set -euo pipefail");
542    let _ = writeln!(out);
543    let _ = writeln!(out, "REPO_URL=\"{github_repo}\"");
544    let _ = writeln!(out, "VERSION=\"{version}\"");
545    let _ = writeln!(out, "FFI_PKG_NAME=\"{ffi_pkg_name}\"");
546    let _ = writeln!(out, "FFI_DIR=\"ffi\"");
547    let _ = writeln!(out);
548    let _ = writeln!(out, "# Detect OS and architecture.");
549    let _ = writeln!(out, "OS=\"$(uname -s | tr '[:upper:]' '[:lower:]')\"");
550    let _ = writeln!(out, "ARCH=\"$(uname -m)\"");
551    let _ = writeln!(out);
552    let _ = writeln!(out, "case \"$ARCH\" in");
553    let _ = writeln!(out, "x86_64 | amd64) ARCH=\"x86_64\" ;;");
554    let _ = writeln!(out, "arm64 | aarch64) ARCH=\"aarch64\" ;;");
555    let _ = writeln!(out, "*)");
556    let _ = writeln!(out, "  echo \"Unsupported architecture: $ARCH\" >&2");
557    let _ = writeln!(out, "  exit 1");
558    let _ = writeln!(out, "  ;;");
559    let _ = writeln!(out, "esac");
560    let _ = writeln!(out);
561    let _ = writeln!(out, "case \"$OS\" in");
562    let _ = writeln!(out, "linux) TRIPLE=\"${{ARCH}}-unknown-linux-gnu\" ;;");
563    let _ = writeln!(out, "darwin) TRIPLE=\"${{ARCH}}-apple-darwin\" ;;");
564    let _ = writeln!(out, "*)");
565    let _ = writeln!(out, "  echo \"Unsupported OS: $OS\" >&2");
566    let _ = writeln!(out, "  exit 1");
567    let _ = writeln!(out, "  ;;");
568    let _ = writeln!(out, "esac");
569    let _ = writeln!(out);
570    let _ = writeln!(out, "ARCHIVE=\"${{FFI_PKG_NAME}}-${{TRIPLE}}.tar.gz\"");
571    let _ = writeln!(
572        out,
573        "URL=\"${{REPO_URL}}/releases/download/v${{VERSION}}/${{ARCHIVE}}\""
574    );
575    let _ = writeln!(out);
576    let _ = writeln!(out, "echo \"Downloading ${{ARCHIVE}} from v${{VERSION}}...\"");
577    let _ = writeln!(out, "mkdir -p \"$FFI_DIR\"");
578    let _ = writeln!(out, "curl -fSL \"$URL\" | tar xz -C \"$FFI_DIR\"");
579    let _ = writeln!(out, "echo \"FFI library extracted to $FFI_DIR/\"");
580    out
581}
582
583fn render_test_runner_header(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
584    let mut out = String::new();
585    out.push_str(&hash::header(CommentStyle::Block));
586    let _ = writeln!(out, "#ifndef TEST_RUNNER_H");
587    let _ = writeln!(out, "#define TEST_RUNNER_H");
588    let _ = writeln!(out);
589    let _ = writeln!(out, "#include <string.h>");
590    let _ = writeln!(out, "#include <stdlib.h>");
591    let _ = writeln!(out);
592    // Trim helper for comparing strings that may have trailing whitespace/newlines.
593    let _ = writeln!(out, "/**");
594    let _ = writeln!(
595        out,
596        " * Compare a string against an expected value, trimming trailing whitespace."
597    );
598    let _ = writeln!(
599        out,
600        " * Returns 0 if the trimmed actual string equals the expected string."
601    );
602    let _ = writeln!(out, " */");
603    let _ = writeln!(
604        out,
605        "static inline int str_trim_eq(const char *actual, const char *expected) {{"
606    );
607    let _ = writeln!(
608        out,
609        "    if (actual == NULL || expected == NULL) return actual != expected;"
610    );
611    let _ = writeln!(out, "    size_t alen = strlen(actual);");
612    let _ = writeln!(
613        out,
614        "    while (alen > 0 && (actual[alen-1] == ' ' || actual[alen-1] == '\\n' || actual[alen-1] == '\\r' || actual[alen-1] == '\\t')) alen--;"
615    );
616    let _ = writeln!(out, "    size_t elen = strlen(expected);");
617    let _ = writeln!(out, "    if (alen != elen) return 1;");
618    let _ = writeln!(out, "    return memcmp(actual, expected, elen);");
619    let _ = writeln!(out, "}}");
620    let _ = writeln!(out);
621
622    // Forward declaration so alef_json_get_string can fall through to the
623    // object/array extractor for non-string values without reordering the helpers.
624    let _ = writeln!(
625        out,
626        "static inline char *alef_json_get_object(const char *json, const char *key);"
627    );
628    let _ = writeln!(out);
629    let _ = writeln!(out, "/**");
630    let _ = writeln!(
631        out,
632        " * Extract a string value for a given key from a JSON object string."
633    );
634    let _ = writeln!(
635        out,
636        " * Returns a heap-allocated copy of the value, or NULL if not found."
637    );
638    let _ = writeln!(out, " * Caller must free() the returned string.");
639    let _ = writeln!(out, " */");
640    let _ = writeln!(
641        out,
642        "static inline char *alef_json_get_string(const char *json, const char *key) {{"
643    );
644    let _ = writeln!(out, "    if (json == NULL || key == NULL) return NULL;");
645    let _ = writeln!(out, "    /* Build search pattern: \"key\":  */");
646    let _ = writeln!(out, "    size_t key_len = strlen(key);");
647    let _ = writeln!(out, "    char *pattern = (char *)malloc(key_len + 5);");
648    let _ = writeln!(out, "    if (!pattern) return NULL;");
649    let _ = writeln!(out, "    pattern[0] = '\"';");
650    let _ = writeln!(out, "    memcpy(pattern + 1, key, key_len);");
651    let _ = writeln!(out, "    pattern[key_len + 1] = '\"';");
652    let _ = writeln!(out, "    pattern[key_len + 2] = ':';");
653    let _ = writeln!(out, "    pattern[key_len + 3] = '\\0';");
654    let _ = writeln!(out, "    const char *found = strstr(json, pattern);");
655    let _ = writeln!(out, "    free(pattern);");
656    let _ = writeln!(out, "    if (!found) return NULL;");
657    let _ = writeln!(out, "    found += key_len + 3; /* skip past \"key\": */");
658    let _ = writeln!(out, "    while (*found == ' ' || *found == '\\t') found++;");
659    let _ = writeln!(
660        out,
661        "    /* Non-string values (arrays/objects) — fall through to alef_json_get_object so"
662    );
663    let _ = writeln!(
664        out,
665        "       leaf accessors over collection-typed fields (Vec<T>, Option<Vec<T>>) work for"
666    );
667    let _ = writeln!(
668        out,
669        "       not_empty / count_equals assertions without needing per-field type metadata. */"
670    );
671    let _ = writeln!(out, "    if (*found == '{{' || *found == '[') {{");
672    let _ = writeln!(out, "        return alef_json_get_object(json, key);");
673    let _ = writeln!(out, "    }}");
674    let _ = writeln!(
675        out,
676        "    /* Primitive non-string value: extract its raw token (numeric / true / false / null)"
677    );
678    let _ = writeln!(
679        out,
680        "       so callers asserting on numeric fields can `atoll`/`atof` the result. */"
681    );
682    let _ = writeln!(out, "    if (*found != '\"') {{");
683    let _ = writeln!(out, "        const char *p = found;");
684    let _ = writeln!(
685        out,
686        "        while (*p && *p != ',' && *p != '}}' && *p != ']' && *p != ' ' && *p != '\\t' && *p != '\\n' && *p != '\\r') p++;"
687    );
688    let _ = writeln!(out, "        size_t plen = (size_t)(p - found);");
689    let _ = writeln!(out, "        if (plen == 0) return NULL;");
690    let _ = writeln!(out, "        char *prim = (char *)malloc(plen + 1);");
691    let _ = writeln!(out, "        if (!prim) return NULL;");
692    let _ = writeln!(out, "        memcpy(prim, found, plen);");
693    let _ = writeln!(out, "        prim[plen] = '\\0';");
694    let _ = writeln!(out, "        return prim;");
695    let _ = writeln!(out, "    }}");
696    let _ = writeln!(out, "    found++; /* skip opening quote */");
697    let _ = writeln!(out, "    const char *end = found;");
698    let _ = writeln!(out, "    while (*end && *end != '\"') {{");
699    let _ = writeln!(out, "        if (*end == '\\\\') {{ end++; if (*end) end++; }}");
700    let _ = writeln!(out, "        else end++;");
701    let _ = writeln!(out, "    }}");
702    let _ = writeln!(out, "    size_t val_len = (size_t)(end - found);");
703    let _ = writeln!(out, "    char *result_str = (char *)malloc(val_len + 1);");
704    let _ = writeln!(out, "    if (!result_str) return NULL;");
705    let _ = writeln!(out, "    memcpy(result_str, found, val_len);");
706    let _ = writeln!(out, "    result_str[val_len] = '\\0';");
707    let _ = writeln!(out, "    return result_str;");
708    let _ = writeln!(out, "}}");
709    let _ = writeln!(out);
710    let _ = writeln!(out, "/**");
711    let _ = writeln!(
712        out,
713        " * Extract a JSON object/array value `{{...}}` or `[...]` for a given key from"
714    );
715    let _ = writeln!(
716        out,
717        " * a JSON object string. Returns a heap-allocated copy of the value INCLUDING"
718    );
719    let _ = writeln!(
720        out,
721        " * its surrounding braces, or NULL if the key is missing or its value is a"
722    );
723    let _ = writeln!(out, " * primitive. Caller must free() the returned string.");
724    let _ = writeln!(out, " *");
725    let _ = writeln!(
726        out,
727        " * Used by chained-accessor codegen for intermediate object extraction:"
728    );
729    let _ = writeln!(
730        out,
731        " * `choices[0].message.content` first peels off `message` (an object), then"
732    );
733    let _ = writeln!(out, " * looks up `content` (a string) within the extracted substring.");
734    let _ = writeln!(out, " */");
735    let _ = writeln!(
736        out,
737        "static inline char *alef_json_get_object(const char *json, const char *key) {{"
738    );
739    let _ = writeln!(out, "    if (json == NULL || key == NULL) return NULL;");
740    let _ = writeln!(out, "    size_t key_len = strlen(key);");
741    let _ = writeln!(out, "    char *pattern = (char *)malloc(key_len + 4);");
742    let _ = writeln!(out, "    if (!pattern) return NULL;");
743    let _ = writeln!(out, "    pattern[0] = '\"';");
744    let _ = writeln!(out, "    memcpy(pattern + 1, key, key_len);");
745    let _ = writeln!(out, "    pattern[key_len + 1] = '\"';");
746    let _ = writeln!(out, "    pattern[key_len + 2] = ':';");
747    let _ = writeln!(out, "    pattern[key_len + 3] = '\\0';");
748    let _ = writeln!(out, "    const char *found = strstr(json, pattern);");
749    let _ = writeln!(out, "    free(pattern);");
750    let _ = writeln!(out, "    if (!found) return NULL;");
751    let _ = writeln!(out, "    found += key_len + 3;");
752    let _ = writeln!(out, "    while (*found == ' ' || *found == '\\t') found++;");
753    let _ = writeln!(out, "    char open_ch = *found;");
754    let _ = writeln!(out, "    char close_ch;");
755    let _ = writeln!(out, "    if (open_ch == '{{') close_ch = '}}';");
756    let _ = writeln!(out, "    else if (open_ch == '[') close_ch = ']';");
757    let _ = writeln!(
758        out,
759        "    else return NULL; /* primitive — caller should use alef_json_get_string */"
760    );
761    let _ = writeln!(out, "    int depth = 0;");
762    let _ = writeln!(out, "    int in_string = 0;");
763    let _ = writeln!(out, "    const char *end = found;");
764    let _ = writeln!(out, "    for (; *end; end++) {{");
765    let _ = writeln!(out, "        if (in_string) {{");
766    let _ = writeln!(
767        out,
768        "            if (*end == '\\\\' && *(end + 1)) {{ end++; continue; }}"
769    );
770    let _ = writeln!(out, "            if (*end == '\"') in_string = 0;");
771    let _ = writeln!(out, "            continue;");
772    let _ = writeln!(out, "        }}");
773    let _ = writeln!(out, "        if (*end == '\"') {{ in_string = 1; continue; }}");
774    let _ = writeln!(out, "        if (*end == open_ch) depth++;");
775    let _ = writeln!(out, "        else if (*end == close_ch) {{");
776    let _ = writeln!(out, "            depth--;");
777    let _ = writeln!(out, "            if (depth == 0) {{ end++; break; }}");
778    let _ = writeln!(out, "        }}");
779    let _ = writeln!(out, "    }}");
780    let _ = writeln!(out, "    if (depth != 0) return NULL;");
781    let _ = writeln!(out, "    size_t val_len = (size_t)(end - found);");
782    let _ = writeln!(out, "    char *result_str = (char *)malloc(val_len + 1);");
783    let _ = writeln!(out, "    if (!result_str) return NULL;");
784    let _ = writeln!(out, "    memcpy(result_str, found, val_len);");
785    let _ = writeln!(out, "    result_str[val_len] = '\\0';");
786    let _ = writeln!(out, "    return result_str;");
787    let _ = writeln!(out, "}}");
788    let _ = writeln!(out);
789    let _ = writeln!(out, "/**");
790    let _ = writeln!(
791        out,
792        " * Extract the Nth top-level element of a JSON array as a heap string."
793    );
794    let _ = writeln!(
795        out,
796        " * Returns NULL if the input is not an array, the index is out of bounds, or"
797    );
798    let _ = writeln!(out, " * allocation fails. Caller must free() the returned string.");
799    let _ = writeln!(out, " */");
800    let _ = writeln!(
801        out,
802        "static inline char *alef_json_array_get_index(const char *json, int index) {{"
803    );
804    let _ = writeln!(out, "    if (json == NULL || index < 0) return NULL;");
805    let _ = writeln!(
806        out,
807        "    while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
808    );
809    let _ = writeln!(out, "    if (*json != '[') return NULL;");
810    let _ = writeln!(out, "    json++;");
811    let _ = writeln!(out, "    int current = 0;");
812    let _ = writeln!(out, "    while (*json) {{");
813    let _ = writeln!(
814        out,
815        "        while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
816    );
817    let _ = writeln!(out, "        if (*json == ']') return NULL;");
818    let _ = writeln!(out, "        const char *elem_start = json;");
819    let _ = writeln!(out, "        int depth = 0;");
820    let _ = writeln!(out, "        int in_string = 0;");
821    let _ = writeln!(out, "        for (; *json; json++) {{");
822    let _ = writeln!(out, "            if (in_string) {{");
823    let _ = writeln!(
824        out,
825        "                if (*json == '\\\\' && *(json + 1)) {{ json++; continue; }}"
826    );
827    let _ = writeln!(out, "                if (*json == '\"') in_string = 0;");
828    let _ = writeln!(out, "                continue;");
829    let _ = writeln!(out, "            }}");
830    let _ = writeln!(out, "            if (*json == '\"') {{ in_string = 1; continue; }}");
831    let _ = writeln!(out, "            if (*json == '{{' || *json == '[') depth++;");
832    let _ = writeln!(out, "            else if (*json == '}}' || *json == ']') {{");
833    let _ = writeln!(out, "                if (depth == 0) break;");
834    let _ = writeln!(out, "                depth--;");
835    let _ = writeln!(out, "            }}");
836    let _ = writeln!(out, "            else if (*json == ',' && depth == 0) break;");
837    let _ = writeln!(out, "        }}");
838    let _ = writeln!(out, "        if (current == index) {{");
839    let _ = writeln!(out, "            const char *elem_end = json;");
840    let _ = writeln!(
841        out,
842        "            while (elem_end > elem_start && (*(elem_end - 1) == ' ' || *(elem_end - 1) == '\\t' || *(elem_end - 1) == '\\n')) elem_end--;"
843    );
844    let _ = writeln!(out, "            size_t elem_len = (size_t)(elem_end - elem_start);");
845    let _ = writeln!(out, "            char *out_buf = (char *)malloc(elem_len + 1);");
846    let _ = writeln!(out, "            if (!out_buf) return NULL;");
847    let _ = writeln!(out, "            memcpy(out_buf, elem_start, elem_len);");
848    let _ = writeln!(out, "            out_buf[elem_len] = '\\0';");
849    let _ = writeln!(out, "            return out_buf;");
850    let _ = writeln!(out, "        }}");
851    let _ = writeln!(out, "        current++;");
852    let _ = writeln!(out, "        if (*json == ']') return NULL;");
853    let _ = writeln!(out, "        if (*json == ',') json++;");
854    let _ = writeln!(out, "    }}");
855    let _ = writeln!(out, "    return NULL;");
856    let _ = writeln!(out, "}}");
857    let _ = writeln!(out);
858    let _ = writeln!(out, "/**");
859    let _ = writeln!(out, " * Count top-level elements in a JSON array string.");
860    let _ = writeln!(out, " * Returns 0 for empty arrays (\"[]\") or NULL input.");
861    let _ = writeln!(out, " */");
862    let _ = writeln!(out, "static inline int alef_json_array_count(const char *json) {{");
863    let _ = writeln!(out, "    if (json == NULL) return 0;");
864    let _ = writeln!(out, "    /* Skip leading whitespace */");
865    let _ = writeln!(
866        out,
867        "    while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
868    );
869    let _ = writeln!(out, "    if (*json != '[') return 0;");
870    let _ = writeln!(out, "    json++;");
871    let _ = writeln!(out, "    /* Skip whitespace after '[' */");
872    let _ = writeln!(
873        out,
874        "    while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
875    );
876    let _ = writeln!(out, "    if (*json == ']') return 0;");
877    let _ = writeln!(out, "    int count = 1;");
878    let _ = writeln!(out, "    int depth = 0;");
879    let _ = writeln!(out, "    int in_string = 0;");
880    let _ = writeln!(
881        out,
882        "    for (; *json && !(*json == ']' && depth == 0 && !in_string); json++) {{"
883    );
884    let _ = writeln!(out, "        if (*json == '\\\\' && in_string) {{ json++; continue; }}");
885    let _ = writeln!(
886        out,
887        "        if (*json == '\"') {{ in_string = !in_string; continue; }}"
888    );
889    let _ = writeln!(out, "        if (in_string) continue;");
890    let _ = writeln!(out, "        if (*json == '[' || *json == '{{') depth++;");
891    let _ = writeln!(out, "        else if (*json == ']' || *json == '}}') depth--;");
892    let _ = writeln!(out, "        else if (*json == ',' && depth == 0) count++;");
893    let _ = writeln!(out, "    }}");
894    let _ = writeln!(out, "    return count;");
895    let _ = writeln!(out, "}}");
896    let _ = writeln!(out);
897
898    for (group, fixtures) in active_groups {
899        let _ = writeln!(out, "/* Tests for category: {} */", group.category);
900        for fixture in fixtures {
901            let fn_name = sanitize_ident(&fixture.id);
902            let _ = writeln!(out, "void test_{fn_name}(void);");
903        }
904        let _ = writeln!(out);
905    }
906
907    let _ = writeln!(out, "#endif /* TEST_RUNNER_H */");
908    out
909}
910
911fn render_main_c(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
912    let mut out = String::new();
913    out.push_str(&hash::header(CommentStyle::Block));
914    let _ = writeln!(out, "#include <stdio.h>");
915    let _ = writeln!(out, "#include \"test_runner.h\"");
916    let _ = writeln!(out);
917    let _ = writeln!(out, "int main(void) {{");
918    let _ = writeln!(out, "    int passed = 0;");
919    let _ = writeln!(out);
920
921    for (group, fixtures) in active_groups {
922        let _ = writeln!(out, "    /* Category: {} */", group.category);
923        for fixture in fixtures {
924            let fn_name = sanitize_ident(&fixture.id);
925            let _ = writeln!(out, "    printf(\"  Running test_{fn_name}...\");");
926            let _ = writeln!(out, "    test_{fn_name}();");
927            let _ = writeln!(out, "    printf(\" PASSED\\n\");");
928            let _ = writeln!(out, "    passed++;");
929        }
930        let _ = writeln!(out);
931    }
932
933    let _ = writeln!(out, "    printf(\"\\nResults: %d passed, 0 failed\\n\", passed);");
934    let _ = writeln!(out, "    return 0;");
935    let _ = writeln!(out, "}}");
936    out
937}
938
939#[allow(clippy::too_many_arguments)]
940fn render_test_file(
941    category: &str,
942    fixtures: &[&Fixture],
943    header: &str,
944    prefix: &str,
945    result_var: &str,
946    e2e_config: &E2eConfig,
947    lang: &str,
948    field_resolver: &FieldResolver,
949) -> String {
950    let mut out = String::new();
951    out.push_str(&hash::header(CommentStyle::Block));
952    let _ = writeln!(out, "/* E2e tests for category: {category} */");
953    let _ = writeln!(out);
954    let _ = writeln!(out, "#include <assert.h>");
955    let _ = writeln!(out, "#include <stdint.h>");
956    let _ = writeln!(out, "#include <string.h>");
957    let _ = writeln!(out, "#include <stdio.h>");
958    let _ = writeln!(out, "#include <stdlib.h>");
959    let _ = writeln!(out, "#include \"{header}\"");
960    let _ = writeln!(out, "#include \"test_runner.h\"");
961    let _ = writeln!(out);
962
963    for (i, fixture) in fixtures.iter().enumerate() {
964        // Visitor fixtures are filtered out before render_test_file is called.
965        // This guard is a safety net in case a fixture reaches here unexpectedly.
966        if fixture.visitor.is_some() {
967            panic!(
968                "C e2e generator: visitor pattern not supported for fixture: {}",
969                fixture.id
970            );
971        }
972
973        let call_info = resolve_fixture_call_info(fixture, e2e_config, lang);
974
975        // Effective enum fields for this fixture: merge global e2e_config.fields_enum
976        // (HashSet) with the per-call C override's enum_fields (HashMap keys). This
977        // mirrors Ruby/Java's pattern: global = always-enum-typed paths; per-call =
978        // context-dependent paths (BatchObject.status is BatchStatus, but
979        // ResponseObject.status is plain String).
980        let mut effective_fields_enum = e2e_config.fields_enum.clone();
981        let fixture_call = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
982        if let Some(co) = fixture_call.overrides.get(lang) {
983            for k in co.enum_fields.keys() {
984                effective_fields_enum.insert(k.clone());
985            }
986        }
987
988        render_test_function(
989            &mut out,
990            fixture,
991            prefix,
992            &call_info.function_name,
993            result_var,
994            &call_info.args,
995            field_resolver,
996            &e2e_config.fields_c_types,
997            &effective_fields_enum,
998            &call_info.result_type_name,
999            &call_info.options_type_name,
1000            call_info.client_factory.as_deref(),
1001            call_info.raw_c_result_type.as_deref(),
1002            call_info.c_free_fn.as_deref(),
1003            call_info.c_engine_factory.as_deref(),
1004            call_info.result_is_option,
1005            call_info.result_is_bytes,
1006            &call_info.extra_args,
1007        );
1008        if i + 1 < fixtures.len() {
1009            let _ = writeln!(out);
1010        }
1011    }
1012
1013    out
1014}
1015
1016#[allow(clippy::too_many_arguments)]
1017fn render_test_function(
1018    out: &mut String,
1019    fixture: &Fixture,
1020    prefix: &str,
1021    function_name: &str,
1022    result_var: &str,
1023    args: &[crate::config::ArgMapping],
1024    field_resolver: &FieldResolver,
1025    fields_c_types: &HashMap<String, String>,
1026    fields_enum: &HashSet<String>,
1027    result_type_name: &str,
1028    options_type_name: &str,
1029    client_factory: Option<&str>,
1030    raw_c_result_type: Option<&str>,
1031    c_free_fn: Option<&str>,
1032    c_engine_factory: Option<&str>,
1033    result_is_option: bool,
1034    result_is_bytes: bool,
1035    extra_args: &[String],
1036) {
1037    let fn_name = sanitize_ident(&fixture.id);
1038    let description = &fixture.description;
1039
1040    let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
1041
1042    let _ = writeln!(out, "void test_{fn_name}(void) {{");
1043    let _ = writeln!(out, "    /* {description} */");
1044
1045    // Smoke/live fixtures gated on a required env var (e.g. OPENAI_API_KEY).
1046    // When the var is missing, treat as a successful skip — mirrors Python's
1047    // `pytest.skip("OPENAI_API_KEY not set")` and Java's `Assumptions.assumeTrue(...)`
1048    // so CI runs without provider credentials don't fail every smoke test.
1049    //
1050    // When the fixture also has a mock_response/http block, we support an env+mock
1051    // fallback: if the API key is set, use the real API; otherwise fall back to the
1052    // mock server. This lets the same fixture exercise both paths.
1053    let has_mock = fixture.needs_mock_server();
1054    let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
1055    if let Some(env) = &fixture.env {
1056        if let Some(var) = &env.api_key_var {
1057            let fixture_id = &fixture.id;
1058            if has_mock {
1059                let _ = writeln!(out, "    const char* api_key = getenv(\"{var}\");");
1060                let _ = writeln!(out, "    const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1061                let _ = writeln!(out, "    char base_url_buf[512];");
1062                let _ = writeln!(out, "    int use_mock = !(api_key && api_key[0] != '\\0');");
1063                let _ = writeln!(out, "    if (!use_mock) {{");
1064                let _ = writeln!(
1065                    out,
1066                    "        fprintf(stderr, \"{fixture_id}: using real API ({var} is set)\\n\");"
1067                );
1068                let _ = writeln!(out, "    }} else {{");
1069                let _ = writeln!(
1070                    out,
1071                    "        fprintf(stderr, \"{fixture_id}: using mock server ({var} not set)\\n\");"
1072                );
1073                let _ = writeln!(
1074                    out,
1075                    "        snprintf(base_url_buf, sizeof(base_url_buf), \"%s/fixtures/{fixture_id}\", mock_base ? mock_base : \"\");"
1076                );
1077                let _ = writeln!(out, "        api_key = \"test-key\";");
1078                let _ = writeln!(out, "    }}");
1079            } else {
1080                let _ = writeln!(out, "    if (getenv(\"{var}\") == NULL) {{ return; }}");
1081            }
1082        }
1083    }
1084
1085    let prefix_upper = prefix.to_uppercase();
1086
1087    // Engine-factory pattern: used when c_engine_factory is configured (e.g. kreuzcrawl).
1088    // Creates a config handle from JSON, builds an engine, calls {prefix}_{function}(engine, url),
1089    // frees result and engine.
1090    if let Some(config_type) = c_engine_factory {
1091        render_engine_factory_test_function(
1092            out,
1093            fixture,
1094            prefix,
1095            function_name,
1096            result_var,
1097            field_resolver,
1098            fields_c_types,
1099            fields_enum,
1100            result_type_name,
1101            config_type,
1102            expects_error,
1103            raw_c_result_type,
1104        );
1105        return;
1106    }
1107
1108    // Streaming pattern: chat_stream uses an FFI iterator handle instead of a
1109    // single response. Emit start/next/free loop and aggregate per-chunk data
1110    // into local vars (chunks_count, stream_content, stream_complete) so fixture
1111    // assertions on pseudo-fields resolve to those locals rather than to
1112    // non-existent accessor functions on a single chunk handle.
1113    if client_factory.is_some() && function_name == "chat_stream" {
1114        render_chat_stream_test_function(
1115            out,
1116            fixture,
1117            prefix,
1118            result_var,
1119            args,
1120            options_type_name,
1121            expects_error,
1122            api_key_var,
1123        );
1124        return;
1125    }
1126
1127    // Byte-buffer pattern: methods like `speech` and `file_content` return raw
1128    // bytes via the out-pointer FFI shape:
1129    //   `int32_t fn(this, req, uint8_t** out_ptr, uintptr_t* out_len, uintptr_t* out_cap)`
1130    // rather than as an opaque `*Response` handle. The C codegen must declare
1131    // the out-params, check the int32_t status code, and free with
1132    // `<prefix>_free_bytes` rather than emitting non-existent
1133    // `<prefix>_<response>_audio` / `_content` accessors.
1134    if let Some(factory) = client_factory {
1135        if result_is_bytes {
1136            render_bytes_test_function(
1137                out,
1138                fixture,
1139                prefix,
1140                function_name,
1141                result_var,
1142                args,
1143                options_type_name,
1144                result_type_name,
1145                factory,
1146                expects_error,
1147            );
1148            return;
1149        }
1150    }
1151
1152    // Client pattern: used when client_factory is configured (e.g. liter-llm).
1153    // Builds typed request handles from json_object args, creates a client via the
1154    // factory function, calls {prefix}_default_client_{function_name}(client, req),
1155    // then frees result, request handles, and client.
1156    if let Some(factory) = client_factory {
1157        let mut request_handle_vars: Vec<(String, String)> = Vec::new(); // (arg_name, var_name)
1158        // Inline argument expressions appended after request handles in the
1159        // method call (e.g. literal C strings for `string` args, `NULL` for
1160        // optional pointer args). Order matches the position in `args`.
1161        let mut inline_method_args: Vec<String> = Vec::new();
1162
1163        for arg in args {
1164            if arg.arg_type == "json_object" {
1165                // Prefer options_type from the C override when set, since the result
1166                // type isn't always a clean strip-Response/append-Request transform
1167                // (e.g. transcribe -> Create**Transcription**Request, not TranscriptionRequest).
1168                // Fall back to deriving from result_type for backward-compat cases.
1169                let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
1170                    options_type_name.to_string()
1171                } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
1172                    format!("{}Request", stripped)
1173                } else {
1174                    format!("{result_type_name}Request")
1175                };
1176                let request_type_snake = request_type_pascal.to_snake_case();
1177                let var_name = format!("{request_type_snake}_handle");
1178
1179                let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1180                let json_val = if field.is_empty() || field == "input" {
1181                    Some(&fixture.input)
1182                } else {
1183                    fixture.input.get(field)
1184                };
1185
1186                if let Some(val) = json_val {
1187                    if !val.is_null() {
1188                        let normalized = super::transform_json_keys_for_language(val, "snake_case");
1189                        let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1190                        let escaped = escape_c(&json_str);
1191                        let _ = writeln!(
1192                            out,
1193                            "    {prefix_upper}{request_type_pascal}* {var_name} = \
1194                             {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
1195                        );
1196                        if expects_error {
1197                            // For error fixtures (e.g. invalid enum value rejected by
1198                            // serde), `_from_json` may legitimately return NULL — that
1199                            // counts as the expected failure. Mirror Java's pattern of
1200                            // wrapping setup + call inside `assertThrows(...)` so error
1201                            // fixtures pass at *any* failure step. The test returns
1202                            // before attempting to create a client, leaving no
1203                            // resources to free.
1204                            let _ = writeln!(out, "    if ({var_name} == NULL) {{ return; }}");
1205                        } else {
1206                            let _ = writeln!(out, "    assert({var_name} != NULL && \"failed to build request\");");
1207                        }
1208                        request_handle_vars.push((arg.name.clone(), var_name));
1209                    }
1210                }
1211            } else if arg.arg_type == "string" {
1212                // String arg: read fixture input, emit as a C string literal inline.
1213                let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1214                let val = fixture.input.get(field);
1215                match val {
1216                    Some(v) if v.is_string() => {
1217                        let s = v.as_str().unwrap_or_default();
1218                        let escaped = escape_c(s);
1219                        inline_method_args.push(format!("\"{escaped}\""));
1220                    }
1221                    Some(serde_json::Value::Null) | None if arg.optional => {
1222                        inline_method_args.push("NULL".to_string());
1223                    }
1224                    None => {
1225                        inline_method_args.push("\"\"".to_string());
1226                    }
1227                    Some(other) => {
1228                        let s = serde_json::to_string(other).unwrap_or_default();
1229                        let escaped = escape_c(&s);
1230                        inline_method_args.push(format!("\"{escaped}\""));
1231                    }
1232                }
1233            } else if arg.optional {
1234                // Optional non-string, non-json_object arg: pass NULL.
1235                inline_method_args.push("NULL".to_string());
1236            }
1237        }
1238
1239        let fixture_id = &fixture.id;
1240        // Pass UINT64_MAX/UINT32_MAX (≡ -1ULL/-1U) as the FFI's None sentinel for
1241        // optional numeric primitives — passing literal 0 makes the binding see
1242        // Some(0), which Rust core treats as `Duration::from_secs(0)` (immediate
1243        // request deadline) and breaks every HTTP fixture.
1244        if has_mock && api_key_var.is_some() {
1245            // api_key and base_url_buf are already declared in the env-fallback block above.
1246            // use_mock was captured before api_key was potentially reassigned to "test-key",
1247            // so it correctly reflects the original env state.
1248            let _ = writeln!(out, "    const char* _base_url_arg = use_mock ? base_url_buf : NULL;");
1249            let _ = writeln!(
1250                out,
1251                "    {prefix_upper}DefaultClient* client = {prefix}_{factory}(api_key, _base_url_arg, (uint64_t)-1, (uint32_t)-1, NULL);"
1252            );
1253        } else if has_mock {
1254            let _ = writeln!(out, "    const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1255            let _ = writeln!(out, "    assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
1256            let _ = writeln!(out, "    char base_url[1024];");
1257            let _ = writeln!(
1258                out,
1259                "    snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
1260            );
1261            let _ = writeln!(
1262                out,
1263                "    {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
1264            );
1265        } else {
1266            let _ = writeln!(
1267                out,
1268                "    {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
1269            );
1270        }
1271        let _ = writeln!(out, "    assert(client != NULL && \"failed to create client\");");
1272
1273        let method_args = if request_handle_vars.is_empty() && inline_method_args.is_empty() && extra_args.is_empty() {
1274            String::new()
1275        } else {
1276            let handles: Vec<String> = request_handle_vars.iter().map(|(_, v)| v.clone()).collect();
1277            let parts: Vec<String> = handles
1278                .into_iter()
1279                .chain(inline_method_args.iter().cloned())
1280                .chain(extra_args.iter().cloned())
1281                .collect();
1282            format!(", {}", parts.join(", "))
1283        };
1284
1285        let call_fn = format!("{prefix}_default_client_{function_name}");
1286
1287        if expects_error {
1288            let _ = writeln!(
1289                out,
1290                "    {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
1291            );
1292            for (_, var_name) in &request_handle_vars {
1293                let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
1294                let _ = writeln!(out, "    {prefix}_{req_snake}_free({var_name});");
1295            }
1296            let _ = writeln!(out, "    {prefix}_default_client_free(client);");
1297            let _ = writeln!(out, "    assert({result_var} == NULL && \"expected call to fail\");");
1298            let _ = writeln!(out, "}}");
1299            return;
1300        }
1301
1302        let _ = writeln!(
1303            out,
1304            "    {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
1305        );
1306        let _ = writeln!(out, "    assert({result_var} != NULL && \"expected call to succeed\");");
1307
1308        let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1309        let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1310        // Locals declared as primitive C scalars (uint64_t, double, bool, ...).
1311        // Locals not present here default to char* (heap-allocated accessor result).
1312        let mut primitive_locals: HashMap<String, String> = HashMap::new();
1313        // Locals declared as opaque struct handles (e.g. LITERLLMUsage*).
1314        // Keyed by local_var, value is the snake_case type name used for free().
1315        let mut opaque_handle_locals: HashMap<String, String> = HashMap::new();
1316
1317        for assertion in &fixture.assertions {
1318            if let Some(f) = &assertion.field {
1319                if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
1320                    let resolved = field_resolver.resolve(f);
1321                    let local_var = f.replace(['.', '['], "_").replace(']', "");
1322                    let has_map_access = resolved.contains('[');
1323                    if resolved.contains('.') {
1324                        let leaf_primitive = emit_nested_accessor(
1325                            out,
1326                            prefix,
1327                            resolved,
1328                            &local_var,
1329                            result_var,
1330                            fields_c_types,
1331                            fields_enum,
1332                            &mut intermediate_handles,
1333                            result_type_name,
1334                            f,
1335                        );
1336                        if let Some(prim) = leaf_primitive {
1337                            primitive_locals.insert(local_var.clone(), prim);
1338                        }
1339                    } else {
1340                        let result_type_snake = result_type_name.to_snake_case();
1341                        let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1342                        let lookup_key = format!("{result_type_snake}.{resolved}");
1343                        if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1344                            let _ = writeln!(out, "    {t} {local_var} = {accessor_fn}({result_var});");
1345                            primitive_locals.insert(local_var.clone(), t.clone());
1346                        } else if try_emit_enum_accessor(
1347                            out,
1348                            prefix,
1349                            &prefix_upper,
1350                            f,
1351                            resolved,
1352                            &result_type_snake,
1353                            &accessor_fn,
1354                            result_var,
1355                            &local_var,
1356                            fields_c_types,
1357                            fields_enum,
1358                            &mut intermediate_handles,
1359                        ) {
1360                            // accessor emitted with enum-to-string conversion
1361                        } else if let Some(handle_pascal) =
1362                            infer_opaque_handle_type(fields_c_types, &result_type_snake, resolved)
1363                        {
1364                            // Opaque struct handle: cannot be read as char*.
1365                            let _ = writeln!(
1366                                out,
1367                                "    {prefix_upper}{handle_pascal}* {local_var} = {accessor_fn}({result_var});"
1368                            );
1369                            opaque_handle_locals.insert(local_var.clone(), handle_pascal.to_snake_case());
1370                        } else {
1371                            let _ = writeln!(out, "    char* {local_var} = {accessor_fn}({result_var});");
1372                        }
1373                    }
1374                    accessed_fields.push((f.clone(), local_var, has_map_access));
1375                }
1376            }
1377        }
1378
1379        for assertion in &fixture.assertions {
1380            render_assertion(
1381                out,
1382                assertion,
1383                result_var,
1384                prefix,
1385                field_resolver,
1386                &accessed_fields,
1387                &primitive_locals,
1388                &opaque_handle_locals,
1389            );
1390        }
1391
1392        for (_f, local_var, from_json) in &accessed_fields {
1393            if primitive_locals.contains_key(local_var) {
1394                continue;
1395            }
1396            if let Some(snake_type) = opaque_handle_locals.get(local_var) {
1397                let _ = writeln!(out, "    {prefix}_{snake_type}_free({local_var});");
1398                continue;
1399            }
1400            if *from_json {
1401                let _ = writeln!(out, "    free({local_var});");
1402            } else {
1403                let _ = writeln!(out, "    {prefix}_free_string({local_var});");
1404            }
1405        }
1406        for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1407            if snake_type == "free_string" {
1408                let _ = writeln!(out, "    {prefix}_free_string({handle_var});");
1409            } else if snake_type == "free" {
1410                // Intermediate JSON-key extraction (alef_json_get_string) — heap
1411                // char* allocated by malloc-class helper; freed via plain free().
1412                let _ = writeln!(out, "    free({handle_var});");
1413            } else {
1414                let _ = writeln!(out, "    {prefix}_{snake_type}_free({handle_var});");
1415            }
1416        }
1417        let result_type_snake = result_type_name.to_snake_case();
1418        let _ = writeln!(out, "    {prefix}_{result_type_snake}_free({result_var});");
1419        for (_, var_name) in &request_handle_vars {
1420            let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
1421            let _ = writeln!(out, "    {prefix}_{req_snake}_free({var_name});");
1422        }
1423        let _ = writeln!(out, "    {prefix}_default_client_free(client);");
1424        let _ = writeln!(out, "}}");
1425        return;
1426    }
1427
1428    // Raw C result type path: functions returning a primitive C type (char*, int32_t,
1429    // uintptr_t) rather than an opaque handle pointer.
1430    if let Some(raw_type) = raw_c_result_type {
1431        // Build argument string. Void-arg functions pass nothing.
1432        let args_str = if args.is_empty() {
1433            String::new()
1434        } else {
1435            let parts: Vec<String> = args
1436                .iter()
1437                .filter_map(|arg| {
1438                    let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1439                    let val = fixture.input.get(field);
1440                    match val {
1441                        None if arg.optional => Some("NULL".to_string()),
1442                        None => None,
1443                        Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
1444                        Some(v) => Some(json_to_c(v)),
1445                    }
1446                })
1447                .collect();
1448            parts.join(", ")
1449        };
1450
1451        // Declare result variable.
1452        let _ = writeln!(out, "    {raw_type} {result_var} = {function_name}({args_str});");
1453
1454        // not_error assertion.
1455        let has_not_error = fixture.assertions.iter().any(|a| a.assertion_type == "not_error");
1456        if has_not_error {
1457            match raw_type {
1458                "char*" if !result_is_option => {
1459                    let _ = writeln!(out, "    assert({result_var} != NULL && \"expected call to succeed\");");
1460                }
1461                "int32_t" => {
1462                    let _ = writeln!(out, "    assert({result_var} >= 0 && \"expected call to succeed\");");
1463                }
1464                "uintptr_t" => {
1465                    let _ = writeln!(
1466                        out,
1467                        "    assert({prefix}_last_error_code() == 0 && \"expected call to succeed\");"
1468                    );
1469                }
1470                _ => {}
1471            }
1472        }
1473
1474        // Other assertions.
1475        for assertion in &fixture.assertions {
1476            match assertion.assertion_type.as_str() {
1477                "not_error" | "error" => {} // handled above / not applicable
1478                "not_empty" => {
1479                    let _ = writeln!(
1480                        out,
1481                        "    assert({result_var} != NULL && strlen({result_var}) > 0 && \"expected non-empty value\");"
1482                    );
1483                }
1484                "is_empty" => {
1485                    if result_is_option && raw_type == "char*" {
1486                        let _ = writeln!(
1487                            out,
1488                            "    assert({result_var} == NULL && \"expected empty/null value\");"
1489                        );
1490                    } else {
1491                        let _ = writeln!(
1492                            out,
1493                            "    assert(strlen({result_var}) == 0 && \"expected empty value\");"
1494                        );
1495                    }
1496                }
1497                "count_min" => {
1498                    if let Some(val) = &assertion.value {
1499                        if let Some(n) = val.as_u64() {
1500                            match raw_type {
1501                                "char*" => {
1502                                    let _ = writeln!(out, "    {{");
1503                                    let _ = writeln!(
1504                                        out,
1505                                        "        assert({result_var} != NULL && \"expected non-null JSON array\");"
1506                                    );
1507                                    let _ =
1508                                        writeln!(out, "        int elem_count = alef_json_array_count({result_var});");
1509                                    let _ = writeln!(
1510                                        out,
1511                                        "        assert(elem_count >= {n} && \"expected at least {n} elements\");"
1512                                    );
1513                                    let _ = writeln!(out, "    }}");
1514                                }
1515                                _ => {
1516                                    let _ = writeln!(
1517                                        out,
1518                                        "    assert((size_t){result_var} >= {n} && \"expected at least {n} elements\");"
1519                                    );
1520                                }
1521                            }
1522                        }
1523                    }
1524                }
1525                "greater_than_or_equal" => {
1526                    if let Some(val) = &assertion.value {
1527                        let c_val = json_to_c(val);
1528                        let _ = writeln!(
1529                            out,
1530                            "    assert({result_var} >= {c_val} && \"expected greater than or equal\");"
1531                        );
1532                    }
1533                }
1534                "contains" => {
1535                    if let Some(val) = &assertion.value {
1536                        let c_val = json_to_c(val);
1537                        let _ = writeln!(
1538                            out,
1539                            "    assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
1540                        );
1541                    }
1542                }
1543                "contains_all" => {
1544                    if let Some(values) = &assertion.values {
1545                        for val in values {
1546                            let c_val = json_to_c(val);
1547                            let _ = writeln!(
1548                                out,
1549                                "    assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
1550                            );
1551                        }
1552                    }
1553                }
1554                "equals" => {
1555                    if let Some(val) = &assertion.value {
1556                        let c_val = json_to_c(val);
1557                        if val.is_string() {
1558                            let _ = writeln!(
1559                                out,
1560                                "    assert({result_var} != NULL && str_trim_eq({result_var}, {c_val}) == 0 && \"equals assertion failed\");"
1561                            );
1562                        } else {
1563                            let _ = writeln!(
1564                                out,
1565                                "    assert({result_var} == {c_val} && \"equals assertion failed\");"
1566                            );
1567                        }
1568                    }
1569                }
1570                "not_contains" => {
1571                    if let Some(val) = &assertion.value {
1572                        let c_val = json_to_c(val);
1573                        let _ = writeln!(
1574                            out,
1575                            "    assert(strstr({result_var}, {c_val}) == NULL && \"expected NOT to contain substring\");"
1576                        );
1577                    }
1578                }
1579                "starts_with" => {
1580                    if let Some(val) = &assertion.value {
1581                        let c_val = json_to_c(val);
1582                        let _ = writeln!(
1583                            out,
1584                            "    assert(strncmp({result_var}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
1585                        );
1586                    }
1587                }
1588                "is_true" => {
1589                    let _ = writeln!(out, "    assert({result_var});");
1590                }
1591                "is_false" => {
1592                    let _ = writeln!(out, "    assert(!{result_var});");
1593                }
1594                other => {
1595                    panic!("C e2e raw-result generator: unsupported assertion type: {other}");
1596                }
1597            }
1598        }
1599
1600        // Free char* results.
1601        if raw_type == "char*" {
1602            let free_fn = c_free_fn
1603                .map(|s| s.to_string())
1604                .unwrap_or_else(|| format!("{prefix}_free_string"));
1605            if result_is_option {
1606                let _ = writeln!(out, "    if ({result_var} != NULL) {{ {free_fn}({result_var}); }}");
1607            } else {
1608                let _ = writeln!(out, "    {free_fn}({result_var});");
1609            }
1610        }
1611
1612        let _ = writeln!(out, "}}");
1613        return;
1614    }
1615
1616    // Legacy (non-client) path: call the function directly.
1617    // Used for libraries like html-to-markdown that expose standalone FFI functions.
1618
1619    // Use the function name directly — the override already includes the prefix
1620    // (e.g. "htm_convert"), so we must NOT prepend it again.
1621    let prefixed_fn = function_name.to_string();
1622
1623    // For json_object args, emit a from_json call to construct the options handle.
1624    let mut has_options_handle = false;
1625    for arg in args {
1626        if arg.arg_type == "json_object" {
1627            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1628            if let Some(val) = fixture.input.get(field) {
1629                if !val.is_null() {
1630                    // Fixture keys are camelCase; the FFI htm_conversion_options_from_json
1631                    // deserializes into the Rust ConversionOptions type which uses default
1632                    // serde (snake_case). Normalize keys before serializing.
1633                    let normalized = super::transform_json_keys_for_language(val, "snake_case");
1634                    let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1635                    let escaped = escape_c(&json_str);
1636                    let upper = prefix.to_uppercase();
1637                    let options_type_pascal = options_type_name;
1638                    let options_type_snake = options_type_name.to_snake_case();
1639                    let _ = writeln!(
1640                        out,
1641                        "    {upper}{options_type_pascal}* options_handle = {prefix}_{options_type_snake}_from_json(\"{escaped}\");"
1642                    );
1643                    has_options_handle = true;
1644                }
1645            }
1646        }
1647    }
1648
1649    let args_str = build_args_string_c(&fixture.input, args, has_options_handle);
1650
1651    if expects_error {
1652        let _ = writeln!(
1653            out,
1654            "    {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
1655        );
1656        if has_options_handle {
1657            let options_type_snake = options_type_name.to_snake_case();
1658            let _ = writeln!(out, "    {prefix}_{options_type_snake}_free(options_handle);");
1659        }
1660        let _ = writeln!(out, "    assert({result_var} == NULL && \"expected call to fail\");");
1661        let _ = writeln!(out, "}}");
1662        return;
1663    }
1664
1665    // The FFI returns an opaque handle; extract the content string from it.
1666    let _ = writeln!(
1667        out,
1668        "    {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
1669    );
1670    let _ = writeln!(out, "    assert({result_var} != NULL && \"expected call to succeed\");");
1671
1672    // Collect fields accessed by assertions so we can emit accessor calls.
1673    // C FFI uses the opaque handle pattern: {prefix}_conversion_result_{field}(handle).
1674    // For nested paths we generate chained FFI accessor calls using the type
1675    // chain from `fields_c_types`.
1676    // Each entry: (fixture_field, local_var, from_json_extract).
1677    // `from_json_extract` is true when the variable was extracted from a JSON
1678    // map via alef_json_get_string and needs free() instead of {prefix}_free_string().
1679    let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1680    // Track intermediate handles emitted so we can free them and avoid duplicates.
1681    // Each entry: (handle_var_name, snake_type_name) — freed in reverse order.
1682    let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1683    // Locals declared as primitive C scalars (uint64_t, double, bool, ...).
1684    let mut primitive_locals: HashMap<String, String> = HashMap::new();
1685    // Locals declared as opaque struct handles (e.g. LITERLLMUsage*).
1686    let mut opaque_handle_locals: HashMap<String, String> = HashMap::new();
1687
1688    for assertion in &fixture.assertions {
1689        if let Some(f) = &assertion.field {
1690            if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
1691                let resolved = field_resolver.resolve(f);
1692                let local_var = f.replace(['.', '['], "_").replace(']', "");
1693                let has_map_access = resolved.contains('[');
1694
1695                if resolved.contains('.') {
1696                    let leaf_primitive = emit_nested_accessor(
1697                        out,
1698                        prefix,
1699                        resolved,
1700                        &local_var,
1701                        result_var,
1702                        fields_c_types,
1703                        fields_enum,
1704                        &mut intermediate_handles,
1705                        result_type_name,
1706                        f,
1707                    );
1708                    if let Some(prim) = leaf_primitive {
1709                        primitive_locals.insert(local_var.clone(), prim);
1710                    }
1711                } else {
1712                    let result_type_snake = result_type_name.to_snake_case();
1713                    let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1714                    let lookup_key = format!("{result_type_snake}.{resolved}");
1715                    if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1716                        let _ = writeln!(out, "    {t} {local_var} = {accessor_fn}({result_var});");
1717                        primitive_locals.insert(local_var.clone(), t.clone());
1718                    } else if try_emit_enum_accessor(
1719                        out,
1720                        prefix,
1721                        &prefix_upper,
1722                        f,
1723                        resolved,
1724                        &result_type_snake,
1725                        &accessor_fn,
1726                        result_var,
1727                        &local_var,
1728                        fields_c_types,
1729                        fields_enum,
1730                        &mut intermediate_handles,
1731                    ) {
1732                        // accessor emitted with enum-to-string conversion
1733                    } else if let Some(handle_pascal) =
1734                        infer_opaque_handle_type(fields_c_types, &result_type_snake, resolved)
1735                    {
1736                        let _ = writeln!(
1737                            out,
1738                            "    {prefix_upper}{handle_pascal}* {local_var} = {accessor_fn}({result_var});"
1739                        );
1740                        opaque_handle_locals.insert(local_var.clone(), handle_pascal.to_snake_case());
1741                    } else {
1742                        let _ = writeln!(out, "    char* {local_var} = {accessor_fn}({result_var});");
1743                    }
1744                }
1745                accessed_fields.push((f.clone(), local_var.clone(), has_map_access));
1746            }
1747        }
1748    }
1749
1750    for assertion in &fixture.assertions {
1751        render_assertion(
1752            out,
1753            assertion,
1754            result_var,
1755            prefix,
1756            field_resolver,
1757            &accessed_fields,
1758            &primitive_locals,
1759            &opaque_handle_locals,
1760        );
1761    }
1762
1763    // Free extracted leaf strings.
1764    for (_f, local_var, from_json) in &accessed_fields {
1765        if primitive_locals.contains_key(local_var) {
1766            continue;
1767        }
1768        if let Some(snake_type) = opaque_handle_locals.get(local_var) {
1769            let _ = writeln!(out, "    {prefix}_{snake_type}_free({local_var});");
1770            continue;
1771        }
1772        if *from_json {
1773            let _ = writeln!(out, "    free({local_var});");
1774        } else {
1775            let _ = writeln!(out, "    {prefix}_free_string({local_var});");
1776        }
1777    }
1778    // Free intermediate handles in reverse order.
1779    for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1780        if snake_type == "free_string" {
1781            // free_string handles are freed with the free_string function directly.
1782            let _ = writeln!(out, "    {prefix}_free_string({handle_var});");
1783        } else {
1784            let _ = writeln!(out, "    {prefix}_{snake_type}_free({handle_var});");
1785        }
1786    }
1787    if has_options_handle {
1788        let options_type_snake = options_type_name.to_snake_case();
1789        let _ = writeln!(out, "    {prefix}_{options_type_snake}_free(options_handle);");
1790    }
1791    let result_type_snake = result_type_name.to_snake_case();
1792    let _ = writeln!(out, "    {prefix}_{result_type_snake}_free({result_var});");
1793    let _ = writeln!(out, "}}");
1794}
1795
1796/// Emit a test function using the engine-factory pattern:
1797///   `{prefix}_crawl_config_from_json(json)` → `{prefix}_create_engine(config)` →
1798///   `{prefix}_{function}(engine, url)` → assertions → free chain.
1799///
1800/// When all fixture assertions are skipped (fields not present on result type,
1801/// or only "error" assertions that C cannot replicate via a simple URL scrape),
1802/// the null-check is a soft guard (`if (result != NULL)`) so the test does not
1803/// abort when the mock server has no matching route.
1804#[allow(clippy::too_many_arguments)]
1805fn render_engine_factory_test_function(
1806    out: &mut String,
1807    fixture: &Fixture,
1808    prefix: &str,
1809    function_name: &str,
1810    result_var: &str,
1811    field_resolver: &FieldResolver,
1812    fields_c_types: &HashMap<String, String>,
1813    fields_enum: &HashSet<String>,
1814    result_type_name: &str,
1815    config_type: &str,
1816    expects_error: bool,
1817    raw_c_result_type: Option<&str>,
1818) {
1819    let prefix_upper = prefix.to_uppercase();
1820    let config_snake = config_type.to_snake_case();
1821
1822    // Build config JSON from fixture input (snake_case keys).
1823    let config_val = fixture.input.get("config");
1824    let config_json = match config_val {
1825        Some(v) if !v.is_null() => {
1826            let normalized = super::transform_json_keys_for_language(v, "snake_case");
1827            serde_json::to_string(&normalized).unwrap_or_else(|_| "{}".to_string())
1828        }
1829        _ => "{}".to_string(),
1830    };
1831    let config_escaped = escape_c(&config_json);
1832    let fixture_id = &fixture.id;
1833
1834    // An assertion is "active" when it has a field that is valid for the result type.
1835    // Error-only assertions are NOT treated as active for the engine factory pattern
1836    // because C's kcrawl_scrape() doesn't replicate batch/validation error semantics.
1837    let has_active_assertions = fixture.assertions.iter().any(|a| {
1838        if let Some(f) = &a.field {
1839            !f.is_empty() && field_resolver.is_valid_for_result(f)
1840        } else {
1841            false
1842        }
1843    });
1844
1845    // --- engine setup ---
1846    let _ = writeln!(
1847        out,
1848        "    {prefix_upper}{config_type}* config_handle = \
1849         {prefix}_{config_snake}_from_json(\"{config_escaped}\");"
1850    );
1851    if expects_error {
1852        // Config parsing may legitimately fail for error fixtures (e.g. invalid config
1853        // rejected by the FFI layer). Return early — that counts as the expected failure.
1854        let _ = writeln!(out, "    if (config_handle == NULL) {{ return; }}");
1855    } else {
1856        let _ = writeln!(out, "    assert(config_handle != NULL && \"failed to parse config\");");
1857    }
1858    let _ = writeln!(
1859        out,
1860        "    {prefix_upper}CrawlEngineHandle* engine = {prefix}_create_engine(config_handle);"
1861    );
1862    let _ = writeln!(out, "    {prefix}_{config_snake}_free(config_handle);");
1863    if expects_error {
1864        // Engine creation may legitimately fail for error fixtures (e.g. invalid config
1865        // rejected at engine-creation time). Return early — that counts as the expected failure.
1866        let _ = writeln!(out, "    if (engine == NULL) {{ return; }}");
1867    } else {
1868        let _ = writeln!(out, "    assert(engine != NULL && \"failed to create engine\");");
1869    }
1870
1871    // --- URL construction: prefer per-fixture MOCK_SERVER_<UPPER_ID> (for fixtures
1872    // that need host-root routes like /robots.txt or /sitemap.xml), fall back to
1873    // MOCK_SERVER_URL/fixtures/<id> for the common case. ---
1874    let fixture_env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1875    let _ = writeln!(out, "    const char* mock_per_fixture = getenv(\"{fixture_env_key}\");");
1876    let _ = writeln!(out, "    const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1877    let _ = writeln!(out, "    char url[2048];");
1878    let _ = writeln!(out, "    if (mock_per_fixture && mock_per_fixture[0] != '\\0') {{");
1879    let _ = writeln!(out, "        snprintf(url, sizeof(url), \"%s\", mock_per_fixture);");
1880    let _ = writeln!(out, "    }} else {{");
1881    let _ = writeln!(
1882        out,
1883        "        assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");"
1884    );
1885    let _ = writeln!(
1886        out,
1887        "        snprintf(url, sizeof(url), \"%s/fixtures/{fixture_id}\", mock_base);"
1888    );
1889    let _ = writeln!(out, "    }}");
1890
1891    // --- call ---
1892    // When the function returns a raw C type (e.g. char* for JSON-returning batch functions
1893    // like batch_scrape / batch_crawl), emit a plain variable declaration rather than an
1894    // opaque handle pointer.
1895    if raw_c_result_type == Some("char*") {
1896        let _ = writeln!(out, "    char* {result_var} = {prefix}_{function_name}(engine, url);");
1897        // For char* results there are no structured field accessors — emit a minimal
1898        // null-guard and free, then return.
1899        let _ = writeln!(out, "    if ({result_var} != NULL) {prefix}_free_string({result_var});");
1900        let _ = writeln!(out, "    {prefix}_crawl_engine_handle_free(engine);");
1901        let _ = writeln!(out, "}}");
1902        return;
1903    }
1904
1905    let _ = writeln!(
1906        out,
1907        "    {prefix_upper}{result_type_name}* {result_var} = {prefix}_{function_name}(engine, url);"
1908    );
1909
1910    // When no assertions can be verified (all skipped or error-only), use a soft
1911    // null-guard so the test is a no-op rather than aborting on a NULL result.
1912    if !has_active_assertions {
1913        let result_type_snake = result_type_name.to_snake_case();
1914        let _ = writeln!(
1915            out,
1916            "    if ({result_var} != NULL) {prefix}_{result_type_snake}_free({result_var});"
1917        );
1918        let _ = writeln!(out, "    {prefix}_crawl_engine_handle_free(engine);");
1919        let _ = writeln!(out, "}}");
1920        return;
1921    }
1922
1923    let _ = writeln!(out, "    assert({result_var} != NULL && \"expected call to succeed\");");
1924
1925    // --- field assertions ---
1926    let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1927    let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1928    let mut primitive_locals: HashMap<String, String> = HashMap::new();
1929    let mut opaque_handle_locals: HashMap<String, String> = HashMap::new();
1930
1931    for assertion in &fixture.assertions {
1932        if let Some(f) = &assertion.field {
1933            if !f.is_empty() && field_resolver.is_valid_for_result(f) && !accessed_fields.iter().any(|(k, _, _)| k == f)
1934            {
1935                let resolved = field_resolver.resolve(f);
1936                let local_var = f.replace(['.', '['], "_").replace(']', "");
1937                let has_map_access = resolved.contains('[');
1938                if resolved.contains('.') {
1939                    let leaf_primitive = emit_nested_accessor(
1940                        out,
1941                        prefix,
1942                        resolved,
1943                        &local_var,
1944                        result_var,
1945                        fields_c_types,
1946                        fields_enum,
1947                        &mut intermediate_handles,
1948                        result_type_name,
1949                        f,
1950                    );
1951                    if let Some(prim) = leaf_primitive {
1952                        primitive_locals.insert(local_var.clone(), prim);
1953                    }
1954                } else {
1955                    let result_type_snake = result_type_name.to_snake_case();
1956                    let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1957                    let lookup_key = format!("{result_type_snake}.{resolved}");
1958                    if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1959                        let _ = writeln!(out, "    {t} {local_var} = {accessor_fn}({result_var});");
1960                        primitive_locals.insert(local_var.clone(), t.clone());
1961                    } else if try_emit_enum_accessor(
1962                        out,
1963                        prefix,
1964                        &prefix_upper,
1965                        f,
1966                        resolved,
1967                        &result_type_snake,
1968                        &accessor_fn,
1969                        result_var,
1970                        &local_var,
1971                        fields_c_types,
1972                        fields_enum,
1973                        &mut intermediate_handles,
1974                    ) {
1975                        // accessor emitted with enum-to-string conversion
1976                    } else if let Some(handle_pascal) =
1977                        infer_opaque_handle_type(fields_c_types, &result_type_snake, resolved)
1978                    {
1979                        let _ = writeln!(
1980                            out,
1981                            "    {prefix_upper}{handle_pascal}* {local_var} = {accessor_fn}({result_var});"
1982                        );
1983                        opaque_handle_locals.insert(local_var.clone(), handle_pascal.to_snake_case());
1984                    } else {
1985                        let _ = writeln!(out, "    char* {local_var} = {accessor_fn}({result_var});");
1986                    }
1987                }
1988                accessed_fields.push((f.clone(), local_var, has_map_access));
1989            }
1990        }
1991    }
1992
1993    for assertion in &fixture.assertions {
1994        render_assertion(
1995            out,
1996            assertion,
1997            result_var,
1998            prefix,
1999            field_resolver,
2000            &accessed_fields,
2001            &primitive_locals,
2002            &opaque_handle_locals,
2003        );
2004    }
2005
2006    // --- free locals ---
2007    for (_f, local_var, from_json) in &accessed_fields {
2008        if primitive_locals.contains_key(local_var) {
2009            continue;
2010        }
2011        if let Some(snake_type) = opaque_handle_locals.get(local_var) {
2012            let _ = writeln!(out, "    {prefix}_{snake_type}_free({local_var});");
2013            continue;
2014        }
2015        if *from_json {
2016            let _ = writeln!(out, "    free({local_var});");
2017        } else {
2018            let _ = writeln!(out, "    {prefix}_free_string({local_var});");
2019        }
2020    }
2021    for (handle_var, snake_type) in intermediate_handles.iter().rev() {
2022        if snake_type == "free_string" {
2023            let _ = writeln!(out, "    {prefix}_free_string({handle_var});");
2024        } else {
2025            let _ = writeln!(out, "    {prefix}_{snake_type}_free({handle_var});");
2026        }
2027    }
2028
2029    let result_type_snake = result_type_name.to_snake_case();
2030    let _ = writeln!(out, "    {prefix}_{result_type_snake}_free({result_var});");
2031    let _ = writeln!(out, "    {prefix}_crawl_engine_handle_free(engine);");
2032    let _ = writeln!(out, "}}");
2033}
2034
2035/// Emit a byte-buffer test function for FFI methods returning raw bytes via
2036/// the out-pointer pattern (e.g. `speech`, `file_content`).
2037///
2038/// FFI signature shape:
2039/// ```c
2040/// int32_t {prefix}_default_client_{fn}(
2041///     const Client *this_,
2042///     const Request *req,                /* present when args is non-empty */
2043///     uint8_t **out_ptr,
2044///     uintptr_t *out_len,
2045///     uintptr_t *out_cap);
2046/// ```
2047///
2048/// Emits:
2049/// - request handle build (same as the standard client pattern)
2050/// - `uint8_t *out_ptr = NULL; uintptr_t out_len = 0, out_cap = 0;`
2051/// - call with `&out_ptr, &out_len, &out_cap`
2052/// - status assertion: `status == 0` on success, `status != 0` on expected error
2053/// - per-assertion: `not_empty` / `not_null` collapse to `out_len > 0` because
2054///   the pseudo "audio" / "content" field is the byte buffer itself
2055/// - `{prefix}_free_bytes(out_ptr, out_len, out_cap)` after assertions
2056#[allow(clippy::too_many_arguments)]
2057fn render_bytes_test_function(
2058    out: &mut String,
2059    fixture: &Fixture,
2060    prefix: &str,
2061    function_name: &str,
2062    _result_var: &str,
2063    args: &[crate::config::ArgMapping],
2064    options_type_name: &str,
2065    result_type_name: &str,
2066    factory: &str,
2067    expects_error: bool,
2068) {
2069    let prefix_upper = prefix.to_uppercase();
2070    let mut request_handle_vars: Vec<(String, String)> = Vec::new();
2071    let mut string_arg_exprs: Vec<String> = Vec::new();
2072
2073    for arg in args {
2074        match arg.arg_type.as_str() {
2075            "json_object" => {
2076                let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
2077                    options_type_name.to_string()
2078                } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
2079                    format!("{}Request", stripped)
2080                } else {
2081                    format!("{result_type_name}Request")
2082                };
2083                let request_type_snake = request_type_pascal.to_snake_case();
2084                let var_name = format!("{request_type_snake}_handle");
2085
2086                let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2087                let json_val = if field.is_empty() || field == "input" {
2088                    Some(&fixture.input)
2089                } else {
2090                    fixture.input.get(field)
2091                };
2092
2093                if let Some(val) = json_val {
2094                    if !val.is_null() {
2095                        let normalized = super::transform_json_keys_for_language(val, "snake_case");
2096                        let json_str = serde_json::to_string(&normalized).unwrap_or_default();
2097                        let escaped = escape_c(&json_str);
2098                        let _ = writeln!(
2099                            out,
2100                            "    {prefix_upper}{request_type_pascal}* {var_name} = \
2101                             {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
2102                        );
2103                        if expects_error {
2104                            // For error fixtures (e.g. invalid enum value rejected by
2105                            // serde), `_from_json` may legitimately return NULL — that
2106                            // counts as the expected failure. Mirror Java's pattern of
2107                            // wrapping setup + call inside `assertThrows(...)` so error
2108                            // fixtures pass at *any* failure step. The test returns
2109                            // before attempting to create a client, leaving no
2110                            // resources to free.
2111                            let _ = writeln!(out, "    if ({var_name} == NULL) {{ return; }}");
2112                        } else {
2113                            let _ = writeln!(out, "    assert({var_name} != NULL && \"failed to build request\");");
2114                        }
2115                        request_handle_vars.push((arg.name.clone(), var_name));
2116                    }
2117                }
2118            }
2119            "string" => {
2120                // Pass string args (e.g. file_id for file_content) directly as
2121                // C string literals.
2122                let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2123                let val = fixture.input.get(field);
2124                let expr = match val {
2125                    Some(serde_json::Value::String(s)) => format!("\"{}\"", escape_c(s)),
2126                    Some(serde_json::Value::Null) | None if arg.optional => "NULL".to_string(),
2127                    Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "NULL".to_string()),
2128                    None => "NULL".to_string(),
2129                };
2130                string_arg_exprs.push(expr);
2131            }
2132            _ => {
2133                // Other arg types are not currently exercised by byte-buffer
2134                // methods; pass NULL so the call shape compiles.
2135                string_arg_exprs.push("NULL".to_string());
2136            }
2137        }
2138    }
2139
2140    let fixture_id = &fixture.id;
2141    if fixture.needs_mock_server() {
2142        let _ = writeln!(out, "    const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
2143        let _ = writeln!(out, "    assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
2144        let _ = writeln!(out, "    char base_url[1024];");
2145        let _ = writeln!(
2146            out,
2147            "    snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
2148        );
2149        // Pass UINT64_MAX/UINT32_MAX (≡ -1ULL/-1U) as the FFI's None sentinel for
2150        // optional numeric primitives — passing literal 0 makes the binding see
2151        // Some(0), which Rust core treats as `Duration::from_secs(0)` (immediate
2152        // request deadline) and breaks every HTTP fixture.
2153        let _ = writeln!(
2154            out,
2155            "    {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
2156        );
2157    } else {
2158        let _ = writeln!(
2159            out,
2160            "    {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
2161        );
2162    }
2163    let _ = writeln!(out, "    assert(client != NULL && \"failed to create client\");");
2164
2165    // Out-params for the byte buffer.
2166    let _ = writeln!(out, "    uint8_t* out_ptr = NULL;");
2167    let _ = writeln!(out, "    uintptr_t out_len = 0;");
2168    let _ = writeln!(out, "    uintptr_t out_cap = 0;");
2169
2170    // Build the comma-separated argument list: handles, then string args.
2171    let mut method_args: Vec<String> = Vec::new();
2172    for (_, v) in &request_handle_vars {
2173        method_args.push(v.clone());
2174    }
2175    method_args.extend(string_arg_exprs.iter().cloned());
2176    let extra_args = if method_args.is_empty() {
2177        String::new()
2178    } else {
2179        format!(", {}", method_args.join(", "))
2180    };
2181
2182    let call_fn = format!("{prefix}_default_client_{function_name}");
2183    let _ = writeln!(
2184        out,
2185        "    int32_t status = {call_fn}(client{extra_args}, &out_ptr, &out_len, &out_cap);"
2186    );
2187
2188    if expects_error {
2189        for (_, var_name) in &request_handle_vars {
2190            let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
2191            let _ = writeln!(out, "    {prefix}_{req_snake}_free({var_name});");
2192        }
2193        let _ = writeln!(out, "    {prefix}_default_client_free(client);");
2194        let _ = writeln!(out, "    assert(status != 0 && \"expected call to fail\");");
2195        // free_bytes accepts a NULL ptr (no-op), so it is safe regardless of
2196        // whether the failed call wrote out_ptr.
2197        let _ = writeln!(out, "    {prefix}_free_bytes(out_ptr, out_len, out_cap);");
2198        let _ = writeln!(out, "}}");
2199        return;
2200    }
2201
2202    let _ = writeln!(out, "    assert(status == 0 && \"expected call to succeed\");");
2203
2204    // Render assertions. For byte-buffer methods, the only meaningful per-field
2205    // assertions are presence/length checks on the buffer itself. Field names
2206    // (e.g. "audio", "content") are pseudo-fields — collapse them all to
2207    // `out_len > 0`.
2208    let mut emitted_len_check = false;
2209    for assertion in &fixture.assertions {
2210        match assertion.assertion_type.as_str() {
2211            "not_error" => {
2212                // Already covered by the status == 0 assertion above.
2213            }
2214            "not_empty" | "not_null" => {
2215                if !emitted_len_check {
2216                    let _ = writeln!(out, "    assert(out_len > 0 && \"expected non-empty value\");");
2217                    emitted_len_check = true;
2218                }
2219            }
2220            _ => {
2221                // Other assertion shapes (equals, contains, ...) don't apply to
2222                // raw bytes; emit a comment so the test stays readable but does
2223                // not emit broken accessor calls.
2224                let _ = writeln!(
2225                    out,
2226                    "    /* skipped: assertion '{}' not meaningful on raw byte buffer */",
2227                    assertion.assertion_type
2228                );
2229            }
2230        }
2231    }
2232
2233    let _ = writeln!(out, "    {prefix}_free_bytes(out_ptr, out_len, out_cap);");
2234    for (_, var_name) in &request_handle_vars {
2235        let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
2236        let _ = writeln!(out, "    {prefix}_{req_snake}_free({var_name});");
2237    }
2238    let _ = writeln!(out, "    {prefix}_default_client_free(client);");
2239    let _ = writeln!(out, "}}");
2240}
2241
2242/// Emit a chat-stream test function that drives the FFI iterator handle.
2243///
2244/// Calls `{prefix}_default_client_chat_stream_start` to obtain an opaque handle,
2245/// loops over `{prefix}_default_client_chat_stream_next` until it returns null,
2246/// and aggregates per-chunk data into local variables (`chunks_count`,
2247/// `stream_content`, `stream_complete`, `last_choices_json`, ...). Fixture
2248/// assertions on streaming pseudo-fields (`chunks`, `stream_content`,
2249/// `stream_complete`, `no_chunks_after_done`, `finish_reason`, `tool_calls`,
2250/// `tool_calls[0].function.name`, `usage.total_tokens`) are translated to
2251/// assertions on these locals.
2252#[allow(clippy::too_many_arguments)]
2253fn render_chat_stream_test_function(
2254    out: &mut String,
2255    fixture: &Fixture,
2256    prefix: &str,
2257    result_var: &str,
2258    args: &[crate::config::ArgMapping],
2259    options_type_name: &str,
2260    expects_error: bool,
2261    api_key_var: Option<&str>,
2262) {
2263    let prefix_upper = prefix.to_uppercase();
2264
2265    let mut request_var: Option<String> = None;
2266    for arg in args {
2267        if arg.arg_type == "json_object" {
2268            let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
2269                options_type_name.to_string()
2270            } else {
2271                "ChatCompletionRequest".to_string()
2272            };
2273            let request_type_snake = request_type_pascal.to_snake_case();
2274            let var_name = format!("{request_type_snake}_handle");
2275
2276            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2277            let json_val = if field.is_empty() || field == "input" {
2278                Some(&fixture.input)
2279            } else {
2280                fixture.input.get(field)
2281            };
2282
2283            if let Some(val) = json_val {
2284                if !val.is_null() {
2285                    let normalized = super::transform_json_keys_for_language(val, "snake_case");
2286                    let json_str = serde_json::to_string(&normalized).unwrap_or_default();
2287                    let escaped = escape_c(&json_str);
2288                    let _ = writeln!(
2289                        out,
2290                        "    {prefix_upper}{request_type_pascal}* {var_name} = \
2291                         {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
2292                    );
2293                    let _ = writeln!(out, "    assert({var_name} != NULL && \"failed to build request\");");
2294                    request_var = Some(var_name);
2295                    break;
2296                }
2297            }
2298        }
2299    }
2300
2301    let req_handle = request_var.clone().unwrap_or_else(|| "NULL".to_string());
2302    let req_snake = request_var
2303        .as_ref()
2304        .and_then(|v| v.strip_suffix("_handle"))
2305        .unwrap_or("chat_completion_request")
2306        .to_string();
2307
2308    let fixture_id = &fixture.id;
2309    let has_mock = fixture.needs_mock_server();
2310    if has_mock && api_key_var.is_some() {
2311        // `api_key` and `base_url_buf` are already declared by the env-fallback
2312        // block above (the smoke+mock path). Reuse them — don't redeclare
2313        // `mock_base`/`base_url`, which would be a C compile error.
2314        // use_mock was captured before api_key was potentially reassigned to "test-key",
2315        // so it correctly reflects the original env state.
2316        let _ = writeln!(out, "    const char* _base_url_arg = use_mock ? base_url_buf : NULL;");
2317        let _ = writeln!(
2318            out,
2319            "    {prefix_upper}DefaultClient* client = {prefix}_create_client(api_key, _base_url_arg, (uint64_t)-1, (uint32_t)-1, NULL);"
2320        );
2321    } else if has_mock {
2322        let _ = writeln!(out, "    const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
2323        let _ = writeln!(out, "    assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
2324        let _ = writeln!(out, "    char base_url[1024];");
2325        let _ = writeln!(
2326            out,
2327            "    snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
2328        );
2329        // Pass UINT64_MAX/UINT32_MAX (≡ -1ULL/-1U) as the FFI's None sentinel for
2330        // optional numeric primitives — passing literal 0 makes the binding see
2331        // Some(0), which Rust core treats as `Duration::from_secs(0)` (immediate
2332        // request deadline) and breaks every HTTP fixture.
2333        let _ = writeln!(
2334            out,
2335            "    {prefix_upper}DefaultClient* client = {prefix}_create_client(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
2336        );
2337    } else {
2338        let _ = writeln!(
2339            out,
2340            "    {prefix_upper}DefaultClient* client = {prefix}_create_client(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
2341        );
2342    }
2343    let _ = writeln!(out, "    assert(client != NULL && \"failed to create client\");");
2344
2345    let _ = writeln!(
2346        out,
2347        "    {prefix_upper}LiterllmDefaultClientChatStreamStreamHandle* stream_handle = \
2348         {prefix}_default_client_chat_stream_start(client, {req_handle});"
2349    );
2350
2351    if expects_error {
2352        let _ = writeln!(
2353            out,
2354            "    assert(stream_handle == NULL && \"expected stream-start to fail\");"
2355        );
2356        if request_var.is_some() {
2357            let _ = writeln!(out, "    {prefix}_{req_snake}_free({req_handle});");
2358        }
2359        let _ = writeln!(out, "    {prefix}_default_client_free(client);");
2360        let _ = writeln!(out, "}}");
2361        return;
2362    }
2363
2364    let _ = writeln!(
2365        out,
2366        "    assert(stream_handle != NULL && \"expected stream-start to succeed\");"
2367    );
2368
2369    let _ = writeln!(out, "    size_t chunks_count = 0;");
2370    let _ = writeln!(out, "    char* stream_content = (char*)malloc(1);");
2371    let _ = writeln!(out, "    assert(stream_content != NULL);");
2372    let _ = writeln!(out, "    stream_content[0] = '\\0';");
2373    let _ = writeln!(out, "    size_t stream_content_len = 0;");
2374    let _ = writeln!(out, "    int stream_complete = 0;");
2375    let _ = writeln!(out, "    int no_chunks_after_done = 1;");
2376    let _ = writeln!(out, "    char* last_choices_json = NULL;");
2377    let _ = writeln!(out, "    uint64_t total_tokens = 0;");
2378    let _ = writeln!(out);
2379
2380    let _ = writeln!(out, "    while (1) {{");
2381    let _ = writeln!(
2382        out,
2383        "        {prefix_upper}ChatCompletionChunk* {result_var} = \
2384         {prefix}_default_client_chat_stream_next(stream_handle);"
2385    );
2386    let _ = writeln!(out, "        if ({result_var} == NULL) {{");
2387    let _ = writeln!(
2388        out,
2389        "            if ({prefix}_last_error_code() == 0) {{ stream_complete = 1; }}"
2390    );
2391    let _ = writeln!(out, "            break;");
2392    let _ = writeln!(out, "        }}");
2393    let _ = writeln!(out, "        chunks_count++;");
2394    let _ = writeln!(
2395        out,
2396        "        char* choices_json = {prefix}_chat_completion_chunk_choices({result_var});"
2397    );
2398    let _ = writeln!(out, "        if (choices_json != NULL) {{");
2399    let _ = writeln!(
2400        out,
2401        "            const char* d = strstr(choices_json, \"\\\"content\\\":\");"
2402    );
2403    let _ = writeln!(out, "            if (d != NULL) {{");
2404    let _ = writeln!(out, "                d += 10;");
2405    let _ = writeln!(out, "                while (*d == ' ' || *d == '\\t') d++;");
2406    let _ = writeln!(out, "                if (*d == '\"') {{");
2407    let _ = writeln!(out, "                    d++;");
2408    let _ = writeln!(out, "                    const char* e = d;");
2409    let _ = writeln!(out, "                    while (*e && *e != '\"') {{");
2410    let _ = writeln!(
2411        out,
2412        "                        if (*e == '\\\\' && *(e+1)) e += 2; else e++;"
2413    );
2414    let _ = writeln!(out, "                    }}");
2415    let _ = writeln!(out, "                    size_t add = (size_t)(e - d);");
2416    let _ = writeln!(out, "                    if (add > 0) {{");
2417    let _ = writeln!(
2418        out,
2419        "                        char* nc = (char*)realloc(stream_content, stream_content_len + add + 1);"
2420    );
2421    let _ = writeln!(out, "                        if (nc != NULL) {{");
2422    let _ = writeln!(out, "                            stream_content = nc;");
2423    let _ = writeln!(
2424        out,
2425        "                            memcpy(stream_content + stream_content_len, d, add);"
2426    );
2427    let _ = writeln!(out, "                            stream_content_len += add;");
2428    let _ = writeln!(
2429        out,
2430        "                            stream_content[stream_content_len] = '\\0';"
2431    );
2432    let _ = writeln!(out, "                        }}");
2433    let _ = writeln!(out, "                    }}");
2434    let _ = writeln!(out, "                }}");
2435    let _ = writeln!(out, "            }}");
2436    let _ = writeln!(
2437        out,
2438        "            if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
2439    );
2440    let _ = writeln!(out, "            last_choices_json = choices_json;");
2441    let _ = writeln!(out, "        }}");
2442    let _ = writeln!(
2443        out,
2444        "        {prefix_upper}Usage* usage_handle = {prefix}_chat_completion_chunk_usage({result_var});"
2445    );
2446    let _ = writeln!(out, "        if (usage_handle != NULL) {{");
2447    let _ = writeln!(
2448        out,
2449        "            total_tokens = (uint64_t){prefix}_usage_total_tokens(usage_handle);"
2450    );
2451    let _ = writeln!(out, "            {prefix}_usage_free(usage_handle);");
2452    let _ = writeln!(out, "        }}");
2453    let _ = writeln!(out, "        {prefix}_chat_completion_chunk_free({result_var});");
2454    let _ = writeln!(out, "    }}");
2455    let _ = writeln!(out, "    {prefix}_default_client_chat_stream_free(stream_handle);");
2456    let _ = writeln!(out);
2457
2458    let _ = writeln!(out, "    char* finish_reason = NULL;");
2459    let _ = writeln!(out, "    char* tool_calls_json = NULL;");
2460    let _ = writeln!(out, "    char* tool_calls_0_function_name = NULL;");
2461    let _ = writeln!(out, "    if (last_choices_json != NULL) {{");
2462    let _ = writeln!(
2463        out,
2464        "        finish_reason = alef_json_get_string(last_choices_json, \"finish_reason\");"
2465    );
2466    let _ = writeln!(
2467        out,
2468        "        const char* tc = strstr(last_choices_json, \"\\\"tool_calls\\\":\");"
2469    );
2470    let _ = writeln!(out, "        if (tc != NULL) {{");
2471    let _ = writeln!(out, "            tc += 13;");
2472    let _ = writeln!(out, "            while (*tc == ' ' || *tc == '\\t') tc++;");
2473    let _ = writeln!(out, "            if (*tc == '[') {{");
2474    let _ = writeln!(out, "                int depth = 0;");
2475    let _ = writeln!(out, "                const char* end = tc;");
2476    let _ = writeln!(out, "                int in_str = 0;");
2477    let _ = writeln!(out, "                for (; *end; end++) {{");
2478    let _ = writeln!(
2479        out,
2480        "                    if (*end == '\\\\' && in_str) {{ if (*(end+1)) end++; continue; }}"
2481    );
2482    let _ = writeln!(
2483        out,
2484        "                    if (*end == '\"') {{ in_str = !in_str; continue; }}"
2485    );
2486    let _ = writeln!(out, "                    if (in_str) continue;");
2487    let _ = writeln!(out, "                    if (*end == '[' || *end == '{{') depth++;");
2488    let _ = writeln!(
2489        out,
2490        "                    else if (*end == ']' || *end == '}}') {{ depth--; if (depth == 0) {{ end++; break; }} }}"
2491    );
2492    let _ = writeln!(out, "                }}");
2493    let _ = writeln!(out, "                size_t tlen = (size_t)(end - tc);");
2494    let _ = writeln!(out, "                tool_calls_json = (char*)malloc(tlen + 1);");
2495    let _ = writeln!(out, "                if (tool_calls_json != NULL) {{");
2496    let _ = writeln!(out, "                    memcpy(tool_calls_json, tc, tlen);");
2497    let _ = writeln!(out, "                    tool_calls_json[tlen] = '\\0';");
2498    let _ = writeln!(
2499        out,
2500        "                    const char* fn = strstr(tool_calls_json, \"\\\"function\\\"\");"
2501    );
2502    let _ = writeln!(out, "                    if (fn != NULL) {{");
2503    let _ = writeln!(
2504        out,
2505        "                        const char* np = strstr(fn, \"\\\"name\\\":\");"
2506    );
2507    let _ = writeln!(out, "                        if (np != NULL) {{");
2508    let _ = writeln!(out, "                            np += 7;");
2509    let _ = writeln!(
2510        out,
2511        "                            while (*np == ' ' || *np == '\\t') np++;"
2512    );
2513    let _ = writeln!(out, "                            if (*np == '\"') {{");
2514    let _ = writeln!(out, "                                np++;");
2515    let _ = writeln!(out, "                                const char* ne = np;");
2516    let _ = writeln!(
2517        out,
2518        "                                while (*ne && *ne != '\"') {{ if (*ne == '\\\\' && *(ne+1)) ne += 2; else ne++; }}"
2519    );
2520    let _ = writeln!(out, "                                size_t nlen = (size_t)(ne - np);");
2521    let _ = writeln!(
2522        out,
2523        "                                tool_calls_0_function_name = (char*)malloc(nlen + 1);"
2524    );
2525    let _ = writeln!(
2526        out,
2527        "                                if (tool_calls_0_function_name != NULL) {{"
2528    );
2529    let _ = writeln!(
2530        out,
2531        "                                    memcpy(tool_calls_0_function_name, np, nlen);"
2532    );
2533    let _ = writeln!(
2534        out,
2535        "                                    tool_calls_0_function_name[nlen] = '\\0';"
2536    );
2537    let _ = writeln!(out, "                                }}");
2538    let _ = writeln!(out, "                            }}");
2539    let _ = writeln!(out, "                        }}");
2540    let _ = writeln!(out, "                    }}");
2541    let _ = writeln!(out, "                }}");
2542    let _ = writeln!(out, "            }}");
2543    let _ = writeln!(out, "        }}");
2544    let _ = writeln!(out, "    }}");
2545    let _ = writeln!(out);
2546
2547    for assertion in &fixture.assertions {
2548        emit_chat_stream_assertion(out, assertion);
2549    }
2550
2551    let _ = writeln!(out, "    free(stream_content);");
2552    let _ = writeln!(
2553        out,
2554        "    if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
2555    );
2556    let _ = writeln!(out, "    if (finish_reason != NULL) free(finish_reason);");
2557    let _ = writeln!(out, "    if (tool_calls_json != NULL) free(tool_calls_json);");
2558    let _ = writeln!(
2559        out,
2560        "    if (tool_calls_0_function_name != NULL) free(tool_calls_0_function_name);"
2561    );
2562    if request_var.is_some() {
2563        let _ = writeln!(out, "    {prefix}_{req_snake}_free({req_handle});");
2564    }
2565    let _ = writeln!(out, "    {prefix}_default_client_free(client);");
2566    let _ = writeln!(
2567        out,
2568        "    /* suppress unused */ (void)total_tokens; (void)no_chunks_after_done; \
2569         (void)stream_complete; (void)chunks_count; (void)stream_content_len;"
2570    );
2571    let _ = writeln!(out, "}}");
2572}
2573
2574/// Emit a single fixture assertion for a chat-stream test, mapping fixture
2575/// pseudo-field references (`chunks`, `stream_content`, `stream_complete`, ...)
2576/// to the local aggregator variables built by [`render_chat_stream_test_function`].
2577fn emit_chat_stream_assertion(out: &mut String, assertion: &Assertion) {
2578    let field = assertion.field.as_deref().unwrap_or("");
2579
2580    enum Kind {
2581        IntCount,
2582        Bool,
2583        Str,
2584        IntTokens,
2585        Unsupported,
2586    }
2587
2588    let (expr, kind) = match field {
2589        "chunks" => ("chunks_count", Kind::IntCount),
2590        "stream_content" => ("stream_content", Kind::Str),
2591        "stream_complete" => ("stream_complete", Kind::Bool),
2592        "no_chunks_after_done" => ("no_chunks_after_done", Kind::Bool),
2593        "finish_reason" => ("finish_reason", Kind::Str),
2594        // tool_calls / tool_calls[0].function.name require accumulating across
2595        // delta chunks (the OpenAI SSE wire format spreads the array contents
2596        // over many chunks). The current C inline SSE parser only inspects the
2597        // *last* chunk's `choices`, which carries `finish_reason=tool_calls`
2598        // but no payload — so these assertions can't reliably evaluate. Skip
2599        // them, mirroring Python's `# skipped: field 'tool_calls' not available
2600        // on result type` outcome (Python's stream iterator doesn't expose them
2601        // either). Adding a delta-merge accumulator is its own follow-up.
2602        "tool_calls" | "tool_calls[0].function.name" => ("", Kind::Unsupported),
2603        "usage.total_tokens" => ("total_tokens", Kind::IntTokens),
2604        _ => ("", Kind::Unsupported),
2605    };
2606
2607    let atype = assertion.assertion_type.as_str();
2608    if atype == "not_error" || atype == "error" {
2609        return;
2610    }
2611
2612    if matches!(kind, Kind::Unsupported) {
2613        let _ = writeln!(
2614            out,
2615            "    /* skipped: streaming assertion on unsupported field '{field}' */"
2616        );
2617        return;
2618    }
2619
2620    match (atype, &kind) {
2621        ("count_min", Kind::IntCount) => {
2622            if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2623                let _ = writeln!(out, "    assert({expr} >= {n} && \"expected at least {n} chunks\");");
2624            }
2625        }
2626        ("equals", Kind::Str) => {
2627            if let Some(val) = &assertion.value {
2628                let c_val = json_to_c(val);
2629                let _ = writeln!(
2630                    out,
2631                    "    assert({expr} != NULL && str_trim_eq({expr}, {c_val}) == 0 && \"streaming equals assertion failed\");"
2632                );
2633            }
2634        }
2635        ("contains", Kind::Str) => {
2636            if let Some(val) = &assertion.value {
2637                let c_val = json_to_c(val);
2638                let _ = writeln!(
2639                    out,
2640                    "    assert({expr} != NULL && strstr({expr}, {c_val}) != NULL && \"streaming contains assertion failed\");"
2641                );
2642            }
2643        }
2644        ("not_empty", Kind::Str) => {
2645            let _ = writeln!(
2646                out,
2647                "    assert({expr} != NULL && strlen({expr}) > 0 && \"expected non-empty {field}\");"
2648            );
2649        }
2650        ("is_true", Kind::Bool) => {
2651            let _ = writeln!(out, "    assert({expr} && \"expected {field} to be true\");");
2652        }
2653        ("is_false", Kind::Bool) => {
2654            let _ = writeln!(out, "    assert(!{expr} && \"expected {field} to be false\");");
2655        }
2656        ("greater_than_or_equal", Kind::IntCount) | ("greater_than_or_equal", Kind::IntTokens) => {
2657            if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2658                let _ = writeln!(out, "    assert({expr} >= {n} && \"expected {expr} >= {n}\");");
2659            }
2660        }
2661        ("equals", Kind::IntCount) | ("equals", Kind::IntTokens) => {
2662            if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2663                let _ = writeln!(out, "    assert({expr} == {n} && \"equals assertion failed\");");
2664            }
2665        }
2666        _ => {
2667            let _ = writeln!(
2668                out,
2669                "    /* skipped: streaming assertion '{atype}' on field '{field}' not supported */"
2670            );
2671        }
2672    }
2673}
2674
2675/// Emit chained FFI accessor calls for a nested resolved field path.
2676///
2677/// For a path like `metadata.document.title`, this generates:
2678/// ```c
2679/// HTMHtmlMetadata* metadata_handle = htm_conversion_result_metadata(result);
2680/// assert(metadata_handle != NULL);
2681/// HTMDocumentMetadata* doc_handle = htm_html_metadata_document(metadata_handle);
2682/// assert(doc_handle != NULL);
2683/// char* metadata_title = htm_document_metadata_title(doc_handle);
2684/// ```
2685///
2686/// The type chain is looked up from `fields_c_types` which maps
2687/// `"{parent_snake_type}.{field}"` -> `"PascalCaseType"`.
2688#[allow(clippy::too_many_arguments)]
2689fn emit_nested_accessor(
2690    out: &mut String,
2691    prefix: &str,
2692    resolved: &str,
2693    local_var: &str,
2694    result_var: &str,
2695    fields_c_types: &HashMap<String, String>,
2696    fields_enum: &HashSet<String>,
2697    intermediate_handles: &mut Vec<(String, String)>,
2698    result_type_name: &str,
2699    raw_field: &str,
2700) -> Option<String> {
2701    let segments: Vec<&str> = resolved.split('.').collect();
2702    let prefix_upper = prefix.to_uppercase();
2703
2704    // Walk the path, starting from the root result type.
2705    let mut current_snake_type = result_type_name.to_snake_case();
2706    let mut current_handle = result_var.to_string();
2707    // Set to true when we've traversed a `[]` array element accessor and subsequent
2708    // fields must be extracted via alef_json_get_string rather than FFI function calls.
2709    let mut json_extract_mode = false;
2710
2711    for (i, segment) in segments.iter().enumerate() {
2712        let is_leaf = i + 1 == segments.len();
2713
2714        // In JSON extraction mode, the current_handle is a JSON string and all
2715        // segments name keys to extract via alef_json_get_string (for primitive
2716        // leaves) or alef_json_get_object (for intermediate object hops).
2717        if json_extract_mode {
2718            // Decompose `field` or `field[N]`/`field[]`. Numeric indexing must
2719            // extract the Nth element so later key lookups don't ambiguously
2720            // pick the first occurrence (matters for fixtures with multiple
2721            // array elements like `data[0]`/`data[1]`).
2722            let (bare_segment, bracket_key): (&str, Option<&str>) = match segment.find('[') {
2723                Some(pos) => (&segment[..pos], Some(segment[pos + 1..].trim_end_matches(']'))),
2724                None => (segment, None),
2725            };
2726            let seg_snake = bare_segment.to_snake_case();
2727            if is_leaf {
2728                let _ = writeln!(
2729                    out,
2730                    "    char* {local_var} = alef_json_get_string({current_handle}, \"{seg_snake}\");"
2731                );
2732                return None; // JSON key leaf — char*.
2733            }
2734            // Intermediate JSON key — must be an object/array value. Use the
2735            // object extractor so the substring includes braces/brackets and
2736            // downstream primitive lookups against it find their keys
2737            // (alef_json_get_string would return NULL on non-string values).
2738            let json_var = format!("{seg_snake}_json");
2739            if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2740                let _ = writeln!(
2741                    out,
2742                    "    char* {json_var} = alef_json_get_object({current_handle}, \"{seg_snake}\");"
2743                );
2744                intermediate_handles.push((json_var.clone(), "free".to_string()));
2745            }
2746            // If the segment also includes a numeric index `[N]`, drill into
2747            // the Nth element of the extracted array; otherwise stay on the
2748            // object/array substring.
2749            if let Some(key) = bracket_key {
2750                if let Ok(idx) = key.parse::<usize>() {
2751                    let elem_var = format!("{seg_snake}_{idx}_json");
2752                    if !intermediate_handles.iter().any(|(h, _)| h == &elem_var) {
2753                        let _ = writeln!(
2754                            out,
2755                            "    char* {elem_var} = alef_json_array_get_index({json_var}, {idx});"
2756                        );
2757                        intermediate_handles.push((elem_var.clone(), "free".to_string()));
2758                    }
2759                    current_handle = elem_var;
2760                    continue;
2761                }
2762            }
2763            current_handle = json_var;
2764            continue;
2765        }
2766
2767        // Check for map access: "field[key]" or array element access: "field[]"
2768        if let Some(bracket_pos) = segment.find('[') {
2769            let field_name = &segment[..bracket_pos];
2770            let key = segment[bracket_pos + 1..].trim_end_matches(']');
2771            let field_snake = field_name.to_snake_case();
2772            let accessor_fn = format!("{prefix}_{current_snake_type}_{field_snake}");
2773
2774            // The accessor returns a char* (JSON object/array string).
2775            let json_var = format!("{field_snake}_json");
2776            if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2777                let _ = writeln!(out, "    char* {json_var} = {accessor_fn}({current_handle});");
2778                let _ = writeln!(out, "    assert({json_var} != NULL);");
2779                // Track for freeing — use prefix_free_string since it's a char*.
2780                intermediate_handles.push((json_var.clone(), "free_string".to_string()));
2781            }
2782
2783            // Empty key `[]`: array-element substring access (any element matches).
2784            // Numeric key `[N]` (e.g. `choices[0]`, `data[1]`): extract the exact
2785            // Nth top-level element so subsequent key lookups don't ambiguously
2786            // pick the first occurrence — required for fixtures whose results
2787            // contain multiple array elements (e.g. `data[0].index`/`data[1].index`).
2788            if key.is_empty() {
2789                if !is_leaf {
2790                    current_handle = json_var;
2791                    json_extract_mode = true;
2792                    continue;
2793                }
2794                return None;
2795            }
2796            if let Ok(idx) = key.parse::<usize>() {
2797                let elem_var = format!("{field_snake}_{idx}_json");
2798                if !intermediate_handles.iter().any(|(h, _)| h == &elem_var) {
2799                    let _ = writeln!(
2800                        out,
2801                        "    char* {elem_var} = alef_json_array_get_index({json_var}, {idx});"
2802                    );
2803                    intermediate_handles.push((elem_var.clone(), "free".to_string()));
2804                }
2805                if !is_leaf {
2806                    current_handle = elem_var;
2807                    json_extract_mode = true;
2808                    continue;
2809                }
2810                // Trailing `[N]` — caller asserts on the element JSON.
2811                return None;
2812            }
2813
2814            // Named map key access: extract the key value from the JSON object.
2815            let _ = writeln!(
2816                out,
2817                "    char* {local_var} = alef_json_get_string({json_var}, \"{key}\");"
2818            );
2819            return None; // Map access leaf — char*.
2820        }
2821
2822        let seg_snake = segment.to_snake_case();
2823        let accessor_fn = format!("{prefix}_{current_snake_type}_{seg_snake}");
2824
2825        if is_leaf {
2826            // Leaf may be a primitive scalar (uint64_t, double, ...) when
2827            // configured in `fields_c_types`. Otherwise default to char*.
2828            let lookup_key = format!("{current_snake_type}.{seg_snake}");
2829            if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
2830                let _ = writeln!(out, "    {t} {local_var} = {accessor_fn}({current_handle});");
2831                return Some(t.clone());
2832            }
2833            // Enum leaf: opaque enum pointer that needs `_to_string` conversion.
2834            if try_emit_enum_accessor(
2835                out,
2836                prefix,
2837                &prefix_upper,
2838                raw_field,
2839                &seg_snake,
2840                &current_snake_type,
2841                &accessor_fn,
2842                &current_handle,
2843                local_var,
2844                fields_c_types,
2845                fields_enum,
2846                intermediate_handles,
2847            ) {
2848                return None;
2849            }
2850            let _ = writeln!(out, "    char* {local_var} = {accessor_fn}({current_handle});");
2851        } else {
2852            // Intermediate field — check if it's a char* (JSON string/array) or an opaque handle.
2853            let lookup_key = format!("{current_snake_type}.{seg_snake}");
2854            let return_type_pascal = match fields_c_types.get(&lookup_key) {
2855                Some(t) => t.clone(),
2856                None => {
2857                    // Fallback: derive PascalCase from the segment name itself.
2858                    segment.to_pascal_case()
2859                }
2860            };
2861
2862            // Special case: intermediate char* fields (e.g. links, assets) are JSON
2863            // strings/arrays, not opaque handles. For a `.length` suffix, emit alef_json_array_count.
2864            if return_type_pascal == "char*" {
2865                let json_var = format!("{seg_snake}_json");
2866                if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2867                    let _ = writeln!(out, "    char* {json_var} = {accessor_fn}({current_handle});");
2868                    intermediate_handles.push((json_var.clone(), "free_string".to_string()));
2869                }
2870                // If the next (and final) segment is "length", emit the count accessor.
2871                if i + 2 == segments.len() && segments[i + 1] == "length" {
2872                    let _ = writeln!(out, "    int {local_var} = alef_json_array_count({json_var});");
2873                    return Some("int".to_string());
2874                }
2875                current_snake_type = seg_snake.clone();
2876                current_handle = json_var;
2877                continue;
2878            }
2879
2880            let return_snake = return_type_pascal.to_snake_case();
2881            let handle_var = format!("{seg_snake}_handle");
2882
2883            // Only emit the handle if we haven't already (multiple fields may
2884            // share the same intermediate path prefix).
2885            if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
2886                let _ = writeln!(
2887                    out,
2888                    "    {prefix_upper}{return_type_pascal}* {handle_var} = \
2889                     {accessor_fn}({current_handle});"
2890                );
2891                let _ = writeln!(out, "    assert({handle_var} != NULL);");
2892                intermediate_handles.push((handle_var.clone(), return_snake.clone()));
2893            }
2894
2895            current_snake_type = return_snake;
2896            current_handle = handle_var;
2897        }
2898    }
2899    None
2900}
2901
2902/// Build the C argument string for the function call.
2903/// When `has_options_handle` is true, json_object args are replaced with
2904/// the `options_handle` pointer (which was constructed via `from_json`).
2905fn build_args_string_c(
2906    input: &serde_json::Value,
2907    args: &[crate::config::ArgMapping],
2908    has_options_handle: bool,
2909) -> String {
2910    if args.is_empty() {
2911        return json_to_c(input);
2912    }
2913
2914    let parts: Vec<String> = args
2915        .iter()
2916        .filter_map(|arg| {
2917            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2918            let val = input.get(field);
2919            match val {
2920                // Field missing entirely and optional → pass NULL.
2921                None if arg.optional => Some("NULL".to_string()),
2922                // Field missing and required → skip (caller error, but don't crash).
2923                None => None,
2924                // Explicit null on optional arg → pass NULL.
2925                Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
2926                Some(v) => {
2927                    // For json_object args, use the options_handle pointer
2928                    // instead of the raw JSON string.
2929                    if arg.arg_type == "json_object" && has_options_handle && !v.is_null() {
2930                        Some("options_handle".to_string())
2931                    } else {
2932                        Some(json_to_c(v))
2933                    }
2934                }
2935            }
2936        })
2937        .collect();
2938
2939    parts.join(", ")
2940}
2941
2942#[allow(clippy::too_many_arguments)]
2943fn render_assertion(
2944    out: &mut String,
2945    assertion: &Assertion,
2946    result_var: &str,
2947    ffi_prefix: &str,
2948    _field_resolver: &FieldResolver,
2949    accessed_fields: &[(String, String, bool)],
2950    primitive_locals: &HashMap<String, String>,
2951    opaque_handle_locals: &HashMap<String, String>,
2952) {
2953    // Skip assertions on fields that don't exist on the result type.
2954    if let Some(f) = &assertion.field {
2955        if !f.is_empty() && !_field_resolver.is_valid_for_result(f) {
2956            let _ = writeln!(out, "    // skipped: field '{f}' not available on result type");
2957            return;
2958        }
2959    }
2960
2961    let field_expr = match &assertion.field {
2962        Some(f) if !f.is_empty() => {
2963            // Use the local variable extracted from the opaque handle.
2964            accessed_fields
2965                .iter()
2966                .find(|(k, _, _)| k == f)
2967                .map(|(_, local, _)| local.clone())
2968                .unwrap_or_else(|| result_var.to_string())
2969        }
2970        _ => result_var.to_string(),
2971    };
2972
2973    let field_is_primitive = primitive_locals.contains_key(&field_expr);
2974    let field_primitive_type = primitive_locals.get(&field_expr).cloned();
2975    // Opaque-handle fields (e.g. `usage` → LITERLLMUsage*) cannot be treated
2976    // as C strings — `strlen` / `strcmp` on a struct pointer is undefined
2977    // behavior (SIGABRT in practice). `not_empty` / `is_empty` collapse to
2978    // NULL checks; other string assertions are skipped for these fields.
2979    let field_is_opaque_handle = opaque_handle_locals.contains_key(&field_expr);
2980    // Map-access fields are extracted via `alef_json_get_string` and end up
2981    // as char*. When the assertion expects a numeric or boolean value, we
2982    // emit a parsed/literal comparison rather than `strcmp`.
2983    let field_is_map_access = if let Some(f) = &assertion.field {
2984        accessed_fields.iter().any(|(k, _, m)| k == f && *m)
2985    } else {
2986        false
2987    };
2988
2989    // Check if the assertion field is optional — used to emit conditional assertions
2990    // for optional numeric fields (returns 0 when None, so 0 == "not set").
2991    // Check both the raw field name and its resolved alias.
2992    let assertion_field_is_optional = assertion
2993        .field
2994        .as_deref()
2995        .map(|f| {
2996            if f.is_empty() {
2997                return false;
2998            }
2999            if _field_resolver.is_optional(f) {
3000                return true;
3001            }
3002            // Also check the resolved alias (e.g. "robots.crawl_delay" → "crawl_delay").
3003            let resolved = _field_resolver.resolve(f);
3004            _field_resolver.is_optional(resolved)
3005        })
3006        .unwrap_or(false);
3007
3008    match assertion.assertion_type.as_str() {
3009        "equals" => {
3010            if let Some(expected) = &assertion.value {
3011                let c_val = json_to_c(expected);
3012                if field_is_primitive {
3013                    let cmp_val = if field_primitive_type.as_deref() == Some("bool") {
3014                        match expected.as_bool() {
3015                            Some(true) => "1".to_string(),
3016                            Some(false) => "0".to_string(),
3017                            None => c_val,
3018                        }
3019                    } else {
3020                        c_val
3021                    };
3022                    // For optional numeric fields, treat 0 as "not set" and allow it.
3023                    // This mirrors Go's nil-pointer check for optional fields.
3024                    let is_numeric = field_primitive_type.as_deref().map(|t| t != "bool").unwrap_or(false);
3025                    if assertion_field_is_optional && is_numeric {
3026                        let _ = writeln!(
3027                            out,
3028                            "    assert(({field_expr} == 0 || {field_expr} == {cmp_val}) && \"equals assertion failed\");"
3029                        );
3030                    } else {
3031                        let _ = writeln!(
3032                            out,
3033                            "    assert({field_expr} == {cmp_val} && \"equals assertion failed\");"
3034                        );
3035                    }
3036                } else if expected.is_string() {
3037                    let _ = writeln!(
3038                        out,
3039                        "    assert(str_trim_eq({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
3040                    );
3041                } else if field_is_map_access && expected.is_boolean() {
3042                    let lit = match expected.as_bool() {
3043                        Some(true) => "\"true\"",
3044                        _ => "\"false\"",
3045                    };
3046                    let _ = writeln!(
3047                        out,
3048                        "    assert({field_expr} != NULL && strcmp({field_expr}, {lit}) == 0 && \"equals assertion failed\");"
3049                    );
3050                } else if field_is_map_access && expected.is_number() {
3051                    if expected.is_f64() {
3052                        let _ = writeln!(
3053                            out,
3054                            "    assert({field_expr} != NULL && atof({field_expr}) == {c_val} && \"equals assertion failed\");"
3055                        );
3056                    } else {
3057                        let _ = writeln!(
3058                            out,
3059                            "    assert({field_expr} != NULL && atoll({field_expr}) == {c_val} && \"equals assertion failed\");"
3060                        );
3061                    }
3062                } else {
3063                    let _ = writeln!(
3064                        out,
3065                        "    assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
3066                    );
3067                }
3068            }
3069        }
3070        "contains" => {
3071            if let Some(expected) = &assertion.value {
3072                let c_val = json_to_c(expected);
3073                let _ = writeln!(
3074                    out,
3075                    "    assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
3076                );
3077            }
3078        }
3079        "contains_all" => {
3080            if let Some(values) = &assertion.values {
3081                for val in values {
3082                    let c_val = json_to_c(val);
3083                    let _ = writeln!(
3084                        out,
3085                        "    assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
3086                    );
3087                }
3088            }
3089        }
3090        "not_contains" => {
3091            if let Some(expected) = &assertion.value {
3092                let c_val = json_to_c(expected);
3093                let _ = writeln!(
3094                    out,
3095                    "    assert(({field_expr} == NULL || strstr({field_expr}, {c_val}) == NULL) && \"expected NOT to contain substring\");"
3096                );
3097            }
3098        }
3099        "not_empty" => {
3100            if field_is_opaque_handle {
3101                // Opaque struct handle: `strlen` on a struct pointer is UB.
3102                // Weaken to a non-null check — strictly weaker than the
3103                // original intent but won't false-trigger SIGABRT.
3104                let _ = writeln!(out, "    assert({field_expr} != NULL && \"expected non-null handle\");");
3105            } else {
3106                let _ = writeln!(
3107                    out,
3108                    "    assert({field_expr} != NULL && strlen({field_expr}) > 0 && \"expected non-empty value\");"
3109                );
3110            }
3111        }
3112        "is_empty" => {
3113            if field_is_opaque_handle {
3114                let _ = writeln!(out, "    assert({field_expr} == NULL && \"expected null handle\");");
3115            } else if assertion_field_is_optional || !field_is_primitive {
3116                // Optional string fields may return NULL — treat NULL as empty.
3117                let _ = writeln!(
3118                    out,
3119                    "    assert(({field_expr} == NULL || strlen({field_expr}) == 0) && \"expected empty value\");"
3120                );
3121            } else {
3122                let _ = writeln!(
3123                    out,
3124                    "    assert(strlen({field_expr}) == 0 && \"expected empty value\");"
3125                );
3126            }
3127        }
3128        "contains_any" => {
3129            if let Some(values) = &assertion.values {
3130                let _ = writeln!(out, "    {{");
3131                let _ = writeln!(out, "        int found = 0;");
3132                for val in values {
3133                    let c_val = json_to_c(val);
3134                    let _ = writeln!(
3135                        out,
3136                        "        if (strstr({field_expr}, {c_val}) != NULL) {{ found = 1; }}"
3137                    );
3138                }
3139                let _ = writeln!(
3140                    out,
3141                    "        assert(found && \"expected to contain at least one of the specified values\");"
3142                );
3143                let _ = writeln!(out, "    }}");
3144            }
3145        }
3146        "greater_than" => {
3147            if let Some(val) = &assertion.value {
3148                let c_val = json_to_c(val);
3149                if field_is_map_access && val.is_number() && !field_is_primitive {
3150                    let _ = writeln!(
3151                        out,
3152                        "    assert({field_expr} != NULL && atof({field_expr}) > {c_val} && \"expected greater than\");"
3153                    );
3154                } else {
3155                    let _ = writeln!(out, "    assert({field_expr} > {c_val} && \"expected greater than\");");
3156                }
3157            }
3158        }
3159        "less_than" => {
3160            if let Some(val) = &assertion.value {
3161                let c_val = json_to_c(val);
3162                if field_is_map_access && val.is_number() && !field_is_primitive {
3163                    let _ = writeln!(
3164                        out,
3165                        "    assert({field_expr} != NULL && atof({field_expr}) < {c_val} && \"expected less than\");"
3166                    );
3167                } else {
3168                    let _ = writeln!(out, "    assert({field_expr} < {c_val} && \"expected less than\");");
3169                }
3170            }
3171        }
3172        "greater_than_or_equal" => {
3173            if let Some(val) = &assertion.value {
3174                let c_val = json_to_c(val);
3175                if field_is_map_access && val.is_number() && !field_is_primitive {
3176                    let _ = writeln!(
3177                        out,
3178                        "    assert({field_expr} != NULL && atof({field_expr}) >= {c_val} && \"expected greater than or equal\");"
3179                    );
3180                } else {
3181                    let _ = writeln!(
3182                        out,
3183                        "    assert({field_expr} >= {c_val} && \"expected greater than or equal\");"
3184                    );
3185                }
3186            }
3187        }
3188        "less_than_or_equal" => {
3189            if let Some(val) = &assertion.value {
3190                let c_val = json_to_c(val);
3191                if field_is_map_access && val.is_number() && !field_is_primitive {
3192                    let _ = writeln!(
3193                        out,
3194                        "    assert({field_expr} != NULL && atof({field_expr}) <= {c_val} && \"expected less than or equal\");"
3195                    );
3196                } else {
3197                    let _ = writeln!(
3198                        out,
3199                        "    assert({field_expr} <= {c_val} && \"expected less than or equal\");"
3200                    );
3201                }
3202            }
3203        }
3204        "starts_with" => {
3205            if let Some(expected) = &assertion.value {
3206                let c_val = json_to_c(expected);
3207                let _ = writeln!(
3208                    out,
3209                    "    assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
3210                );
3211            }
3212        }
3213        "ends_with" => {
3214            if let Some(expected) = &assertion.value {
3215                let c_val = json_to_c(expected);
3216                let _ = writeln!(out, "    assert(strlen({field_expr}) >= strlen({c_val}) && ");
3217                let _ = writeln!(
3218                    out,
3219                    "           strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
3220                );
3221            }
3222        }
3223        "min_length" => {
3224            if let Some(val) = &assertion.value {
3225                if let Some(n) = val.as_u64() {
3226                    let _ = writeln!(
3227                        out,
3228                        "    assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
3229                    );
3230                }
3231            }
3232        }
3233        "max_length" => {
3234            if let Some(val) = &assertion.value {
3235                if let Some(n) = val.as_u64() {
3236                    let _ = writeln!(
3237                        out,
3238                        "    assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
3239                    );
3240                }
3241            }
3242        }
3243        "count_min" => {
3244            if let Some(val) = &assertion.value {
3245                if let Some(n) = val.as_u64() {
3246                    let _ = writeln!(out, "    {{");
3247                    let _ = writeln!(out, "        /* count_min: count top-level JSON array elements */");
3248                    let _ = writeln!(
3249                        out,
3250                        "        assert({field_expr} != NULL && \"expected non-null collection JSON\");"
3251                    );
3252                    let _ = writeln!(out, "        int elem_count = alef_json_array_count({field_expr});");
3253                    let _ = writeln!(
3254                        out,
3255                        "        assert(elem_count >= {n} && \"expected at least {n} elements\");"
3256                    );
3257                    let _ = writeln!(out, "    }}");
3258                }
3259            }
3260        }
3261        "count_equals" => {
3262            if let Some(val) = &assertion.value {
3263                if let Some(n) = val.as_u64() {
3264                    let _ = writeln!(out, "    {{");
3265                    let _ = writeln!(out, "        /* count_equals: count elements in array */");
3266                    let _ = writeln!(
3267                        out,
3268                        "        assert({field_expr} != NULL && \"expected non-null collection JSON\");"
3269                    );
3270                    let _ = writeln!(out, "        int elem_count = alef_json_array_count({field_expr});");
3271                    let _ = writeln!(out, "        assert(elem_count == {n} && \"expected {n} elements\");");
3272                    let _ = writeln!(out, "    }}");
3273                }
3274            }
3275        }
3276        "is_true" => {
3277            let _ = writeln!(out, "    assert({field_expr});");
3278        }
3279        "is_false" => {
3280            let _ = writeln!(out, "    assert(!{field_expr});");
3281        }
3282        "method_result" => {
3283            if let Some(method_name) = &assertion.method {
3284                render_method_result_assertion(
3285                    out,
3286                    result_var,
3287                    ffi_prefix,
3288                    method_name,
3289                    assertion.args.as_ref(),
3290                    assertion.return_type.as_deref(),
3291                    assertion.check.as_deref().unwrap_or("is_true"),
3292                    assertion.value.as_ref(),
3293                );
3294            } else {
3295                panic!("C e2e generator: method_result assertion missing 'method' field");
3296            }
3297        }
3298        "matches_regex" => {
3299            if let Some(expected) = &assertion.value {
3300                let c_val = json_to_c(expected);
3301                let _ = writeln!(out, "    {{");
3302                let _ = writeln!(out, "        regex_t _re;");
3303                let _ = writeln!(
3304                    out,
3305                    "        assert(regcomp(&_re, {c_val}, REG_EXTENDED) == 0 && \"regex compile failed\");"
3306                );
3307                let _ = writeln!(
3308                    out,
3309                    "        assert(regexec(&_re, {field_expr}, 0, NULL, 0) == 0 && \"expected value to match regex\");"
3310                );
3311                let _ = writeln!(out, "        regfree(&_re);");
3312                let _ = writeln!(out, "    }}");
3313            }
3314        }
3315        "not_error" => {
3316            // Already handled — the NULL check above covers this.
3317        }
3318        "error" => {
3319            // Handled at the test function level.
3320        }
3321        other => {
3322            panic!("C e2e generator: unsupported assertion type: {other}");
3323        }
3324    }
3325}
3326
3327/// Render a `method_result` assertion in C.
3328///
3329/// Dispatches generically using `{ffi_prefix}_{method_name}` for the FFI call.
3330/// The `return_type` fixture field controls how the return value is handled:
3331/// - `"string"` — the method returns a heap-allocated `char*`; the generator
3332///   emits a scoped block that asserts, then calls `free()`.
3333/// - absent/other — treated as a primitive integer (or pointer-as-bool); the
3334///   assertion is emitted inline without any heap management.
3335#[allow(clippy::too_many_arguments)]
3336fn render_method_result_assertion(
3337    out: &mut String,
3338    result_var: &str,
3339    ffi_prefix: &str,
3340    method_name: &str,
3341    args: Option<&serde_json::Value>,
3342    return_type: Option<&str>,
3343    check: &str,
3344    value: Option<&serde_json::Value>,
3345) {
3346    let call_expr = build_c_method_call(result_var, ffi_prefix, method_name, args);
3347
3348    if return_type == Some("string") {
3349        // Heap-allocated char* return: emit a scoped block, assert, then free.
3350        let _ = writeln!(out, "    {{");
3351        let _ = writeln!(out, "        char* _method_result = {call_expr};");
3352        if check == "is_error" {
3353            let _ = writeln!(
3354                out,
3355                "        assert(_method_result == NULL && \"expected method to return error\");"
3356            );
3357            let _ = writeln!(out, "    }}");
3358            return;
3359        }
3360        let _ = writeln!(
3361            out,
3362            "        assert(_method_result != NULL && \"method_result returned NULL\");"
3363        );
3364        match check {
3365            "contains" => {
3366                if let Some(val) = value {
3367                    let c_val = json_to_c(val);
3368                    let _ = writeln!(
3369                        out,
3370                        "        assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
3371                    );
3372                }
3373            }
3374            "equals" => {
3375                if let Some(val) = value {
3376                    let c_val = json_to_c(val);
3377                    let _ = writeln!(
3378                        out,
3379                        "        assert(str_trim_eq(_method_result, {c_val}) == 0 && \"method_result equals assertion failed\");"
3380                    );
3381                }
3382            }
3383            "is_true" => {
3384                let _ = writeln!(
3385                    out,
3386                    "        assert(_method_result != NULL && strlen(_method_result) > 0 && \"method_result is_true assertion failed\");"
3387                );
3388            }
3389            "count_min" => {
3390                if let Some(val) = value {
3391                    let n = val.as_u64().unwrap_or(0);
3392                    let _ = writeln!(out, "        int _elem_count = alef_json_array_count(_method_result);");
3393                    let _ = writeln!(
3394                        out,
3395                        "        assert(_elem_count >= {n} && \"method_result count_min assertion failed\");"
3396                    );
3397                }
3398            }
3399            other_check => {
3400                panic!("C e2e generator: unsupported method_result check type for string return: {other_check}");
3401            }
3402        }
3403        let _ = writeln!(out, "        free(_method_result);");
3404        let _ = writeln!(out, "    }}");
3405        return;
3406    }
3407
3408    // Primitive (integer / pointer-as-bool) return: inline assert, no heap management.
3409    match check {
3410        "equals" => {
3411            if let Some(val) = value {
3412                let c_val = json_to_c(val);
3413                let _ = writeln!(
3414                    out,
3415                    "    assert({call_expr} == {c_val} && \"method_result equals assertion failed\");"
3416                );
3417            }
3418        }
3419        "is_true" => {
3420            let _ = writeln!(
3421                out,
3422                "    assert({call_expr} && \"method_result is_true assertion failed\");"
3423            );
3424        }
3425        "is_false" => {
3426            let _ = writeln!(
3427                out,
3428                "    assert(!{call_expr} && \"method_result is_false assertion failed\");"
3429            );
3430        }
3431        "greater_than_or_equal" => {
3432            if let Some(val) = value {
3433                let n = val.as_u64().unwrap_or(0);
3434                let _ = writeln!(
3435                    out,
3436                    "    assert({call_expr} >= {n} && \"method_result >= {n} assertion failed\");"
3437                );
3438            }
3439        }
3440        "count_min" => {
3441            if let Some(val) = value {
3442                let n = val.as_u64().unwrap_or(0);
3443                let _ = writeln!(
3444                    out,
3445                    "    assert({call_expr} >= {n} && \"method_result count_min assertion failed\");"
3446                );
3447            }
3448        }
3449        other_check => {
3450            panic!("C e2e generator: unsupported method_result check type: {other_check}");
3451        }
3452    }
3453}
3454
3455/// Build a C call expression for a `method_result` assertion.
3456///
3457/// Uses generic dispatch: `{ffi_prefix}_{method_name}(result_var, args...)`.
3458/// Args from the fixture JSON object are emitted as positional C arguments in
3459/// insertion order, using best-effort type conversion (strings → C string literals,
3460/// numbers and booleans → verbatim literals).
3461fn build_c_method_call(
3462    result_var: &str,
3463    ffi_prefix: &str,
3464    method_name: &str,
3465    args: Option<&serde_json::Value>,
3466) -> String {
3467    let extra_args = if let Some(args_val) = args {
3468        args_val
3469            .as_object()
3470            .map(|obj| {
3471                obj.values()
3472                    .map(|v| match v {
3473                        serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
3474                        serde_json::Value::Bool(true) => "1".to_string(),
3475                        serde_json::Value::Bool(false) => "0".to_string(),
3476                        serde_json::Value::Number(n) => n.to_string(),
3477                        serde_json::Value::Null => "NULL".to_string(),
3478                        other => format!("\"{}\"", escape_c(&other.to_string())),
3479                    })
3480                    .collect::<Vec<_>>()
3481                    .join(", ")
3482            })
3483            .unwrap_or_default()
3484    } else {
3485        String::new()
3486    };
3487
3488    if extra_args.is_empty() {
3489        format!("{ffi_prefix}_{method_name}({result_var})")
3490    } else {
3491        format!("{ffi_prefix}_{method_name}({result_var}, {extra_args})")
3492    }
3493}
3494
3495/// Convert a `serde_json::Value` to a C literal string.
3496fn json_to_c(value: &serde_json::Value) -> String {
3497    match value {
3498        serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
3499        serde_json::Value::Bool(true) => "1".to_string(),
3500        serde_json::Value::Bool(false) => "0".to_string(),
3501        serde_json::Value::Number(n) => n.to_string(),
3502        serde_json::Value::Null => "NULL".to_string(),
3503        other => format!("\"{}\"", escape_c(&other.to_string())),
3504    }
3505}