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