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        render_test_function(
1001            &mut out,
1002            fixture,
1003            prefix,
1004            &call_info.function_name,
1005            result_var,
1006            &call_info.args,
1007            field_resolver,
1008            &e2e_config.fields_c_types,
1009            &effective_fields_enum,
1010            &call_info.result_type_name,
1011            &call_info.options_type_name,
1012            call_info.client_factory.as_deref(),
1013            call_info.raw_c_result_type.as_deref(),
1014            call_info.c_free_fn.as_deref(),
1015            call_info.c_engine_factory.as_deref(),
1016            call_info.result_is_option,
1017            call_info.result_is_bytes,
1018            &call_info.extra_args,
1019        );
1020        if i + 1 < fixtures.len() {
1021            let _ = writeln!(out);
1022        }
1023    }
1024
1025    out
1026}
1027
1028#[allow(clippy::too_many_arguments)]
1029fn render_test_function(
1030    out: &mut String,
1031    fixture: &Fixture,
1032    prefix: &str,
1033    function_name: &str,
1034    result_var: &str,
1035    args: &[crate::config::ArgMapping],
1036    field_resolver: &FieldResolver,
1037    fields_c_types: &HashMap<String, String>,
1038    fields_enum: &HashSet<String>,
1039    result_type_name: &str,
1040    options_type_name: &str,
1041    client_factory: Option<&str>,
1042    raw_c_result_type: Option<&str>,
1043    c_free_fn: Option<&str>,
1044    c_engine_factory: Option<&str>,
1045    result_is_option: bool,
1046    result_is_bytes: bool,
1047    extra_args: &[String],
1048) {
1049    let fn_name = sanitize_ident(&fixture.id);
1050    let description = &fixture.description;
1051
1052    let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
1053
1054    let _ = writeln!(out, "void test_{fn_name}(void) {{");
1055    let _ = writeln!(out, "    /* {description} */");
1056
1057    // Smoke/live fixtures gated on a required env var (e.g. OPENAI_API_KEY).
1058    // When the var is missing, treat as a successful skip — mirrors Python's
1059    // `pytest.skip("OPENAI_API_KEY not set")` and Java's `Assumptions.assumeTrue(...)`
1060    // so CI runs without provider credentials don't fail every smoke test.
1061    //
1062    // When the fixture also has a mock_response/http block, we support an env+mock
1063    // fallback: if the API key is set, use the real API; otherwise fall back to the
1064    // mock server. This lets the same fixture exercise both paths.
1065    let has_mock = fixture.needs_mock_server();
1066    let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
1067    if let Some(env) = &fixture.env {
1068        if let Some(var) = &env.api_key_var {
1069            let fixture_id = &fixture.id;
1070            if has_mock {
1071                let _ = writeln!(out, "    const char* api_key = getenv(\"{var}\");");
1072                let _ = writeln!(out, "    const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1073                let _ = writeln!(out, "    char base_url_buf[512];");
1074                let _ = writeln!(out, "    int use_mock = !(api_key && api_key[0] != '\\0');");
1075                let _ = writeln!(out, "    if (!use_mock) {{");
1076                let _ = writeln!(
1077                    out,
1078                    "        fprintf(stderr, \"{fixture_id}: using real API ({var} is set)\\n\");"
1079                );
1080                let _ = writeln!(out, "    }} else {{");
1081                let _ = writeln!(
1082                    out,
1083                    "        fprintf(stderr, \"{fixture_id}: using mock server ({var} not set)\\n\");"
1084                );
1085                let _ = writeln!(
1086                    out,
1087                    "        snprintf(base_url_buf, sizeof(base_url_buf), \"%s/fixtures/{fixture_id}\", mock_base ? mock_base : \"\");"
1088                );
1089                let _ = writeln!(out, "        api_key = \"test-key\";");
1090                let _ = writeln!(out, "    }}");
1091            } else {
1092                let _ = writeln!(out, "    if (getenv(\"{var}\") == NULL) {{ return; }}");
1093            }
1094        }
1095    }
1096
1097    let prefix_upper = prefix.to_uppercase();
1098
1099    // Engine-factory pattern: used when c_engine_factory is configured (e.g. kreuzcrawl).
1100    // Creates a config handle from JSON, builds an engine, calls {prefix}_{function}(engine, url),
1101    // frees result and engine.
1102    if let Some(config_type) = c_engine_factory {
1103        render_engine_factory_test_function(
1104            out,
1105            fixture,
1106            prefix,
1107            function_name,
1108            result_var,
1109            field_resolver,
1110            fields_c_types,
1111            fields_enum,
1112            result_type_name,
1113            config_type,
1114            expects_error,
1115            raw_c_result_type,
1116        );
1117        return;
1118    }
1119
1120    // Streaming pattern: chat_stream uses an FFI iterator handle instead of a
1121    // single response. Emit start/next/free loop and aggregate per-chunk data
1122    // into local vars (chunks_count, stream_content, stream_complete) so fixture
1123    // assertions on pseudo-fields resolve to those locals rather than to
1124    // non-existent accessor functions on a single chunk handle.
1125    if client_factory.is_some() && function_name == "chat_stream" {
1126        render_chat_stream_test_function(
1127            out,
1128            fixture,
1129            prefix,
1130            result_var,
1131            args,
1132            options_type_name,
1133            expects_error,
1134            api_key_var,
1135        );
1136        return;
1137    }
1138
1139    // Byte-buffer pattern: methods like `speech` and `file_content` return raw
1140    // bytes via the out-pointer FFI shape:
1141    //   `int32_t fn(this, req, uint8_t** out_ptr, uintptr_t* out_len, uintptr_t* out_cap)`
1142    // rather than as an opaque `*Response` handle. The C codegen must declare
1143    // the out-params, check the int32_t status code, and free with
1144    // `<prefix>_free_bytes` rather than emitting non-existent
1145    // `<prefix>_<response>_audio` / `_content` accessors.
1146    if let Some(factory) = client_factory {
1147        if result_is_bytes {
1148            render_bytes_test_function(
1149                out,
1150                fixture,
1151                prefix,
1152                function_name,
1153                result_var,
1154                args,
1155                options_type_name,
1156                result_type_name,
1157                factory,
1158                expects_error,
1159            );
1160            return;
1161        }
1162    }
1163
1164    // Client pattern: used when client_factory is configured (e.g. liter-llm).
1165    // Builds typed request handles from json_object args, creates a client via the
1166    // factory function, calls {prefix}_default_client_{function_name}(client, req),
1167    // then frees result, request handles, and client.
1168    if let Some(factory) = client_factory {
1169        let mut request_handle_vars: Vec<(String, String)> = Vec::new(); // (arg_name, var_name)
1170        // Inline argument expressions appended after request handles in the
1171        // method call (e.g. literal C strings for `string` args, `NULL` for
1172        // optional pointer args). Order matches the position in `args`.
1173        let mut inline_method_args: Vec<String> = Vec::new();
1174
1175        for arg in args {
1176            if arg.arg_type == "json_object" {
1177                // Prefer options_type from the C override when set, since the result
1178                // type isn't always a clean strip-Response/append-Request transform
1179                // (e.g. transcribe -> Create**Transcription**Request, not TranscriptionRequest).
1180                // Fall back to deriving from result_type for backward-compat cases.
1181                let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
1182                    options_type_name.to_string()
1183                } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
1184                    format!("{}Request", stripped)
1185                } else {
1186                    format!("{result_type_name}Request")
1187                };
1188                let request_type_snake = request_type_pascal.to_snake_case();
1189                let var_name = format!("{request_type_snake}_handle");
1190
1191                let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1192                let json_val = if field.is_empty() || field == "input" {
1193                    Some(&fixture.input)
1194                } else {
1195                    fixture.input.get(field)
1196                };
1197
1198                if let Some(val) = json_val {
1199                    if !val.is_null() {
1200                        let normalized = super::transform_json_keys_for_language(val, "snake_case");
1201                        let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1202                        let escaped = escape_c(&json_str);
1203                        let _ = writeln!(
1204                            out,
1205                            "    {prefix_upper}{request_type_pascal}* {var_name} = \
1206                             {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
1207                        );
1208                        if expects_error {
1209                            // For error fixtures (e.g. invalid enum value rejected by
1210                            // serde), `_from_json` may legitimately return NULL — that
1211                            // counts as the expected failure. Mirror Java's pattern of
1212                            // wrapping setup + call inside `assertThrows(...)` so error
1213                            // fixtures pass at *any* failure step. The test returns
1214                            // before attempting to create a client, leaving no
1215                            // resources to free.
1216                            let _ = writeln!(out, "    if ({var_name} == NULL) {{ return; }}");
1217                        } else {
1218                            let _ = writeln!(out, "    assert({var_name} != NULL && \"failed to build request\");");
1219                        }
1220                        request_handle_vars.push((arg.name.clone(), var_name));
1221                    }
1222                }
1223            } else if arg.arg_type == "string" {
1224                // String arg: read fixture input, emit as a C string literal inline.
1225                let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1226                let val = fixture.input.get(field);
1227                match val {
1228                    Some(v) if v.is_string() => {
1229                        let s = v.as_str().unwrap_or_default();
1230                        let escaped = escape_c(s);
1231                        inline_method_args.push(format!("\"{escaped}\""));
1232                    }
1233                    Some(serde_json::Value::Null) | None if arg.optional => {
1234                        inline_method_args.push("NULL".to_string());
1235                    }
1236                    None => {
1237                        inline_method_args.push("\"\"".to_string());
1238                    }
1239                    Some(other) => {
1240                        let s = serde_json::to_string(other).unwrap_or_default();
1241                        let escaped = escape_c(&s);
1242                        inline_method_args.push(format!("\"{escaped}\""));
1243                    }
1244                }
1245            } else if arg.optional {
1246                // Optional non-string, non-json_object arg: pass NULL.
1247                inline_method_args.push("NULL".to_string());
1248            }
1249        }
1250
1251        let fixture_id = &fixture.id;
1252        // Pass UINT64_MAX/UINT32_MAX (≡ -1ULL/-1U) as the FFI's None sentinel for
1253        // optional numeric primitives — passing literal 0 makes the binding see
1254        // Some(0), which Rust core treats as `Duration::from_secs(0)` (immediate
1255        // request deadline) and breaks every HTTP fixture.
1256        if has_mock && api_key_var.is_some() {
1257            // api_key and base_url_buf are already declared in the env-fallback block above.
1258            // use_mock was captured before api_key was potentially reassigned to "test-key",
1259            // so it correctly reflects the original env state.
1260            let _ = writeln!(out, "    const char* _base_url_arg = use_mock ? base_url_buf : NULL;");
1261            let _ = writeln!(
1262                out,
1263                "    {prefix_upper}DefaultClient* client = {prefix}_{factory}(api_key, _base_url_arg, (uint64_t)-1, (uint32_t)-1, NULL);"
1264            );
1265        } else if has_mock {
1266            let _ = writeln!(out, "    const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1267            let _ = writeln!(out, "    assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
1268            let _ = writeln!(out, "    char base_url[1024];");
1269            let _ = writeln!(
1270                out,
1271                "    snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
1272            );
1273            let _ = writeln!(
1274                out,
1275                "    {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
1276            );
1277        } else {
1278            let _ = writeln!(
1279                out,
1280                "    {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
1281            );
1282        }
1283        let _ = writeln!(out, "    assert(client != NULL && \"failed to create client\");");
1284
1285        let method_args = if request_handle_vars.is_empty() && inline_method_args.is_empty() && extra_args.is_empty() {
1286            String::new()
1287        } else {
1288            let handles: Vec<String> = request_handle_vars.iter().map(|(_, v)| v.clone()).collect();
1289            let parts: Vec<String> = handles
1290                .into_iter()
1291                .chain(inline_method_args.iter().cloned())
1292                .chain(extra_args.iter().cloned())
1293                .collect();
1294            format!(", {}", parts.join(", "))
1295        };
1296
1297        let call_fn = format!("{prefix}_default_client_{function_name}");
1298
1299        if expects_error {
1300            let _ = writeln!(
1301                out,
1302                "    {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
1303            );
1304            for (_, var_name) in &request_handle_vars {
1305                let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
1306                let _ = writeln!(out, "    {prefix}_{req_snake}_free({var_name});");
1307            }
1308            let _ = writeln!(out, "    {prefix}_default_client_free(client);");
1309            let _ = writeln!(out, "    assert({result_var} == NULL && \"expected call to fail\");");
1310            let _ = writeln!(out, "}}");
1311            return;
1312        }
1313
1314        let _ = writeln!(
1315            out,
1316            "    {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
1317        );
1318        let _ = writeln!(out, "    assert({result_var} != NULL && \"expected call to succeed\");");
1319
1320        let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1321        let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1322        // Locals declared as primitive C scalars (uint64_t, double, bool, ...).
1323        // Locals not present here default to char* (heap-allocated accessor result).
1324        let mut primitive_locals: HashMap<String, String> = HashMap::new();
1325        // Locals declared as opaque struct handles (e.g. LITERLLMUsage*).
1326        // Keyed by local_var, value is the snake_case type name used for free().
1327        let mut opaque_handle_locals: HashMap<String, String> = HashMap::new();
1328
1329        for assertion in &fixture.assertions {
1330            if let Some(f) = &assertion.field {
1331                if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
1332                    let resolved = field_resolver.resolve(f);
1333                    let local_var = f.replace(['.', '['], "_").replace(']', "");
1334                    let has_map_access = resolved.contains('[');
1335                    if resolved.contains('.') {
1336                        let leaf_primitive = emit_nested_accessor(
1337                            out,
1338                            prefix,
1339                            resolved,
1340                            &local_var,
1341                            result_var,
1342                            fields_c_types,
1343                            fields_enum,
1344                            &mut intermediate_handles,
1345                            result_type_name,
1346                            f,
1347                        );
1348                        if let Some(prim) = leaf_primitive {
1349                            primitive_locals.insert(local_var.clone(), prim);
1350                        }
1351                    } else {
1352                        let result_type_snake = result_type_name.to_snake_case();
1353                        let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1354                        let lookup_key = format!("{result_type_snake}.{resolved}");
1355                        if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1356                            let _ = writeln!(out, "    {t} {local_var} = {accessor_fn}({result_var});");
1357                            primitive_locals.insert(local_var.clone(), t.clone());
1358                        } else if try_emit_enum_accessor(
1359                            out,
1360                            prefix,
1361                            &prefix_upper,
1362                            f,
1363                            resolved,
1364                            &result_type_snake,
1365                            &accessor_fn,
1366                            result_var,
1367                            &local_var,
1368                            fields_c_types,
1369                            fields_enum,
1370                            &mut intermediate_handles,
1371                        ) {
1372                            // accessor emitted with enum-to-string conversion
1373                        } else if let Some(handle_pascal) =
1374                            infer_opaque_handle_type(fields_c_types, &result_type_snake, resolved)
1375                        {
1376                            // Opaque struct handle: cannot be read as char*.
1377                            let _ = writeln!(
1378                                out,
1379                                "    {prefix_upper}{handle_pascal}* {local_var} = {accessor_fn}({result_var});"
1380                            );
1381                            opaque_handle_locals.insert(local_var.clone(), handle_pascal.to_snake_case());
1382                        } else {
1383                            let _ = writeln!(out, "    char* {local_var} = {accessor_fn}({result_var});");
1384                        }
1385                    }
1386                    accessed_fields.push((f.clone(), local_var, has_map_access));
1387                }
1388            }
1389        }
1390
1391        for assertion in &fixture.assertions {
1392            render_assertion(
1393                out,
1394                assertion,
1395                result_var,
1396                prefix,
1397                field_resolver,
1398                &accessed_fields,
1399                &primitive_locals,
1400                &opaque_handle_locals,
1401            );
1402        }
1403
1404        for (_f, local_var, from_json) in &accessed_fields {
1405            if primitive_locals.contains_key(local_var) {
1406                continue;
1407            }
1408            if let Some(snake_type) = opaque_handle_locals.get(local_var) {
1409                let _ = writeln!(out, "    {prefix}_{snake_type}_free({local_var});");
1410                continue;
1411            }
1412            if *from_json {
1413                let _ = writeln!(out, "    free({local_var});");
1414            } else {
1415                let _ = writeln!(out, "    {prefix}_free_string({local_var});");
1416            }
1417        }
1418        for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1419            if snake_type == "free_string" {
1420                let _ = writeln!(out, "    {prefix}_free_string({handle_var});");
1421            } else if snake_type == "free" {
1422                // Intermediate JSON-key extraction (alef_json_get_string) — heap
1423                // char* allocated by malloc-class helper; freed via plain free().
1424                let _ = writeln!(out, "    free({handle_var});");
1425            } else {
1426                let _ = writeln!(out, "    {prefix}_{snake_type}_free({handle_var});");
1427            }
1428        }
1429        let result_type_snake = result_type_name.to_snake_case();
1430        let _ = writeln!(out, "    {prefix}_{result_type_snake}_free({result_var});");
1431        for (_, var_name) in &request_handle_vars {
1432            let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
1433            let _ = writeln!(out, "    {prefix}_{req_snake}_free({var_name});");
1434        }
1435        let _ = writeln!(out, "    {prefix}_default_client_free(client);");
1436        let _ = writeln!(out, "}}");
1437        return;
1438    }
1439
1440    // Raw C result type path: functions returning a primitive C type (char*, int32_t,
1441    // uintptr_t) rather than an opaque handle pointer.
1442    if let Some(raw_type) = raw_c_result_type {
1443        // Build argument string. Void-arg functions pass nothing.
1444        let args_str = if args.is_empty() {
1445            String::new()
1446        } else {
1447            let parts: Vec<String> = args
1448                .iter()
1449                .filter_map(|arg| {
1450                    let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1451                    let val = fixture.input.get(field);
1452                    match val {
1453                        None if arg.optional => Some("NULL".to_string()),
1454                        None => None,
1455                        Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
1456                        Some(v) => Some(json_to_c(v)),
1457                    }
1458                })
1459                .collect();
1460            parts.join(", ")
1461        };
1462
1463        // Declare result variable.
1464        let _ = writeln!(out, "    {raw_type} {result_var} = {function_name}({args_str});");
1465
1466        // not_error assertion.
1467        let has_not_error = fixture.assertions.iter().any(|a| a.assertion_type == "not_error");
1468        if has_not_error {
1469            match raw_type {
1470                "char*" if !result_is_option => {
1471                    let _ = writeln!(out, "    assert({result_var} != NULL && \"expected call to succeed\");");
1472                }
1473                "int32_t" => {
1474                    let _ = writeln!(out, "    assert({result_var} >= 0 && \"expected call to succeed\");");
1475                }
1476                "uintptr_t" => {
1477                    let _ = writeln!(
1478                        out,
1479                        "    assert({prefix}_last_error_code() == 0 && \"expected call to succeed\");"
1480                    );
1481                }
1482                _ => {}
1483            }
1484        }
1485
1486        // Other assertions.
1487        for assertion in &fixture.assertions {
1488            match assertion.assertion_type.as_str() {
1489                "not_error" | "error" => {} // handled above / not applicable
1490                "not_empty" => {
1491                    let _ = writeln!(
1492                        out,
1493                        "    assert({result_var} != NULL && strlen({result_var}) > 0 && \"expected non-empty value\");"
1494                    );
1495                }
1496                "is_empty" => {
1497                    if result_is_option && raw_type == "char*" {
1498                        let _ = writeln!(
1499                            out,
1500                            "    assert({result_var} == NULL && \"expected empty/null value\");"
1501                        );
1502                    } else {
1503                        let _ = writeln!(
1504                            out,
1505                            "    assert(strlen({result_var}) == 0 && \"expected empty value\");"
1506                        );
1507                    }
1508                }
1509                "count_min" => {
1510                    if let Some(val) = &assertion.value {
1511                        if let Some(n) = val.as_u64() {
1512                            match raw_type {
1513                                "char*" => {
1514                                    let _ = writeln!(out, "    {{");
1515                                    let _ = writeln!(
1516                                        out,
1517                                        "        assert({result_var} != NULL && \"expected non-null JSON array\");"
1518                                    );
1519                                    let _ =
1520                                        writeln!(out, "        int elem_count = alef_json_array_count({result_var});");
1521                                    let _ = writeln!(
1522                                        out,
1523                                        "        assert(elem_count >= {n} && \"expected at least {n} elements\");"
1524                                    );
1525                                    let _ = writeln!(out, "    }}");
1526                                }
1527                                _ => {
1528                                    let _ = writeln!(
1529                                        out,
1530                                        "    assert((size_t){result_var} >= {n} && \"expected at least {n} elements\");"
1531                                    );
1532                                }
1533                            }
1534                        }
1535                    }
1536                }
1537                "greater_than_or_equal" => {
1538                    if let Some(val) = &assertion.value {
1539                        let c_val = json_to_c(val);
1540                        let _ = writeln!(
1541                            out,
1542                            "    assert({result_var} >= {c_val} && \"expected greater than or equal\");"
1543                        );
1544                    }
1545                }
1546                "contains" => {
1547                    if let Some(val) = &assertion.value {
1548                        let c_val = json_to_c(val);
1549                        let _ = writeln!(
1550                            out,
1551                            "    assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
1552                        );
1553                    }
1554                }
1555                "contains_all" => {
1556                    if let Some(values) = &assertion.values {
1557                        for val in values {
1558                            let c_val = json_to_c(val);
1559                            let _ = writeln!(
1560                                out,
1561                                "    assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
1562                            );
1563                        }
1564                    }
1565                }
1566                "equals" => {
1567                    if let Some(val) = &assertion.value {
1568                        let c_val = json_to_c(val);
1569                        if val.is_string() {
1570                            let _ = writeln!(
1571                                out,
1572                                "    assert({result_var} != NULL && str_trim_eq({result_var}, {c_val}) == 0 && \"equals assertion failed\");"
1573                            );
1574                        } else {
1575                            let _ = writeln!(
1576                                out,
1577                                "    assert({result_var} == {c_val} && \"equals assertion failed\");"
1578                            );
1579                        }
1580                    }
1581                }
1582                "not_contains" => {
1583                    if let Some(val) = &assertion.value {
1584                        let c_val = json_to_c(val);
1585                        let _ = writeln!(
1586                            out,
1587                            "    assert(strstr({result_var}, {c_val}) == NULL && \"expected NOT to contain substring\");"
1588                        );
1589                    }
1590                }
1591                "starts_with" => {
1592                    if let Some(val) = &assertion.value {
1593                        let c_val = json_to_c(val);
1594                        let _ = writeln!(
1595                            out,
1596                            "    assert(strncmp({result_var}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
1597                        );
1598                    }
1599                }
1600                "is_true" => {
1601                    let _ = writeln!(out, "    assert({result_var});");
1602                }
1603                "is_false" => {
1604                    let _ = writeln!(out, "    assert(!{result_var});");
1605                }
1606                other => {
1607                    panic!("C e2e raw-result generator: unsupported assertion type: {other}");
1608                }
1609            }
1610        }
1611
1612        // Free char* results.
1613        if raw_type == "char*" {
1614            let free_fn = c_free_fn
1615                .map(|s| s.to_string())
1616                .unwrap_or_else(|| format!("{prefix}_free_string"));
1617            if result_is_option {
1618                let _ = writeln!(out, "    if ({result_var} != NULL) {{ {free_fn}({result_var}); }}");
1619            } else {
1620                let _ = writeln!(out, "    {free_fn}({result_var});");
1621            }
1622        }
1623
1624        let _ = writeln!(out, "}}");
1625        return;
1626    }
1627
1628    // Legacy (non-client) path: call the function directly.
1629    // Used for libraries like html-to-markdown that expose standalone FFI functions.
1630
1631    // Use the function name directly — the override already includes the prefix
1632    // (e.g. "htm_convert"), so we must NOT prepend it again.
1633    let prefixed_fn = function_name.to_string();
1634
1635    // For json_object args, emit a from_json call to construct the options handle.
1636    let mut has_options_handle = false;
1637    for arg in args {
1638        if arg.arg_type == "json_object" {
1639            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1640            if let Some(val) = fixture.input.get(field) {
1641                if !val.is_null() {
1642                    // Fixture keys are camelCase; the FFI htm_conversion_options_from_json
1643                    // deserializes into the Rust ConversionOptions type which uses default
1644                    // serde (snake_case). Normalize keys before serializing.
1645                    let normalized = super::transform_json_keys_for_language(val, "snake_case");
1646                    let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1647                    let escaped = escape_c(&json_str);
1648                    let upper = prefix.to_uppercase();
1649                    let options_type_pascal = options_type_name;
1650                    let options_type_snake = options_type_name.to_snake_case();
1651                    let _ = writeln!(
1652                        out,
1653                        "    {upper}{options_type_pascal}* options_handle = {prefix}_{options_type_snake}_from_json(\"{escaped}\");"
1654                    );
1655                    has_options_handle = true;
1656                }
1657            }
1658        }
1659    }
1660
1661    let args_str = build_args_string_c(&fixture.input, args, has_options_handle);
1662
1663    if expects_error {
1664        let _ = writeln!(
1665            out,
1666            "    {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
1667        );
1668        if has_options_handle {
1669            let options_type_snake = options_type_name.to_snake_case();
1670            let _ = writeln!(out, "    {prefix}_{options_type_snake}_free(options_handle);");
1671        }
1672        let _ = writeln!(out, "    assert({result_var} == NULL && \"expected call to fail\");");
1673        let _ = writeln!(out, "}}");
1674        return;
1675    }
1676
1677    // The FFI returns an opaque handle; extract the content string from it.
1678    let _ = writeln!(
1679        out,
1680        "    {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
1681    );
1682    let _ = writeln!(out, "    assert({result_var} != NULL && \"expected call to succeed\");");
1683
1684    // Collect fields accessed by assertions so we can emit accessor calls.
1685    // C FFI uses the opaque handle pattern: {prefix}_conversion_result_{field}(handle).
1686    // For nested paths we generate chained FFI accessor calls using the type
1687    // chain from `fields_c_types`.
1688    // Each entry: (fixture_field, local_var, from_json_extract).
1689    // `from_json_extract` is true when the variable was extracted from a JSON
1690    // map via alef_json_get_string and needs free() instead of {prefix}_free_string().
1691    let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1692    // Track intermediate handles emitted so we can free them and avoid duplicates.
1693    // Each entry: (handle_var_name, snake_type_name) — freed in reverse order.
1694    let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1695    // Locals declared as primitive C scalars (uint64_t, double, bool, ...).
1696    let mut primitive_locals: HashMap<String, String> = HashMap::new();
1697    // Locals declared as opaque struct handles (e.g. LITERLLMUsage*).
1698    let mut opaque_handle_locals: HashMap<String, String> = HashMap::new();
1699
1700    for assertion in &fixture.assertions {
1701        if let Some(f) = &assertion.field {
1702            if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
1703                let resolved = field_resolver.resolve(f);
1704                let local_var = f.replace(['.', '['], "_").replace(']', "");
1705                let has_map_access = resolved.contains('[');
1706
1707                if resolved.contains('.') {
1708                    let leaf_primitive = emit_nested_accessor(
1709                        out,
1710                        prefix,
1711                        resolved,
1712                        &local_var,
1713                        result_var,
1714                        fields_c_types,
1715                        fields_enum,
1716                        &mut intermediate_handles,
1717                        result_type_name,
1718                        f,
1719                    );
1720                    if let Some(prim) = leaf_primitive {
1721                        primitive_locals.insert(local_var.clone(), prim);
1722                    }
1723                } else {
1724                    let result_type_snake = result_type_name.to_snake_case();
1725                    let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1726                    let lookup_key = format!("{result_type_snake}.{resolved}");
1727                    if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1728                        let _ = writeln!(out, "    {t} {local_var} = {accessor_fn}({result_var});");
1729                        primitive_locals.insert(local_var.clone(), t.clone());
1730                    } else if try_emit_enum_accessor(
1731                        out,
1732                        prefix,
1733                        &prefix_upper,
1734                        f,
1735                        resolved,
1736                        &result_type_snake,
1737                        &accessor_fn,
1738                        result_var,
1739                        &local_var,
1740                        fields_c_types,
1741                        fields_enum,
1742                        &mut intermediate_handles,
1743                    ) {
1744                        // accessor emitted with enum-to-string conversion
1745                    } else if let Some(handle_pascal) =
1746                        infer_opaque_handle_type(fields_c_types, &result_type_snake, resolved)
1747                    {
1748                        let _ = writeln!(
1749                            out,
1750                            "    {prefix_upper}{handle_pascal}* {local_var} = {accessor_fn}({result_var});"
1751                        );
1752                        opaque_handle_locals.insert(local_var.clone(), handle_pascal.to_snake_case());
1753                    } else {
1754                        let _ = writeln!(out, "    char* {local_var} = {accessor_fn}({result_var});");
1755                    }
1756                }
1757                accessed_fields.push((f.clone(), local_var.clone(), has_map_access));
1758            }
1759        }
1760    }
1761
1762    for assertion in &fixture.assertions {
1763        render_assertion(
1764            out,
1765            assertion,
1766            result_var,
1767            prefix,
1768            field_resolver,
1769            &accessed_fields,
1770            &primitive_locals,
1771            &opaque_handle_locals,
1772        );
1773    }
1774
1775    // Free extracted leaf strings.
1776    for (_f, local_var, from_json) in &accessed_fields {
1777        if primitive_locals.contains_key(local_var) {
1778            continue;
1779        }
1780        if let Some(snake_type) = opaque_handle_locals.get(local_var) {
1781            let _ = writeln!(out, "    {prefix}_{snake_type}_free({local_var});");
1782            continue;
1783        }
1784        if *from_json {
1785            let _ = writeln!(out, "    free({local_var});");
1786        } else {
1787            let _ = writeln!(out, "    {prefix}_free_string({local_var});");
1788        }
1789    }
1790    // Free intermediate handles in reverse order.
1791    for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1792        if snake_type == "free_string" {
1793            // free_string handles are freed with the free_string function directly.
1794            let _ = writeln!(out, "    {prefix}_free_string({handle_var});");
1795        } else {
1796            let _ = writeln!(out, "    {prefix}_{snake_type}_free({handle_var});");
1797        }
1798    }
1799    if has_options_handle {
1800        let options_type_snake = options_type_name.to_snake_case();
1801        let _ = writeln!(out, "    {prefix}_{options_type_snake}_free(options_handle);");
1802    }
1803    let result_type_snake = result_type_name.to_snake_case();
1804    let _ = writeln!(out, "    {prefix}_{result_type_snake}_free({result_var});");
1805    let _ = writeln!(out, "}}");
1806}
1807
1808/// Emit a test function using the engine-factory pattern:
1809///   `{prefix}_crawl_config_from_json(json)` → `{prefix}_create_engine(config)` →
1810///   `{prefix}_{function}(engine, url)` → assertions → free chain.
1811///
1812/// When all fixture assertions are skipped (fields not present on result type,
1813/// or only "error" assertions that C cannot replicate via a simple URL scrape),
1814/// the null-check is a soft guard (`if (result != NULL)`) so the test does not
1815/// abort when the mock server has no matching route.
1816#[allow(clippy::too_many_arguments)]
1817fn render_engine_factory_test_function(
1818    out: &mut String,
1819    fixture: &Fixture,
1820    prefix: &str,
1821    function_name: &str,
1822    result_var: &str,
1823    field_resolver: &FieldResolver,
1824    fields_c_types: &HashMap<String, String>,
1825    fields_enum: &HashSet<String>,
1826    result_type_name: &str,
1827    config_type: &str,
1828    expects_error: bool,
1829    raw_c_result_type: Option<&str>,
1830) {
1831    let prefix_upper = prefix.to_uppercase();
1832    let config_snake = config_type.to_snake_case();
1833
1834    // Build config JSON from fixture input (snake_case keys).
1835    let config_val = fixture.input.get("config");
1836    let config_json = match config_val {
1837        Some(v) if !v.is_null() => {
1838            let normalized = super::transform_json_keys_for_language(v, "snake_case");
1839            serde_json::to_string(&normalized).unwrap_or_else(|_| "{}".to_string())
1840        }
1841        _ => "{}".to_string(),
1842    };
1843    let config_escaped = escape_c(&config_json);
1844    let fixture_id = &fixture.id;
1845
1846    // An assertion is "active" when it has a field that is valid for the result type.
1847    // Error-only assertions are NOT treated as active for the engine factory pattern
1848    // because C's kcrawl_scrape() doesn't replicate batch/validation error semantics.
1849    let has_active_assertions = fixture.assertions.iter().any(|a| {
1850        if let Some(f) = &a.field {
1851            !f.is_empty() && field_resolver.is_valid_for_result(f)
1852        } else {
1853            false
1854        }
1855    });
1856
1857    // --- engine setup ---
1858    let _ = writeln!(
1859        out,
1860        "    {prefix_upper}{config_type}* config_handle = \
1861         {prefix}_{config_snake}_from_json(\"{config_escaped}\");"
1862    );
1863    if expects_error {
1864        // Config parsing may legitimately fail for error fixtures (e.g. invalid config
1865        // rejected by the FFI layer). Return early — that counts as the expected failure.
1866        let _ = writeln!(out, "    if (config_handle == NULL) {{ return; }}");
1867    } else {
1868        let _ = writeln!(out, "    assert(config_handle != NULL && \"failed to parse config\");");
1869    }
1870    let _ = writeln!(
1871        out,
1872        "    {prefix_upper}CrawlEngineHandle* engine = {prefix}_create_engine(config_handle);"
1873    );
1874    let _ = writeln!(out, "    {prefix}_{config_snake}_free(config_handle);");
1875    if expects_error {
1876        // Engine creation may legitimately fail for error fixtures (e.g. invalid config
1877        // rejected at engine-creation time). Return early — that counts as the expected failure.
1878        let _ = writeln!(out, "    if (engine == NULL) {{ return; }}");
1879    } else {
1880        let _ = writeln!(out, "    assert(engine != NULL && \"failed to create engine\");");
1881    }
1882
1883    // --- URL construction: prefer per-fixture MOCK_SERVER_<UPPER_ID> (for fixtures
1884    // that need host-root routes like /robots.txt or /sitemap.xml), fall back to
1885    // MOCK_SERVER_URL/fixtures/<id> for the common case. ---
1886    let fixture_env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1887    let _ = writeln!(out, "    const char* mock_per_fixture = getenv(\"{fixture_env_key}\");");
1888    let _ = writeln!(out, "    const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1889    let _ = writeln!(out, "    char url[2048];");
1890    let _ = writeln!(out, "    if (mock_per_fixture && mock_per_fixture[0] != '\\0') {{");
1891    let _ = writeln!(out, "        snprintf(url, sizeof(url), \"%s\", mock_per_fixture);");
1892    let _ = writeln!(out, "    }} else {{");
1893    let _ = writeln!(
1894        out,
1895        "        assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");"
1896    );
1897    let _ = writeln!(
1898        out,
1899        "        snprintf(url, sizeof(url), \"%s/fixtures/{fixture_id}\", mock_base);"
1900    );
1901    let _ = writeln!(out, "    }}");
1902
1903    // --- call ---
1904    // When the function returns a raw C type (e.g. char* for JSON-returning batch functions
1905    // like batch_scrape / batch_crawl), emit a plain variable declaration rather than an
1906    // opaque handle pointer.
1907    if raw_c_result_type == Some("char*") {
1908        let _ = writeln!(out, "    char* {result_var} = {prefix}_{function_name}(engine, url);");
1909        // For char* results there are no structured field accessors — emit a minimal
1910        // null-guard and free, then return.
1911        let _ = writeln!(out, "    if ({result_var} != NULL) {prefix}_free_string({result_var});");
1912        let _ = writeln!(out, "    {prefix}_crawl_engine_handle_free(engine);");
1913        let _ = writeln!(out, "}}");
1914        return;
1915    }
1916
1917    let _ = writeln!(
1918        out,
1919        "    {prefix_upper}{result_type_name}* {result_var} = {prefix}_{function_name}(engine, url);"
1920    );
1921
1922    // When no assertions can be verified (all skipped or error-only), use a soft
1923    // null-guard so the test is a no-op rather than aborting on a NULL result.
1924    if !has_active_assertions {
1925        let result_type_snake = result_type_name.to_snake_case();
1926        let _ = writeln!(
1927            out,
1928            "    if ({result_var} != NULL) {prefix}_{result_type_snake}_free({result_var});"
1929        );
1930        let _ = writeln!(out, "    {prefix}_crawl_engine_handle_free(engine);");
1931        let _ = writeln!(out, "}}");
1932        return;
1933    }
1934
1935    let _ = writeln!(out, "    assert({result_var} != NULL && \"expected call to succeed\");");
1936
1937    // --- field assertions ---
1938    let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1939    let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1940    let mut primitive_locals: HashMap<String, String> = HashMap::new();
1941    let mut opaque_handle_locals: HashMap<String, String> = HashMap::new();
1942
1943    for assertion in &fixture.assertions {
1944        if let Some(f) = &assertion.field {
1945            if !f.is_empty() && field_resolver.is_valid_for_result(f) && !accessed_fields.iter().any(|(k, _, _)| k == f)
1946            {
1947                let resolved = field_resolver.resolve(f);
1948                let local_var = f.replace(['.', '['], "_").replace(']', "");
1949                let has_map_access = resolved.contains('[');
1950                if resolved.contains('.') {
1951                    let leaf_primitive = emit_nested_accessor(
1952                        out,
1953                        prefix,
1954                        resolved,
1955                        &local_var,
1956                        result_var,
1957                        fields_c_types,
1958                        fields_enum,
1959                        &mut intermediate_handles,
1960                        result_type_name,
1961                        f,
1962                    );
1963                    if let Some(prim) = leaf_primitive {
1964                        primitive_locals.insert(local_var.clone(), prim);
1965                    }
1966                } else {
1967                    let result_type_snake = result_type_name.to_snake_case();
1968                    let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1969                    let lookup_key = format!("{result_type_snake}.{resolved}");
1970                    if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1971                        let _ = writeln!(out, "    {t} {local_var} = {accessor_fn}({result_var});");
1972                        primitive_locals.insert(local_var.clone(), t.clone());
1973                    } else if try_emit_enum_accessor(
1974                        out,
1975                        prefix,
1976                        &prefix_upper,
1977                        f,
1978                        resolved,
1979                        &result_type_snake,
1980                        &accessor_fn,
1981                        result_var,
1982                        &local_var,
1983                        fields_c_types,
1984                        fields_enum,
1985                        &mut intermediate_handles,
1986                    ) {
1987                        // accessor emitted with enum-to-string conversion
1988                    } else if let Some(handle_pascal) =
1989                        infer_opaque_handle_type(fields_c_types, &result_type_snake, resolved)
1990                    {
1991                        let _ = writeln!(
1992                            out,
1993                            "    {prefix_upper}{handle_pascal}* {local_var} = {accessor_fn}({result_var});"
1994                        );
1995                        opaque_handle_locals.insert(local_var.clone(), handle_pascal.to_snake_case());
1996                    } else {
1997                        let _ = writeln!(out, "    char* {local_var} = {accessor_fn}({result_var});");
1998                    }
1999                }
2000                accessed_fields.push((f.clone(), local_var, has_map_access));
2001            }
2002        }
2003    }
2004
2005    for assertion in &fixture.assertions {
2006        render_assertion(
2007            out,
2008            assertion,
2009            result_var,
2010            prefix,
2011            field_resolver,
2012            &accessed_fields,
2013            &primitive_locals,
2014            &opaque_handle_locals,
2015        );
2016    }
2017
2018    // --- free locals ---
2019    for (_f, local_var, from_json) in &accessed_fields {
2020        if primitive_locals.contains_key(local_var) {
2021            continue;
2022        }
2023        if let Some(snake_type) = opaque_handle_locals.get(local_var) {
2024            let _ = writeln!(out, "    {prefix}_{snake_type}_free({local_var});");
2025            continue;
2026        }
2027        if *from_json {
2028            let _ = writeln!(out, "    free({local_var});");
2029        } else {
2030            let _ = writeln!(out, "    {prefix}_free_string({local_var});");
2031        }
2032    }
2033    for (handle_var, snake_type) in intermediate_handles.iter().rev() {
2034        if snake_type == "free_string" {
2035            let _ = writeln!(out, "    {prefix}_free_string({handle_var});");
2036        } else {
2037            let _ = writeln!(out, "    {prefix}_{snake_type}_free({handle_var});");
2038        }
2039    }
2040
2041    let result_type_snake = result_type_name.to_snake_case();
2042    let _ = writeln!(out, "    {prefix}_{result_type_snake}_free({result_var});");
2043    let _ = writeln!(out, "    {prefix}_crawl_engine_handle_free(engine);");
2044    let _ = writeln!(out, "}}");
2045}
2046
2047/// Emit a byte-buffer test function for FFI methods returning raw bytes via
2048/// the out-pointer pattern (e.g. `speech`, `file_content`).
2049///
2050/// FFI signature shape:
2051/// ```c
2052/// int32_t {prefix}_default_client_{fn}(
2053///     const Client *this_,
2054///     const Request *req,                /* present when args is non-empty */
2055///     uint8_t **out_ptr,
2056///     uintptr_t *out_len,
2057///     uintptr_t *out_cap);
2058/// ```
2059///
2060/// Emits:
2061/// - request handle build (same as the standard client pattern)
2062/// - `uint8_t *out_ptr = NULL; uintptr_t out_len = 0, out_cap = 0;`
2063/// - call with `&out_ptr, &out_len, &out_cap`
2064/// - status assertion: `status == 0` on success, `status != 0` on expected error
2065/// - per-assertion: `not_empty` / `not_null` collapse to `out_len > 0` because
2066///   the pseudo "audio" / "content" field is the byte buffer itself
2067/// - `{prefix}_free_bytes(out_ptr, out_len, out_cap)` after assertions
2068#[allow(clippy::too_many_arguments)]
2069fn render_bytes_test_function(
2070    out: &mut String,
2071    fixture: &Fixture,
2072    prefix: &str,
2073    function_name: &str,
2074    _result_var: &str,
2075    args: &[crate::config::ArgMapping],
2076    options_type_name: &str,
2077    result_type_name: &str,
2078    factory: &str,
2079    expects_error: bool,
2080) {
2081    let prefix_upper = prefix.to_uppercase();
2082    let mut request_handle_vars: Vec<(String, String)> = Vec::new();
2083    let mut string_arg_exprs: Vec<String> = Vec::new();
2084
2085    for arg in args {
2086        match arg.arg_type.as_str() {
2087            "json_object" => {
2088                let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
2089                    options_type_name.to_string()
2090                } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
2091                    format!("{}Request", stripped)
2092                } else {
2093                    format!("{result_type_name}Request")
2094                };
2095                let request_type_snake = request_type_pascal.to_snake_case();
2096                let var_name = format!("{request_type_snake}_handle");
2097
2098                let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2099                let json_val = if field.is_empty() || field == "input" {
2100                    Some(&fixture.input)
2101                } else {
2102                    fixture.input.get(field)
2103                };
2104
2105                if let Some(val) = json_val {
2106                    if !val.is_null() {
2107                        let normalized = super::transform_json_keys_for_language(val, "snake_case");
2108                        let json_str = serde_json::to_string(&normalized).unwrap_or_default();
2109                        let escaped = escape_c(&json_str);
2110                        let _ = writeln!(
2111                            out,
2112                            "    {prefix_upper}{request_type_pascal}* {var_name} = \
2113                             {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
2114                        );
2115                        if expects_error {
2116                            // For error fixtures (e.g. invalid enum value rejected by
2117                            // serde), `_from_json` may legitimately return NULL — that
2118                            // counts as the expected failure. Mirror Java's pattern of
2119                            // wrapping setup + call inside `assertThrows(...)` so error
2120                            // fixtures pass at *any* failure step. The test returns
2121                            // before attempting to create a client, leaving no
2122                            // resources to free.
2123                            let _ = writeln!(out, "    if ({var_name} == NULL) {{ return; }}");
2124                        } else {
2125                            let _ = writeln!(out, "    assert({var_name} != NULL && \"failed to build request\");");
2126                        }
2127                        request_handle_vars.push((arg.name.clone(), var_name));
2128                    }
2129                }
2130            }
2131            "string" => {
2132                // Pass string args (e.g. file_id for file_content) directly as
2133                // C string literals.
2134                let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2135                let val = fixture.input.get(field);
2136                let expr = match val {
2137                    Some(serde_json::Value::String(s)) => format!("\"{}\"", escape_c(s)),
2138                    Some(serde_json::Value::Null) | None if arg.optional => "NULL".to_string(),
2139                    Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "NULL".to_string()),
2140                    None => "NULL".to_string(),
2141                };
2142                string_arg_exprs.push(expr);
2143            }
2144            _ => {
2145                // Other arg types are not currently exercised by byte-buffer
2146                // methods; pass NULL so the call shape compiles.
2147                string_arg_exprs.push("NULL".to_string());
2148            }
2149        }
2150    }
2151
2152    let fixture_id = &fixture.id;
2153    if fixture.needs_mock_server() {
2154        let _ = writeln!(out, "    const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
2155        let _ = writeln!(out, "    assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
2156        let _ = writeln!(out, "    char base_url[1024];");
2157        let _ = writeln!(
2158            out,
2159            "    snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
2160        );
2161        // Pass UINT64_MAX/UINT32_MAX (≡ -1ULL/-1U) as the FFI's None sentinel for
2162        // optional numeric primitives — passing literal 0 makes the binding see
2163        // Some(0), which Rust core treats as `Duration::from_secs(0)` (immediate
2164        // request deadline) and breaks every HTTP fixture.
2165        let _ = writeln!(
2166            out,
2167            "    {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
2168        );
2169    } else {
2170        let _ = writeln!(
2171            out,
2172            "    {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
2173        );
2174    }
2175    let _ = writeln!(out, "    assert(client != NULL && \"failed to create client\");");
2176
2177    // Out-params for the byte buffer.
2178    let _ = writeln!(out, "    uint8_t* out_ptr = NULL;");
2179    let _ = writeln!(out, "    uintptr_t out_len = 0;");
2180    let _ = writeln!(out, "    uintptr_t out_cap = 0;");
2181
2182    // Build the comma-separated argument list: handles, then string args.
2183    let mut method_args: Vec<String> = Vec::new();
2184    for (_, v) in &request_handle_vars {
2185        method_args.push(v.clone());
2186    }
2187    method_args.extend(string_arg_exprs.iter().cloned());
2188    let extra_args = if method_args.is_empty() {
2189        String::new()
2190    } else {
2191        format!(", {}", method_args.join(", "))
2192    };
2193
2194    let call_fn = format!("{prefix}_default_client_{function_name}");
2195    let _ = writeln!(
2196        out,
2197        "    int32_t status = {call_fn}(client{extra_args}, &out_ptr, &out_len, &out_cap);"
2198    );
2199
2200    if expects_error {
2201        for (_, var_name) in &request_handle_vars {
2202            let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
2203            let _ = writeln!(out, "    {prefix}_{req_snake}_free({var_name});");
2204        }
2205        let _ = writeln!(out, "    {prefix}_default_client_free(client);");
2206        let _ = writeln!(out, "    assert(status != 0 && \"expected call to fail\");");
2207        // free_bytes accepts a NULL ptr (no-op), so it is safe regardless of
2208        // whether the failed call wrote out_ptr.
2209        let _ = writeln!(out, "    {prefix}_free_bytes(out_ptr, out_len, out_cap);");
2210        let _ = writeln!(out, "}}");
2211        return;
2212    }
2213
2214    let _ = writeln!(out, "    assert(status == 0 && \"expected call to succeed\");");
2215
2216    // Render assertions. For byte-buffer methods, the only meaningful per-field
2217    // assertions are presence/length checks on the buffer itself. Field names
2218    // (e.g. "audio", "content") are pseudo-fields — collapse them all to
2219    // `out_len > 0`.
2220    let mut emitted_len_check = false;
2221    for assertion in &fixture.assertions {
2222        match assertion.assertion_type.as_str() {
2223            "not_error" => {
2224                // Already covered by the status == 0 assertion above.
2225            }
2226            "not_empty" | "not_null" => {
2227                if !emitted_len_check {
2228                    let _ = writeln!(out, "    assert(out_len > 0 && \"expected non-empty value\");");
2229                    emitted_len_check = true;
2230                }
2231            }
2232            _ => {
2233                // Other assertion shapes (equals, contains, ...) don't apply to
2234                // raw bytes; emit a comment so the test stays readable but does
2235                // not emit broken accessor calls.
2236                let _ = writeln!(
2237                    out,
2238                    "    /* skipped: assertion '{}' not meaningful on raw byte buffer */",
2239                    assertion.assertion_type
2240                );
2241            }
2242        }
2243    }
2244
2245    let _ = writeln!(out, "    {prefix}_free_bytes(out_ptr, out_len, out_cap);");
2246    for (_, var_name) in &request_handle_vars {
2247        let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
2248        let _ = writeln!(out, "    {prefix}_{req_snake}_free({var_name});");
2249    }
2250    let _ = writeln!(out, "    {prefix}_default_client_free(client);");
2251    let _ = writeln!(out, "}}");
2252}
2253
2254/// Emit a chat-stream test function that drives the FFI iterator handle.
2255///
2256/// Calls `{prefix}_default_client_chat_stream_start` to obtain an opaque handle,
2257/// loops over `{prefix}_default_client_chat_stream_next` until it returns null,
2258/// and aggregates per-chunk data into local variables (`chunks_count`,
2259/// `stream_content`, `stream_complete`, `last_choices_json`, ...). Fixture
2260/// assertions on streaming pseudo-fields (`chunks`, `stream_content`,
2261/// `stream_complete`, `no_chunks_after_done`, `finish_reason`, `tool_calls`,
2262/// `tool_calls[0].function.name`, `usage.total_tokens`) are translated to
2263/// assertions on these locals.
2264#[allow(clippy::too_many_arguments)]
2265fn render_chat_stream_test_function(
2266    out: &mut String,
2267    fixture: &Fixture,
2268    prefix: &str,
2269    result_var: &str,
2270    args: &[crate::config::ArgMapping],
2271    options_type_name: &str,
2272    expects_error: bool,
2273    api_key_var: Option<&str>,
2274) {
2275    let prefix_upper = prefix.to_uppercase();
2276
2277    let mut request_var: Option<String> = None;
2278    for arg in args {
2279        if arg.arg_type == "json_object" {
2280            let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
2281                options_type_name.to_string()
2282            } else {
2283                "ChatCompletionRequest".to_string()
2284            };
2285            let request_type_snake = request_type_pascal.to_snake_case();
2286            let var_name = format!("{request_type_snake}_handle");
2287
2288            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2289            let json_val = if field.is_empty() || field == "input" {
2290                Some(&fixture.input)
2291            } else {
2292                fixture.input.get(field)
2293            };
2294
2295            if let Some(val) = json_val {
2296                if !val.is_null() {
2297                    let normalized = super::transform_json_keys_for_language(val, "snake_case");
2298                    let json_str = serde_json::to_string(&normalized).unwrap_or_default();
2299                    let escaped = escape_c(&json_str);
2300                    let _ = writeln!(
2301                        out,
2302                        "    {prefix_upper}{request_type_pascal}* {var_name} = \
2303                         {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
2304                    );
2305                    let _ = writeln!(out, "    assert({var_name} != NULL && \"failed to build request\");");
2306                    request_var = Some(var_name);
2307                    break;
2308                }
2309            }
2310        }
2311    }
2312
2313    let req_handle = request_var.clone().unwrap_or_else(|| "NULL".to_string());
2314    let req_snake = request_var
2315        .as_ref()
2316        .and_then(|v| v.strip_suffix("_handle"))
2317        .unwrap_or("chat_completion_request")
2318        .to_string();
2319
2320    let fixture_id = &fixture.id;
2321    let has_mock = fixture.needs_mock_server();
2322    if has_mock && api_key_var.is_some() {
2323        // `api_key` and `base_url_buf` are already declared by the env-fallback
2324        // block above (the smoke+mock path). Reuse them — don't redeclare
2325        // `mock_base`/`base_url`, which would be a C compile error.
2326        // use_mock was captured before api_key was potentially reassigned to "test-key",
2327        // so it correctly reflects the original env state.
2328        let _ = writeln!(out, "    const char* _base_url_arg = use_mock ? base_url_buf : NULL;");
2329        let _ = writeln!(
2330            out,
2331            "    {prefix_upper}DefaultClient* client = {prefix}_create_client(api_key, _base_url_arg, (uint64_t)-1, (uint32_t)-1, NULL);"
2332        );
2333    } else if has_mock {
2334        let _ = writeln!(out, "    const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
2335        let _ = writeln!(out, "    assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
2336        let _ = writeln!(out, "    char base_url[1024];");
2337        let _ = writeln!(
2338            out,
2339            "    snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
2340        );
2341        // Pass UINT64_MAX/UINT32_MAX (≡ -1ULL/-1U) as the FFI's None sentinel for
2342        // optional numeric primitives — passing literal 0 makes the binding see
2343        // Some(0), which Rust core treats as `Duration::from_secs(0)` (immediate
2344        // request deadline) and breaks every HTTP fixture.
2345        let _ = writeln!(
2346            out,
2347            "    {prefix_upper}DefaultClient* client = {prefix}_create_client(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
2348        );
2349    } else {
2350        let _ = writeln!(
2351            out,
2352            "    {prefix_upper}DefaultClient* client = {prefix}_create_client(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
2353        );
2354    }
2355    let _ = writeln!(out, "    assert(client != NULL && \"failed to create client\");");
2356
2357    let _ = writeln!(
2358        out,
2359        "    {prefix_upper}LiterllmDefaultClientChatStreamStreamHandle* stream_handle = \
2360         {prefix}_default_client_chat_stream_start(client, {req_handle});"
2361    );
2362
2363    if expects_error {
2364        let _ = writeln!(
2365            out,
2366            "    assert(stream_handle == NULL && \"expected stream-start to fail\");"
2367        );
2368        if request_var.is_some() {
2369            let _ = writeln!(out, "    {prefix}_{req_snake}_free({req_handle});");
2370        }
2371        let _ = writeln!(out, "    {prefix}_default_client_free(client);");
2372        let _ = writeln!(out, "}}");
2373        return;
2374    }
2375
2376    let _ = writeln!(
2377        out,
2378        "    assert(stream_handle != NULL && \"expected stream-start to succeed\");"
2379    );
2380
2381    let _ = writeln!(out, "    size_t chunks_count = 0;");
2382    let _ = writeln!(out, "    char* stream_content = (char*)malloc(1);");
2383    let _ = writeln!(out, "    assert(stream_content != NULL);");
2384    let _ = writeln!(out, "    stream_content[0] = '\\0';");
2385    let _ = writeln!(out, "    size_t stream_content_len = 0;");
2386    let _ = writeln!(out, "    int stream_complete = 0;");
2387    let _ = writeln!(out, "    int no_chunks_after_done = 1;");
2388    let _ = writeln!(out, "    char* last_choices_json = NULL;");
2389    let _ = writeln!(out, "    uint64_t total_tokens = 0;");
2390    let _ = writeln!(out);
2391
2392    let _ = writeln!(out, "    while (1) {{");
2393    let _ = writeln!(
2394        out,
2395        "        {prefix_upper}ChatCompletionChunk* {result_var} = \
2396         {prefix}_default_client_chat_stream_next(stream_handle);"
2397    );
2398    let _ = writeln!(out, "        if ({result_var} == NULL) {{");
2399    let _ = writeln!(
2400        out,
2401        "            if ({prefix}_last_error_code() == 0) {{ stream_complete = 1; }}"
2402    );
2403    let _ = writeln!(out, "            break;");
2404    let _ = writeln!(out, "        }}");
2405    let _ = writeln!(out, "        chunks_count++;");
2406    let _ = writeln!(
2407        out,
2408        "        char* choices_json = {prefix}_chat_completion_chunk_choices({result_var});"
2409    );
2410    let _ = writeln!(out, "        if (choices_json != NULL) {{");
2411    let _ = writeln!(
2412        out,
2413        "            const char* d = strstr(choices_json, \"\\\"content\\\":\");"
2414    );
2415    let _ = writeln!(out, "            if (d != NULL) {{");
2416    let _ = writeln!(out, "                d += 10;");
2417    let _ = writeln!(out, "                while (*d == ' ' || *d == '\\t') d++;");
2418    let _ = writeln!(out, "                if (*d == '\"') {{");
2419    let _ = writeln!(out, "                    d++;");
2420    let _ = writeln!(out, "                    const char* e = d;");
2421    let _ = writeln!(out, "                    while (*e && *e != '\"') {{");
2422    let _ = writeln!(
2423        out,
2424        "                        if (*e == '\\\\' && *(e+1)) e += 2; else e++;"
2425    );
2426    let _ = writeln!(out, "                    }}");
2427    let _ = writeln!(out, "                    size_t add = (size_t)(e - d);");
2428    let _ = writeln!(out, "                    if (add > 0) {{");
2429    let _ = writeln!(
2430        out,
2431        "                        char* nc = (char*)realloc(stream_content, stream_content_len + add + 1);"
2432    );
2433    let _ = writeln!(out, "                        if (nc != NULL) {{");
2434    let _ = writeln!(out, "                            stream_content = nc;");
2435    let _ = writeln!(
2436        out,
2437        "                            memcpy(stream_content + stream_content_len, d, add);"
2438    );
2439    let _ = writeln!(out, "                            stream_content_len += add;");
2440    let _ = writeln!(
2441        out,
2442        "                            stream_content[stream_content_len] = '\\0';"
2443    );
2444    let _ = writeln!(out, "                        }}");
2445    let _ = writeln!(out, "                    }}");
2446    let _ = writeln!(out, "                }}");
2447    let _ = writeln!(out, "            }}");
2448    let _ = writeln!(
2449        out,
2450        "            if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
2451    );
2452    let _ = writeln!(out, "            last_choices_json = choices_json;");
2453    let _ = writeln!(out, "        }}");
2454    let _ = writeln!(
2455        out,
2456        "        {prefix_upper}Usage* usage_handle = {prefix}_chat_completion_chunk_usage({result_var});"
2457    );
2458    let _ = writeln!(out, "        if (usage_handle != NULL) {{");
2459    let _ = writeln!(
2460        out,
2461        "            total_tokens = (uint64_t){prefix}_usage_total_tokens(usage_handle);"
2462    );
2463    let _ = writeln!(out, "            {prefix}_usage_free(usage_handle);");
2464    let _ = writeln!(out, "        }}");
2465    let _ = writeln!(out, "        {prefix}_chat_completion_chunk_free({result_var});");
2466    let _ = writeln!(out, "    }}");
2467    let _ = writeln!(out, "    {prefix}_default_client_chat_stream_free(stream_handle);");
2468    let _ = writeln!(out);
2469
2470    let _ = writeln!(out, "    char* finish_reason = NULL;");
2471    let _ = writeln!(out, "    char* tool_calls_json = NULL;");
2472    let _ = writeln!(out, "    char* tool_calls_0_function_name = NULL;");
2473    let _ = writeln!(out, "    if (last_choices_json != NULL) {{");
2474    let _ = writeln!(
2475        out,
2476        "        finish_reason = alef_json_get_string(last_choices_json, \"finish_reason\");"
2477    );
2478    let _ = writeln!(
2479        out,
2480        "        const char* tc = strstr(last_choices_json, \"\\\"tool_calls\\\":\");"
2481    );
2482    let _ = writeln!(out, "        if (tc != NULL) {{");
2483    let _ = writeln!(out, "            tc += 13;");
2484    let _ = writeln!(out, "            while (*tc == ' ' || *tc == '\\t') tc++;");
2485    let _ = writeln!(out, "            if (*tc == '[') {{");
2486    let _ = writeln!(out, "                int depth = 0;");
2487    let _ = writeln!(out, "                const char* end = tc;");
2488    let _ = writeln!(out, "                int in_str = 0;");
2489    let _ = writeln!(out, "                for (; *end; end++) {{");
2490    let _ = writeln!(
2491        out,
2492        "                    if (*end == '\\\\' && in_str) {{ if (*(end+1)) end++; continue; }}"
2493    );
2494    let _ = writeln!(
2495        out,
2496        "                    if (*end == '\"') {{ in_str = !in_str; continue; }}"
2497    );
2498    let _ = writeln!(out, "                    if (in_str) continue;");
2499    let _ = writeln!(out, "                    if (*end == '[' || *end == '{{') depth++;");
2500    let _ = writeln!(
2501        out,
2502        "                    else if (*end == ']' || *end == '}}') {{ depth--; if (depth == 0) {{ end++; break; }} }}"
2503    );
2504    let _ = writeln!(out, "                }}");
2505    let _ = writeln!(out, "                size_t tlen = (size_t)(end - tc);");
2506    let _ = writeln!(out, "                tool_calls_json = (char*)malloc(tlen + 1);");
2507    let _ = writeln!(out, "                if (tool_calls_json != NULL) {{");
2508    let _ = writeln!(out, "                    memcpy(tool_calls_json, tc, tlen);");
2509    let _ = writeln!(out, "                    tool_calls_json[tlen] = '\\0';");
2510    let _ = writeln!(
2511        out,
2512        "                    const char* fn = strstr(tool_calls_json, \"\\\"function\\\"\");"
2513    );
2514    let _ = writeln!(out, "                    if (fn != NULL) {{");
2515    let _ = writeln!(
2516        out,
2517        "                        const char* np = strstr(fn, \"\\\"name\\\":\");"
2518    );
2519    let _ = writeln!(out, "                        if (np != NULL) {{");
2520    let _ = writeln!(out, "                            np += 7;");
2521    let _ = writeln!(
2522        out,
2523        "                            while (*np == ' ' || *np == '\\t') np++;"
2524    );
2525    let _ = writeln!(out, "                            if (*np == '\"') {{");
2526    let _ = writeln!(out, "                                np++;");
2527    let _ = writeln!(out, "                                const char* ne = np;");
2528    let _ = writeln!(
2529        out,
2530        "                                while (*ne && *ne != '\"') {{ if (*ne == '\\\\' && *(ne+1)) ne += 2; else ne++; }}"
2531    );
2532    let _ = writeln!(out, "                                size_t nlen = (size_t)(ne - np);");
2533    let _ = writeln!(
2534        out,
2535        "                                tool_calls_0_function_name = (char*)malloc(nlen + 1);"
2536    );
2537    let _ = writeln!(
2538        out,
2539        "                                if (tool_calls_0_function_name != NULL) {{"
2540    );
2541    let _ = writeln!(
2542        out,
2543        "                                    memcpy(tool_calls_0_function_name, np, nlen);"
2544    );
2545    let _ = writeln!(
2546        out,
2547        "                                    tool_calls_0_function_name[nlen] = '\\0';"
2548    );
2549    let _ = writeln!(out, "                                }}");
2550    let _ = writeln!(out, "                            }}");
2551    let _ = writeln!(out, "                        }}");
2552    let _ = writeln!(out, "                    }}");
2553    let _ = writeln!(out, "                }}");
2554    let _ = writeln!(out, "            }}");
2555    let _ = writeln!(out, "        }}");
2556    let _ = writeln!(out, "    }}");
2557    let _ = writeln!(out);
2558
2559    for assertion in &fixture.assertions {
2560        emit_chat_stream_assertion(out, assertion);
2561    }
2562
2563    let _ = writeln!(out, "    free(stream_content);");
2564    let _ = writeln!(
2565        out,
2566        "    if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
2567    );
2568    let _ = writeln!(out, "    if (finish_reason != NULL) free(finish_reason);");
2569    let _ = writeln!(out, "    if (tool_calls_json != NULL) free(tool_calls_json);");
2570    let _ = writeln!(
2571        out,
2572        "    if (tool_calls_0_function_name != NULL) free(tool_calls_0_function_name);"
2573    );
2574    if request_var.is_some() {
2575        let _ = writeln!(out, "    {prefix}_{req_snake}_free({req_handle});");
2576    }
2577    let _ = writeln!(out, "    {prefix}_default_client_free(client);");
2578    let _ = writeln!(
2579        out,
2580        "    /* suppress unused */ (void)total_tokens; (void)no_chunks_after_done; \
2581         (void)stream_complete; (void)chunks_count; (void)stream_content_len;"
2582    );
2583    let _ = writeln!(out, "}}");
2584}
2585
2586/// Emit a single fixture assertion for a chat-stream test, mapping fixture
2587/// pseudo-field references (`chunks`, `stream_content`, `stream_complete`, ...)
2588/// to the local aggregator variables built by [`render_chat_stream_test_function`].
2589fn emit_chat_stream_assertion(out: &mut String, assertion: &Assertion) {
2590    let field = assertion.field.as_deref().unwrap_or("");
2591
2592    enum Kind {
2593        IntCount,
2594        Bool,
2595        Str,
2596        IntTokens,
2597        Unsupported,
2598    }
2599
2600    let (expr, kind) = match field {
2601        "chunks" => ("chunks_count", Kind::IntCount),
2602        "stream_content" => ("stream_content", Kind::Str),
2603        "stream_complete" => ("stream_complete", Kind::Bool),
2604        "no_chunks_after_done" => ("no_chunks_after_done", Kind::Bool),
2605        "finish_reason" => ("finish_reason", Kind::Str),
2606        // tool_calls / tool_calls[0].function.name require accumulating across
2607        // delta chunks (the OpenAI SSE wire format spreads the array contents
2608        // over many chunks). The current C inline SSE parser only inspects the
2609        // *last* chunk's `choices`, which carries `finish_reason=tool_calls`
2610        // but no payload — so these assertions can't reliably evaluate. Skip
2611        // them, mirroring Python's `# skipped: field 'tool_calls' not available
2612        // on result type` outcome (Python's stream iterator doesn't expose them
2613        // either). Adding a delta-merge accumulator is its own follow-up.
2614        "tool_calls" | "tool_calls[0].function.name" => ("", Kind::Unsupported),
2615        "usage.total_tokens" => ("total_tokens", Kind::IntTokens),
2616        _ => ("", Kind::Unsupported),
2617    };
2618
2619    let atype = assertion.assertion_type.as_str();
2620    if atype == "not_error" || atype == "error" {
2621        return;
2622    }
2623
2624    if matches!(kind, Kind::Unsupported) {
2625        let _ = writeln!(
2626            out,
2627            "    /* skipped: streaming assertion on unsupported field '{field}' */"
2628        );
2629        return;
2630    }
2631
2632    match (atype, &kind) {
2633        ("count_min", Kind::IntCount) => {
2634            if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2635                let _ = writeln!(out, "    assert({expr} >= {n} && \"expected at least {n} chunks\");");
2636            }
2637        }
2638        ("equals", Kind::Str) => {
2639            if let Some(val) = &assertion.value {
2640                let c_val = json_to_c(val);
2641                let _ = writeln!(
2642                    out,
2643                    "    assert({expr} != NULL && str_trim_eq({expr}, {c_val}) == 0 && \"streaming equals assertion failed\");"
2644                );
2645            }
2646        }
2647        ("contains", Kind::Str) => {
2648            if let Some(val) = &assertion.value {
2649                let c_val = json_to_c(val);
2650                let _ = writeln!(
2651                    out,
2652                    "    assert({expr} != NULL && strstr({expr}, {c_val}) != NULL && \"streaming contains assertion failed\");"
2653                );
2654            }
2655        }
2656        ("not_empty", Kind::Str) => {
2657            let _ = writeln!(
2658                out,
2659                "    assert({expr} != NULL && strlen({expr}) > 0 && \"expected non-empty {field}\");"
2660            );
2661        }
2662        ("is_true", Kind::Bool) => {
2663            let _ = writeln!(out, "    assert({expr} && \"expected {field} to be true\");");
2664        }
2665        ("is_false", Kind::Bool) => {
2666            let _ = writeln!(out, "    assert(!{expr} && \"expected {field} to be false\");");
2667        }
2668        ("greater_than_or_equal", Kind::IntCount) | ("greater_than_or_equal", Kind::IntTokens) => {
2669            if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2670                let _ = writeln!(out, "    assert({expr} >= {n} && \"expected {expr} >= {n}\");");
2671            }
2672        }
2673        ("equals", Kind::IntCount) | ("equals", Kind::IntTokens) => {
2674            if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2675                let _ = writeln!(out, "    assert({expr} == {n} && \"equals assertion failed\");");
2676            }
2677        }
2678        _ => {
2679            let _ = writeln!(
2680                out,
2681                "    /* skipped: streaming assertion '{atype}' on field '{field}' not supported */"
2682            );
2683        }
2684    }
2685}
2686
2687/// Emit chained FFI accessor calls for a nested resolved field path.
2688///
2689/// For a path like `metadata.document.title`, this generates:
2690/// ```c
2691/// HTMHtmlMetadata* metadata_handle = htm_conversion_result_metadata(result);
2692/// assert(metadata_handle != NULL);
2693/// HTMDocumentMetadata* doc_handle = htm_html_metadata_document(metadata_handle);
2694/// assert(doc_handle != NULL);
2695/// char* metadata_title = htm_document_metadata_title(doc_handle);
2696/// ```
2697///
2698/// The type chain is looked up from `fields_c_types` which maps
2699/// `"{parent_snake_type}.{field}"` -> `"PascalCaseType"`.
2700#[allow(clippy::too_many_arguments)]
2701fn emit_nested_accessor(
2702    out: &mut String,
2703    prefix: &str,
2704    resolved: &str,
2705    local_var: &str,
2706    result_var: &str,
2707    fields_c_types: &HashMap<String, String>,
2708    fields_enum: &HashSet<String>,
2709    intermediate_handles: &mut Vec<(String, String)>,
2710    result_type_name: &str,
2711    raw_field: &str,
2712) -> Option<String> {
2713    let segments: Vec<&str> = resolved.split('.').collect();
2714    let prefix_upper = prefix.to_uppercase();
2715
2716    // Walk the path, starting from the root result type.
2717    let mut current_snake_type = result_type_name.to_snake_case();
2718    let mut current_handle = result_var.to_string();
2719    // Set to true when we've traversed a `[]` array element accessor and subsequent
2720    // fields must be extracted via alef_json_get_string rather than FFI function calls.
2721    let mut json_extract_mode = false;
2722
2723    for (i, segment) in segments.iter().enumerate() {
2724        let is_leaf = i + 1 == segments.len();
2725
2726        // In JSON extraction mode, the current_handle is a JSON string and all
2727        // segments name keys to extract via alef_json_get_string (for primitive
2728        // leaves) or alef_json_get_object (for intermediate object hops).
2729        if json_extract_mode {
2730            // Decompose `field` or `field[N]`/`field[]`. Numeric indexing must
2731            // extract the Nth element so later key lookups don't ambiguously
2732            // pick the first occurrence (matters for fixtures with multiple
2733            // array elements like `data[0]`/`data[1]`).
2734            let (bare_segment, bracket_key): (&str, Option<&str>) = match segment.find('[') {
2735                Some(pos) => (&segment[..pos], Some(segment[pos + 1..].trim_end_matches(']'))),
2736                None => (segment, None),
2737            };
2738            let seg_snake = bare_segment.to_snake_case();
2739            if is_leaf {
2740                let _ = writeln!(
2741                    out,
2742                    "    char* {local_var} = alef_json_get_string({current_handle}, \"{seg_snake}\");"
2743                );
2744                return None; // JSON key leaf — char*.
2745            }
2746            // Intermediate JSON key — must be an object/array value. Use the
2747            // object extractor so the substring includes braces/brackets and
2748            // downstream primitive lookups against it find their keys
2749            // (alef_json_get_string would return NULL on non-string values).
2750            let json_var = format!("{seg_snake}_json");
2751            if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2752                let _ = writeln!(
2753                    out,
2754                    "    char* {json_var} = alef_json_get_object({current_handle}, \"{seg_snake}\");"
2755                );
2756                intermediate_handles.push((json_var.clone(), "free".to_string()));
2757            }
2758            // If the segment also includes a numeric index `[N]`, drill into
2759            // the Nth element of the extracted array; otherwise stay on the
2760            // object/array substring.
2761            if let Some(key) = bracket_key {
2762                if let Ok(idx) = key.parse::<usize>() {
2763                    let elem_var = format!("{seg_snake}_{idx}_json");
2764                    if !intermediate_handles.iter().any(|(h, _)| h == &elem_var) {
2765                        let _ = writeln!(
2766                            out,
2767                            "    char* {elem_var} = alef_json_array_get_index({json_var}, {idx});"
2768                        );
2769                        intermediate_handles.push((elem_var.clone(), "free".to_string()));
2770                    }
2771                    current_handle = elem_var;
2772                    continue;
2773                }
2774            }
2775            current_handle = json_var;
2776            continue;
2777        }
2778
2779        // Check for map access: "field[key]" or array element access: "field[]"
2780        if let Some(bracket_pos) = segment.find('[') {
2781            let field_name = &segment[..bracket_pos];
2782            let key = segment[bracket_pos + 1..].trim_end_matches(']');
2783            let field_snake = field_name.to_snake_case();
2784            let accessor_fn = format!("{prefix}_{current_snake_type}_{field_snake}");
2785
2786            // The accessor returns a char* (JSON object/array string).
2787            let json_var = format!("{field_snake}_json");
2788            if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2789                let _ = writeln!(out, "    char* {json_var} = {accessor_fn}({current_handle});");
2790                let _ = writeln!(out, "    assert({json_var} != NULL);");
2791                // Track for freeing — use prefix_free_string since it's a char*.
2792                intermediate_handles.push((json_var.clone(), "free_string".to_string()));
2793            }
2794
2795            // Empty key `[]`: array-element substring access (any element matches).
2796            // Numeric key `[N]` (e.g. `choices[0]`, `data[1]`): extract the exact
2797            // Nth top-level element so subsequent key lookups don't ambiguously
2798            // pick the first occurrence — required for fixtures whose results
2799            // contain multiple array elements (e.g. `data[0].index`/`data[1].index`).
2800            if key.is_empty() {
2801                if !is_leaf {
2802                    current_handle = json_var;
2803                    json_extract_mode = true;
2804                    continue;
2805                }
2806                return None;
2807            }
2808            if let Ok(idx) = key.parse::<usize>() {
2809                let elem_var = format!("{field_snake}_{idx}_json");
2810                if !intermediate_handles.iter().any(|(h, _)| h == &elem_var) {
2811                    let _ = writeln!(
2812                        out,
2813                        "    char* {elem_var} = alef_json_array_get_index({json_var}, {idx});"
2814                    );
2815                    intermediate_handles.push((elem_var.clone(), "free".to_string()));
2816                }
2817                if !is_leaf {
2818                    current_handle = elem_var;
2819                    json_extract_mode = true;
2820                    continue;
2821                }
2822                // Trailing `[N]` — caller asserts on the element JSON.
2823                return None;
2824            }
2825
2826            // Named map key access: extract the key value from the JSON object.
2827            let _ = writeln!(
2828                out,
2829                "    char* {local_var} = alef_json_get_string({json_var}, \"{key}\");"
2830            );
2831            return None; // Map access leaf — char*.
2832        }
2833
2834        let seg_snake = segment.to_snake_case();
2835        let accessor_fn = format!("{prefix}_{current_snake_type}_{seg_snake}");
2836
2837        if is_leaf {
2838            // Leaf may be a primitive scalar (uint64_t, double, ...) when
2839            // configured in `fields_c_types`. Otherwise default to char*.
2840            let lookup_key = format!("{current_snake_type}.{seg_snake}");
2841            if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
2842                let _ = writeln!(out, "    {t} {local_var} = {accessor_fn}({current_handle});");
2843                return Some(t.clone());
2844            }
2845            // Enum leaf: opaque enum pointer that needs `_to_string` conversion.
2846            if try_emit_enum_accessor(
2847                out,
2848                prefix,
2849                &prefix_upper,
2850                raw_field,
2851                &seg_snake,
2852                &current_snake_type,
2853                &accessor_fn,
2854                &current_handle,
2855                local_var,
2856                fields_c_types,
2857                fields_enum,
2858                intermediate_handles,
2859            ) {
2860                return None;
2861            }
2862            let _ = writeln!(out, "    char* {local_var} = {accessor_fn}({current_handle});");
2863        } else {
2864            // Intermediate field — check if it's a char* (JSON string/array) or an opaque handle.
2865            let lookup_key = format!("{current_snake_type}.{seg_snake}");
2866            let return_type_pascal = match fields_c_types.get(&lookup_key) {
2867                Some(t) => t.clone(),
2868                None => {
2869                    // Fallback: derive PascalCase from the segment name itself.
2870                    segment.to_pascal_case()
2871                }
2872            };
2873
2874            // Special case: intermediate char* fields (e.g. links, assets) are JSON
2875            // strings/arrays, not opaque handles. For a `.length` suffix, emit alef_json_array_count.
2876            if return_type_pascal == "char*" {
2877                let json_var = format!("{seg_snake}_json");
2878                if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2879                    let _ = writeln!(out, "    char* {json_var} = {accessor_fn}({current_handle});");
2880                    intermediate_handles.push((json_var.clone(), "free_string".to_string()));
2881                }
2882                // If the next (and final) segment is "length", emit the count accessor.
2883                if i + 2 == segments.len() && segments[i + 1] == "length" {
2884                    let _ = writeln!(out, "    int {local_var} = alef_json_array_count({json_var});");
2885                    return Some("int".to_string());
2886                }
2887                current_snake_type = seg_snake.clone();
2888                current_handle = json_var;
2889                continue;
2890            }
2891
2892            let return_snake = return_type_pascal.to_snake_case();
2893            let handle_var = format!("{seg_snake}_handle");
2894
2895            // Only emit the handle if we haven't already (multiple fields may
2896            // share the same intermediate path prefix).
2897            if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
2898                let _ = writeln!(
2899                    out,
2900                    "    {prefix_upper}{return_type_pascal}* {handle_var} = \
2901                     {accessor_fn}({current_handle});"
2902                );
2903                let _ = writeln!(out, "    assert({handle_var} != NULL);");
2904                intermediate_handles.push((handle_var.clone(), return_snake.clone()));
2905            }
2906
2907            current_snake_type = return_snake;
2908            current_handle = handle_var;
2909        }
2910    }
2911    None
2912}
2913
2914/// Build the C argument string for the function call.
2915/// When `has_options_handle` is true, json_object args are replaced with
2916/// the `options_handle` pointer (which was constructed via `from_json`).
2917fn build_args_string_c(
2918    input: &serde_json::Value,
2919    args: &[crate::config::ArgMapping],
2920    has_options_handle: bool,
2921) -> String {
2922    if args.is_empty() {
2923        return json_to_c(input);
2924    }
2925
2926    let parts: Vec<String> = args
2927        .iter()
2928        .filter_map(|arg| {
2929            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
2930            let val = input.get(field);
2931            match val {
2932                // Field missing entirely and optional → pass NULL.
2933                None if arg.optional => Some("NULL".to_string()),
2934                // Field missing and required → skip (caller error, but don't crash).
2935                None => None,
2936                // Explicit null on optional arg → pass NULL.
2937                Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
2938                Some(v) => {
2939                    // For json_object args, use the options_handle pointer
2940                    // instead of the raw JSON string.
2941                    if arg.arg_type == "json_object" && has_options_handle && !v.is_null() {
2942                        Some("options_handle".to_string())
2943                    } else {
2944                        Some(json_to_c(v))
2945                    }
2946                }
2947            }
2948        })
2949        .collect();
2950
2951    parts.join(", ")
2952}
2953
2954#[allow(clippy::too_many_arguments)]
2955fn render_assertion(
2956    out: &mut String,
2957    assertion: &Assertion,
2958    result_var: &str,
2959    ffi_prefix: &str,
2960    _field_resolver: &FieldResolver,
2961    accessed_fields: &[(String, String, bool)],
2962    primitive_locals: &HashMap<String, String>,
2963    opaque_handle_locals: &HashMap<String, String>,
2964) {
2965    // Skip assertions on fields that don't exist on the result type.
2966    if let Some(f) = &assertion.field {
2967        if !f.is_empty() && !_field_resolver.is_valid_for_result(f) {
2968            let _ = writeln!(out, "    // skipped: field '{f}' not available on result type");
2969            return;
2970        }
2971    }
2972
2973    let field_expr = match &assertion.field {
2974        Some(f) if !f.is_empty() => {
2975            // Use the local variable extracted from the opaque handle.
2976            accessed_fields
2977                .iter()
2978                .find(|(k, _, _)| k == f)
2979                .map(|(_, local, _)| local.clone())
2980                .unwrap_or_else(|| result_var.to_string())
2981        }
2982        _ => result_var.to_string(),
2983    };
2984
2985    let field_is_primitive = primitive_locals.contains_key(&field_expr);
2986    let field_primitive_type = primitive_locals.get(&field_expr).cloned();
2987    // Opaque-handle fields (e.g. `usage` → LITERLLMUsage*) cannot be treated
2988    // as C strings — `strlen` / `strcmp` on a struct pointer is undefined
2989    // behavior (SIGABRT in practice). `not_empty` / `is_empty` collapse to
2990    // NULL checks; other string assertions are skipped for these fields.
2991    let field_is_opaque_handle = opaque_handle_locals.contains_key(&field_expr);
2992    // Map-access fields are extracted via `alef_json_get_string` and end up
2993    // as char*. When the assertion expects a numeric or boolean value, we
2994    // emit a parsed/literal comparison rather than `strcmp`.
2995    let field_is_map_access = if let Some(f) = &assertion.field {
2996        accessed_fields.iter().any(|(k, _, m)| k == f && *m)
2997    } else {
2998        false
2999    };
3000
3001    // Check if the assertion field is optional — used to emit conditional assertions
3002    // for optional numeric fields (returns 0 when None, so 0 == "not set").
3003    // Check both the raw field name and its resolved alias.
3004    let assertion_field_is_optional = assertion
3005        .field
3006        .as_deref()
3007        .map(|f| {
3008            if f.is_empty() {
3009                return false;
3010            }
3011            if _field_resolver.is_optional(f) {
3012                return true;
3013            }
3014            // Also check the resolved alias (e.g. "robots.crawl_delay" → "crawl_delay").
3015            let resolved = _field_resolver.resolve(f);
3016            _field_resolver.is_optional(resolved)
3017        })
3018        .unwrap_or(false);
3019
3020    match assertion.assertion_type.as_str() {
3021        "equals" => {
3022            if let Some(expected) = &assertion.value {
3023                let c_val = json_to_c(expected);
3024                if field_is_primitive {
3025                    let cmp_val = if field_primitive_type.as_deref() == Some("bool") {
3026                        match expected.as_bool() {
3027                            Some(true) => "1".to_string(),
3028                            Some(false) => "0".to_string(),
3029                            None => c_val,
3030                        }
3031                    } else {
3032                        c_val
3033                    };
3034                    // For optional numeric fields, treat 0 as "not set" and allow it.
3035                    // This mirrors Go's nil-pointer check for optional fields.
3036                    let is_numeric = field_primitive_type.as_deref().map(|t| t != "bool").unwrap_or(false);
3037                    if assertion_field_is_optional && is_numeric {
3038                        let _ = writeln!(
3039                            out,
3040                            "    assert(({field_expr} == 0 || {field_expr} == {cmp_val}) && \"equals assertion failed\");"
3041                        );
3042                    } else {
3043                        let _ = writeln!(
3044                            out,
3045                            "    assert({field_expr} == {cmp_val} && \"equals assertion failed\");"
3046                        );
3047                    }
3048                } else if expected.is_string() {
3049                    let _ = writeln!(
3050                        out,
3051                        "    assert(str_trim_eq({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
3052                    );
3053                } else if field_is_map_access && expected.is_boolean() {
3054                    let lit = match expected.as_bool() {
3055                        Some(true) => "\"true\"",
3056                        _ => "\"false\"",
3057                    };
3058                    let _ = writeln!(
3059                        out,
3060                        "    assert({field_expr} != NULL && strcmp({field_expr}, {lit}) == 0 && \"equals assertion failed\");"
3061                    );
3062                } else if field_is_map_access && expected.is_number() {
3063                    if expected.is_f64() {
3064                        let _ = writeln!(
3065                            out,
3066                            "    assert({field_expr} != NULL && atof({field_expr}) == {c_val} && \"equals assertion failed\");"
3067                        );
3068                    } else {
3069                        let _ = writeln!(
3070                            out,
3071                            "    assert({field_expr} != NULL && atoll({field_expr}) == {c_val} && \"equals assertion failed\");"
3072                        );
3073                    }
3074                } else {
3075                    let _ = writeln!(
3076                        out,
3077                        "    assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
3078                    );
3079                }
3080            }
3081        }
3082        "contains" => {
3083            if let Some(expected) = &assertion.value {
3084                let c_val = json_to_c(expected);
3085                let _ = writeln!(
3086                    out,
3087                    "    assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
3088                );
3089            }
3090        }
3091        "contains_all" => {
3092            if let Some(values) = &assertion.values {
3093                for val in values {
3094                    let c_val = json_to_c(val);
3095                    let _ = writeln!(
3096                        out,
3097                        "    assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
3098                    );
3099                }
3100            }
3101        }
3102        "not_contains" => {
3103            if let Some(expected) = &assertion.value {
3104                let c_val = json_to_c(expected);
3105                let _ = writeln!(
3106                    out,
3107                    "    assert(({field_expr} == NULL || strstr({field_expr}, {c_val}) == NULL) && \"expected NOT to contain substring\");"
3108                );
3109            }
3110        }
3111        "not_empty" => {
3112            if field_is_opaque_handle {
3113                // Opaque struct handle: `strlen` on a struct pointer is UB.
3114                // Weaken to a non-null check — strictly weaker than the
3115                // original intent but won't false-trigger SIGABRT.
3116                let _ = writeln!(out, "    assert({field_expr} != NULL && \"expected non-null handle\");");
3117            } else {
3118                let _ = writeln!(
3119                    out,
3120                    "    assert({field_expr} != NULL && strlen({field_expr}) > 0 && \"expected non-empty value\");"
3121                );
3122            }
3123        }
3124        "is_empty" => {
3125            if field_is_opaque_handle {
3126                let _ = writeln!(out, "    assert({field_expr} == NULL && \"expected null handle\");");
3127            } else if assertion_field_is_optional || !field_is_primitive {
3128                // Optional string fields may return NULL — treat NULL as empty.
3129                let _ = writeln!(
3130                    out,
3131                    "    assert(({field_expr} == NULL || strlen({field_expr}) == 0) && \"expected empty value\");"
3132                );
3133            } else {
3134                let _ = writeln!(
3135                    out,
3136                    "    assert(strlen({field_expr}) == 0 && \"expected empty value\");"
3137                );
3138            }
3139        }
3140        "contains_any" => {
3141            if let Some(values) = &assertion.values {
3142                let _ = writeln!(out, "    {{");
3143                let _ = writeln!(out, "        int found = 0;");
3144                for val in values {
3145                    let c_val = json_to_c(val);
3146                    let _ = writeln!(
3147                        out,
3148                        "        if (strstr({field_expr}, {c_val}) != NULL) {{ found = 1; }}"
3149                    );
3150                }
3151                let _ = writeln!(
3152                    out,
3153                    "        assert(found && \"expected to contain at least one of the specified values\");"
3154                );
3155                let _ = writeln!(out, "    }}");
3156            }
3157        }
3158        "greater_than" => {
3159            if let Some(val) = &assertion.value {
3160                let c_val = json_to_c(val);
3161                if field_is_map_access && val.is_number() && !field_is_primitive {
3162                    let _ = writeln!(
3163                        out,
3164                        "    assert({field_expr} != NULL && atof({field_expr}) > {c_val} && \"expected greater than\");"
3165                    );
3166                } else {
3167                    let _ = writeln!(out, "    assert({field_expr} > {c_val} && \"expected greater than\");");
3168                }
3169            }
3170        }
3171        "less_than" => {
3172            if let Some(val) = &assertion.value {
3173                let c_val = json_to_c(val);
3174                if field_is_map_access && val.is_number() && !field_is_primitive {
3175                    let _ = writeln!(
3176                        out,
3177                        "    assert({field_expr} != NULL && atof({field_expr}) < {c_val} && \"expected less than\");"
3178                    );
3179                } else {
3180                    let _ = writeln!(out, "    assert({field_expr} < {c_val} && \"expected less than\");");
3181                }
3182            }
3183        }
3184        "greater_than_or_equal" => {
3185            if let Some(val) = &assertion.value {
3186                let c_val = json_to_c(val);
3187                if field_is_map_access && val.is_number() && !field_is_primitive {
3188                    let _ = writeln!(
3189                        out,
3190                        "    assert({field_expr} != NULL && atof({field_expr}) >= {c_val} && \"expected greater than or equal\");"
3191                    );
3192                } else {
3193                    let _ = writeln!(
3194                        out,
3195                        "    assert({field_expr} >= {c_val} && \"expected greater than or equal\");"
3196                    );
3197                }
3198            }
3199        }
3200        "less_than_or_equal" => {
3201            if let Some(val) = &assertion.value {
3202                let c_val = json_to_c(val);
3203                if field_is_map_access && val.is_number() && !field_is_primitive {
3204                    let _ = writeln!(
3205                        out,
3206                        "    assert({field_expr} != NULL && atof({field_expr}) <= {c_val} && \"expected less than or equal\");"
3207                    );
3208                } else {
3209                    let _ = writeln!(
3210                        out,
3211                        "    assert({field_expr} <= {c_val} && \"expected less than or equal\");"
3212                    );
3213                }
3214            }
3215        }
3216        "starts_with" => {
3217            if let Some(expected) = &assertion.value {
3218                let c_val = json_to_c(expected);
3219                let _ = writeln!(
3220                    out,
3221                    "    assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
3222                );
3223            }
3224        }
3225        "ends_with" => {
3226            if let Some(expected) = &assertion.value {
3227                let c_val = json_to_c(expected);
3228                let _ = writeln!(out, "    assert(strlen({field_expr}) >= strlen({c_val}) && ");
3229                let _ = writeln!(
3230                    out,
3231                    "           strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
3232                );
3233            }
3234        }
3235        "min_length" => {
3236            if let Some(val) = &assertion.value {
3237                if let Some(n) = val.as_u64() {
3238                    let _ = writeln!(
3239                        out,
3240                        "    assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
3241                    );
3242                }
3243            }
3244        }
3245        "max_length" => {
3246            if let Some(val) = &assertion.value {
3247                if let Some(n) = val.as_u64() {
3248                    let _ = writeln!(
3249                        out,
3250                        "    assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
3251                    );
3252                }
3253            }
3254        }
3255        "count_min" => {
3256            if let Some(val) = &assertion.value {
3257                if let Some(n) = val.as_u64() {
3258                    let _ = writeln!(out, "    {{");
3259                    let _ = writeln!(out, "        /* count_min: count top-level JSON array elements */");
3260                    let _ = writeln!(
3261                        out,
3262                        "        assert({field_expr} != NULL && \"expected non-null collection JSON\");"
3263                    );
3264                    let _ = writeln!(out, "        int elem_count = alef_json_array_count({field_expr});");
3265                    let _ = writeln!(
3266                        out,
3267                        "        assert(elem_count >= {n} && \"expected at least {n} elements\");"
3268                    );
3269                    let _ = writeln!(out, "    }}");
3270                }
3271            }
3272        }
3273        "count_equals" => {
3274            if let Some(val) = &assertion.value {
3275                if let Some(n) = val.as_u64() {
3276                    let _ = writeln!(out, "    {{");
3277                    let _ = writeln!(out, "        /* count_equals: count elements in array */");
3278                    let _ = writeln!(
3279                        out,
3280                        "        assert({field_expr} != NULL && \"expected non-null collection JSON\");"
3281                    );
3282                    let _ = writeln!(out, "        int elem_count = alef_json_array_count({field_expr});");
3283                    let _ = writeln!(out, "        assert(elem_count == {n} && \"expected {n} elements\");");
3284                    let _ = writeln!(out, "    }}");
3285                }
3286            }
3287        }
3288        "is_true" => {
3289            let _ = writeln!(out, "    assert({field_expr});");
3290        }
3291        "is_false" => {
3292            let _ = writeln!(out, "    assert(!{field_expr});");
3293        }
3294        "method_result" => {
3295            if let Some(method_name) = &assertion.method {
3296                render_method_result_assertion(
3297                    out,
3298                    result_var,
3299                    ffi_prefix,
3300                    method_name,
3301                    assertion.args.as_ref(),
3302                    assertion.return_type.as_deref(),
3303                    assertion.check.as_deref().unwrap_or("is_true"),
3304                    assertion.value.as_ref(),
3305                );
3306            } else {
3307                panic!("C e2e generator: method_result assertion missing 'method' field");
3308            }
3309        }
3310        "matches_regex" => {
3311            if let Some(expected) = &assertion.value {
3312                let c_val = json_to_c(expected);
3313                let _ = writeln!(out, "    {{");
3314                let _ = writeln!(out, "        regex_t _re;");
3315                let _ = writeln!(
3316                    out,
3317                    "        assert(regcomp(&_re, {c_val}, REG_EXTENDED) == 0 && \"regex compile failed\");"
3318                );
3319                let _ = writeln!(
3320                    out,
3321                    "        assert(regexec(&_re, {field_expr}, 0, NULL, 0) == 0 && \"expected value to match regex\");"
3322                );
3323                let _ = writeln!(out, "        regfree(&_re);");
3324                let _ = writeln!(out, "    }}");
3325            }
3326        }
3327        "not_error" => {
3328            // Already handled — the NULL check above covers this.
3329        }
3330        "error" => {
3331            // Handled at the test function level.
3332        }
3333        other => {
3334            panic!("C e2e generator: unsupported assertion type: {other}");
3335        }
3336    }
3337}
3338
3339/// Render a `method_result` assertion in C.
3340///
3341/// Dispatches generically using `{ffi_prefix}_{method_name}` for the FFI call.
3342/// The `return_type` fixture field controls how the return value is handled:
3343/// - `"string"` — the method returns a heap-allocated `char*`; the generator
3344///   emits a scoped block that asserts, then calls `free()`.
3345/// - absent/other — treated as a primitive integer (or pointer-as-bool); the
3346///   assertion is emitted inline without any heap management.
3347#[allow(clippy::too_many_arguments)]
3348fn render_method_result_assertion(
3349    out: &mut String,
3350    result_var: &str,
3351    ffi_prefix: &str,
3352    method_name: &str,
3353    args: Option<&serde_json::Value>,
3354    return_type: Option<&str>,
3355    check: &str,
3356    value: Option<&serde_json::Value>,
3357) {
3358    let call_expr = build_c_method_call(result_var, ffi_prefix, method_name, args);
3359
3360    if return_type == Some("string") {
3361        // Heap-allocated char* return: emit a scoped block, assert, then free.
3362        let _ = writeln!(out, "    {{");
3363        let _ = writeln!(out, "        char* _method_result = {call_expr};");
3364        if check == "is_error" {
3365            let _ = writeln!(
3366                out,
3367                "        assert(_method_result == NULL && \"expected method to return error\");"
3368            );
3369            let _ = writeln!(out, "    }}");
3370            return;
3371        }
3372        let _ = writeln!(
3373            out,
3374            "        assert(_method_result != NULL && \"method_result returned NULL\");"
3375        );
3376        match check {
3377            "contains" => {
3378                if let Some(val) = value {
3379                    let c_val = json_to_c(val);
3380                    let _ = writeln!(
3381                        out,
3382                        "        assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
3383                    );
3384                }
3385            }
3386            "equals" => {
3387                if let Some(val) = value {
3388                    let c_val = json_to_c(val);
3389                    let _ = writeln!(
3390                        out,
3391                        "        assert(str_trim_eq(_method_result, {c_val}) == 0 && \"method_result equals assertion failed\");"
3392                    );
3393                }
3394            }
3395            "is_true" => {
3396                let _ = writeln!(
3397                    out,
3398                    "        assert(_method_result != NULL && strlen(_method_result) > 0 && \"method_result is_true assertion failed\");"
3399                );
3400            }
3401            "count_min" => {
3402                if let Some(val) = value {
3403                    let n = val.as_u64().unwrap_or(0);
3404                    let _ = writeln!(out, "        int _elem_count = alef_json_array_count(_method_result);");
3405                    let _ = writeln!(
3406                        out,
3407                        "        assert(_elem_count >= {n} && \"method_result count_min assertion failed\");"
3408                    );
3409                }
3410            }
3411            other_check => {
3412                panic!("C e2e generator: unsupported method_result check type for string return: {other_check}");
3413            }
3414        }
3415        let _ = writeln!(out, "        free(_method_result);");
3416        let _ = writeln!(out, "    }}");
3417        return;
3418    }
3419
3420    // Primitive (integer / pointer-as-bool) return: inline assert, no heap management.
3421    match check {
3422        "equals" => {
3423            if let Some(val) = value {
3424                let c_val = json_to_c(val);
3425                let _ = writeln!(
3426                    out,
3427                    "    assert({call_expr} == {c_val} && \"method_result equals assertion failed\");"
3428                );
3429            }
3430        }
3431        "is_true" => {
3432            let _ = writeln!(
3433                out,
3434                "    assert({call_expr} && \"method_result is_true assertion failed\");"
3435            );
3436        }
3437        "is_false" => {
3438            let _ = writeln!(
3439                out,
3440                "    assert(!{call_expr} && \"method_result is_false assertion failed\");"
3441            );
3442        }
3443        "greater_than_or_equal" => {
3444            if let Some(val) = value {
3445                let n = val.as_u64().unwrap_or(0);
3446                let _ = writeln!(
3447                    out,
3448                    "    assert({call_expr} >= {n} && \"method_result >= {n} assertion failed\");"
3449                );
3450            }
3451        }
3452        "count_min" => {
3453            if let Some(val) = value {
3454                let n = val.as_u64().unwrap_or(0);
3455                let _ = writeln!(
3456                    out,
3457                    "    assert({call_expr} >= {n} && \"method_result count_min assertion failed\");"
3458                );
3459            }
3460        }
3461        other_check => {
3462            panic!("C e2e generator: unsupported method_result check type: {other_check}");
3463        }
3464    }
3465}
3466
3467/// Build a C call expression for a `method_result` assertion.
3468///
3469/// Uses generic dispatch: `{ffi_prefix}_{method_name}(result_var, args...)`.
3470/// Args from the fixture JSON object are emitted as positional C arguments in
3471/// insertion order, using best-effort type conversion (strings → C string literals,
3472/// numbers and booleans → verbatim literals).
3473fn build_c_method_call(
3474    result_var: &str,
3475    ffi_prefix: &str,
3476    method_name: &str,
3477    args: Option<&serde_json::Value>,
3478) -> String {
3479    let extra_args = if let Some(args_val) = args {
3480        args_val
3481            .as_object()
3482            .map(|obj| {
3483                obj.values()
3484                    .map(|v| match v {
3485                        serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
3486                        serde_json::Value::Bool(true) => "1".to_string(),
3487                        serde_json::Value::Bool(false) => "0".to_string(),
3488                        serde_json::Value::Number(n) => n.to_string(),
3489                        serde_json::Value::Null => "NULL".to_string(),
3490                        other => format!("\"{}\"", escape_c(&other.to_string())),
3491                    })
3492                    .collect::<Vec<_>>()
3493                    .join(", ")
3494            })
3495            .unwrap_or_default()
3496    } else {
3497        String::new()
3498    };
3499
3500    if extra_args.is_empty() {
3501        format!("{ffi_prefix}_{method_name}({result_var})")
3502    } else {
3503        format!("{ffi_prefix}_{method_name}({result_var}, {extra_args})")
3504    }
3505}
3506
3507/// Convert a `serde_json::Value` to a C literal string.
3508fn json_to_c(value: &serde_json::Value) -> String {
3509    match value {
3510        serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
3511        serde_json::Value::Bool(true) => "1".to_string(),
3512        serde_json::Value::Bool(false) => "0".to_string(),
3513        serde_json::Value::Number(n) => n.to_string(),
3514        serde_json::Value::Null => "NULL".to_string(),
3515        other => format!("\"{}\"", escape_c(&other.to_string())),
3516    }
3517}