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