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