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    let pascal_prefix = prefix.to_pascal_case();
2528    let _ = writeln!(
2529        out,
2530        "    {pascal_prefix}DefaultClientChatStreamStreamHandle* stream_handle = \
2531         {prefix}_default_client_chat_stream_start(client, {req_handle});"
2532    );
2533
2534    if expects_error {
2535        let _ = writeln!(
2536            out,
2537            "    assert(stream_handle == NULL && \"expected stream-start to fail\");"
2538        );
2539        if request_var.is_some() {
2540            let _ = writeln!(out, "    {prefix}_{req_snake}_free({req_handle});");
2541        }
2542        let _ = writeln!(out, "    {prefix}_default_client_free(client);");
2543        let _ = writeln!(out, "}}");
2544        return;
2545    }
2546
2547    let _ = writeln!(
2548        out,
2549        "    assert(stream_handle != NULL && \"expected stream-start to succeed\");"
2550    );
2551
2552    let _ = writeln!(out, "    size_t chunks_count = 0;");
2553    let _ = writeln!(out, "    char* stream_content = (char*)malloc(1);");
2554    let _ = writeln!(out, "    assert(stream_content != NULL);");
2555    let _ = writeln!(out, "    stream_content[0] = '\\0';");
2556    let _ = writeln!(out, "    size_t stream_content_len = 0;");
2557    let _ = writeln!(out, "    int stream_complete = 0;");
2558    let _ = writeln!(out, "    int no_chunks_after_done = 1;");
2559    let _ = writeln!(out, "    char* last_choices_json = NULL;");
2560    let _ = writeln!(out, "    uint64_t total_tokens = 0;");
2561    let _ = writeln!(out);
2562
2563    let _ = writeln!(out, "    while (1) {{");
2564    let _ = writeln!(
2565        out,
2566        "        {prefix_upper}ChatCompletionChunk* {result_var} = \
2567         {prefix}_default_client_chat_stream_next(stream_handle);"
2568    );
2569    let _ = writeln!(out, "        if ({result_var} == NULL) {{");
2570    let _ = writeln!(
2571        out,
2572        "            if ({prefix}_last_error_code() == 0) {{ stream_complete = 1; }}"
2573    );
2574    let _ = writeln!(out, "            break;");
2575    let _ = writeln!(out, "        }}");
2576    let _ = writeln!(out, "        chunks_count++;");
2577    let _ = writeln!(
2578        out,
2579        "        char* choices_json = {prefix}_chat_completion_chunk_choices({result_var});"
2580    );
2581    let _ = writeln!(out, "        if (choices_json != NULL) {{");
2582    let _ = writeln!(
2583        out,
2584        "            const char* d = strstr(choices_json, \"\\\"content\\\":\");"
2585    );
2586    let _ = writeln!(out, "            if (d != NULL) {{");
2587    let _ = writeln!(out, "                d += 10;");
2588    let _ = writeln!(out, "                while (*d == ' ' || *d == '\\t') d++;");
2589    let _ = writeln!(out, "                if (*d == '\"') {{");
2590    let _ = writeln!(out, "                    d++;");
2591    let _ = writeln!(out, "                    const char* e = d;");
2592    let _ = writeln!(out, "                    while (*e && *e != '\"') {{");
2593    let _ = writeln!(
2594        out,
2595        "                        if (*e == '\\\\' && *(e+1)) e += 2; else e++;"
2596    );
2597    let _ = writeln!(out, "                    }}");
2598    let _ = writeln!(out, "                    size_t add = (size_t)(e - d);");
2599    let _ = writeln!(out, "                    if (add > 0) {{");
2600    let _ = writeln!(
2601        out,
2602        "                        char* nc = (char*)realloc(stream_content, stream_content_len + add + 1);"
2603    );
2604    let _ = writeln!(out, "                        if (nc != NULL) {{");
2605    let _ = writeln!(out, "                            stream_content = nc;");
2606    let _ = writeln!(
2607        out,
2608        "                            memcpy(stream_content + stream_content_len, d, add);"
2609    );
2610    let _ = writeln!(out, "                            stream_content_len += add;");
2611    let _ = writeln!(
2612        out,
2613        "                            stream_content[stream_content_len] = '\\0';"
2614    );
2615    let _ = writeln!(out, "                        }}");
2616    let _ = writeln!(out, "                    }}");
2617    let _ = writeln!(out, "                }}");
2618    let _ = writeln!(out, "            }}");
2619    let _ = writeln!(
2620        out,
2621        "            if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
2622    );
2623    let _ = writeln!(out, "            last_choices_json = choices_json;");
2624    let _ = writeln!(out, "        }}");
2625    let _ = writeln!(
2626        out,
2627        "        {prefix_upper}Usage* usage_handle = {prefix}_chat_completion_chunk_usage({result_var});"
2628    );
2629    let _ = writeln!(out, "        if (usage_handle != NULL) {{");
2630    let _ = writeln!(
2631        out,
2632        "            total_tokens = (uint64_t){prefix}_usage_total_tokens(usage_handle);"
2633    );
2634    let _ = writeln!(out, "            {prefix}_usage_free(usage_handle);");
2635    let _ = writeln!(out, "        }}");
2636    let _ = writeln!(out, "        {prefix}_chat_completion_chunk_free({result_var});");
2637    let _ = writeln!(out, "    }}");
2638    let _ = writeln!(out, "    {prefix}_default_client_chat_stream_free(stream_handle);");
2639    let _ = writeln!(out);
2640
2641    let _ = writeln!(out, "    char* finish_reason = NULL;");
2642    let _ = writeln!(out, "    char* tool_calls_json = NULL;");
2643    let _ = writeln!(out, "    char* tool_calls_0_function_name = NULL;");
2644    let _ = writeln!(out, "    if (last_choices_json != NULL) {{");
2645    let _ = writeln!(
2646        out,
2647        "        finish_reason = alef_json_get_string(last_choices_json, \"finish_reason\");"
2648    );
2649    let _ = writeln!(
2650        out,
2651        "        const char* tc = strstr(last_choices_json, \"\\\"tool_calls\\\":\");"
2652    );
2653    let _ = writeln!(out, "        if (tc != NULL) {{");
2654    let _ = writeln!(out, "            tc += 13;");
2655    let _ = writeln!(out, "            while (*tc == ' ' || *tc == '\\t') tc++;");
2656    let _ = writeln!(out, "            if (*tc == '[') {{");
2657    let _ = writeln!(out, "                int depth = 0;");
2658    let _ = writeln!(out, "                const char* end = tc;");
2659    let _ = writeln!(out, "                int in_str = 0;");
2660    let _ = writeln!(out, "                for (; *end; end++) {{");
2661    let _ = writeln!(
2662        out,
2663        "                    if (*end == '\\\\' && in_str) {{ if (*(end+1)) end++; continue; }}"
2664    );
2665    let _ = writeln!(
2666        out,
2667        "                    if (*end == '\"') {{ in_str = !in_str; continue; }}"
2668    );
2669    let _ = writeln!(out, "                    if (in_str) continue;");
2670    let _ = writeln!(out, "                    if (*end == '[' || *end == '{{') depth++;");
2671    let _ = writeln!(
2672        out,
2673        "                    else if (*end == ']' || *end == '}}') {{ depth--; if (depth == 0) {{ end++; break; }} }}"
2674    );
2675    let _ = writeln!(out, "                }}");
2676    let _ = writeln!(out, "                size_t tlen = (size_t)(end - tc);");
2677    let _ = writeln!(out, "                tool_calls_json = (char*)malloc(tlen + 1);");
2678    let _ = writeln!(out, "                if (tool_calls_json != NULL) {{");
2679    let _ = writeln!(out, "                    memcpy(tool_calls_json, tc, tlen);");
2680    let _ = writeln!(out, "                    tool_calls_json[tlen] = '\\0';");
2681    let _ = writeln!(
2682        out,
2683        "                    const char* fn = strstr(tool_calls_json, \"\\\"function\\\"\");"
2684    );
2685    let _ = writeln!(out, "                    if (fn != NULL) {{");
2686    let _ = writeln!(
2687        out,
2688        "                        const char* np = strstr(fn, \"\\\"name\\\":\");"
2689    );
2690    let _ = writeln!(out, "                        if (np != NULL) {{");
2691    let _ = writeln!(out, "                            np += 7;");
2692    let _ = writeln!(
2693        out,
2694        "                            while (*np == ' ' || *np == '\\t') np++;"
2695    );
2696    let _ = writeln!(out, "                            if (*np == '\"') {{");
2697    let _ = writeln!(out, "                                np++;");
2698    let _ = writeln!(out, "                                const char* ne = np;");
2699    let _ = writeln!(
2700        out,
2701        "                                while (*ne && *ne != '\"') {{ if (*ne == '\\\\' && *(ne+1)) ne += 2; else ne++; }}"
2702    );
2703    let _ = writeln!(out, "                                size_t nlen = (size_t)(ne - np);");
2704    let _ = writeln!(
2705        out,
2706        "                                tool_calls_0_function_name = (char*)malloc(nlen + 1);"
2707    );
2708    let _ = writeln!(
2709        out,
2710        "                                if (tool_calls_0_function_name != NULL) {{"
2711    );
2712    let _ = writeln!(
2713        out,
2714        "                                    memcpy(tool_calls_0_function_name, np, nlen);"
2715    );
2716    let _ = writeln!(
2717        out,
2718        "                                    tool_calls_0_function_name[nlen] = '\\0';"
2719    );
2720    let _ = writeln!(out, "                                }}");
2721    let _ = writeln!(out, "                            }}");
2722    let _ = writeln!(out, "                        }}");
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
2730    for assertion in &fixture.assertions {
2731        emit_chat_stream_assertion(out, assertion);
2732    }
2733
2734    let _ = writeln!(out, "    free(stream_content);");
2735    let _ = writeln!(
2736        out,
2737        "    if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
2738    );
2739    let _ = writeln!(out, "    if (finish_reason != NULL) free(finish_reason);");
2740    let _ = writeln!(out, "    if (tool_calls_json != NULL) free(tool_calls_json);");
2741    let _ = writeln!(
2742        out,
2743        "    if (tool_calls_0_function_name != NULL) free(tool_calls_0_function_name);"
2744    );
2745    if request_var.is_some() {
2746        let _ = writeln!(out, "    {prefix}_{req_snake}_free({req_handle});");
2747    }
2748    let _ = writeln!(out, "    {prefix}_default_client_free(client);");
2749    let _ = writeln!(
2750        out,
2751        "    /* suppress unused */ (void)total_tokens; (void)no_chunks_after_done; \
2752         (void)stream_complete; (void)chunks_count; (void)stream_content_len;"
2753    );
2754    let _ = writeln!(out, "}}");
2755}
2756
2757/// Emit a single fixture assertion for a chat-stream test, mapping fixture
2758/// pseudo-field references (`chunks`, `stream_content`, `stream_complete`, ...)
2759/// to the local aggregator variables built by [`render_chat_stream_test_function`].
2760fn emit_chat_stream_assertion(out: &mut String, assertion: &Assertion) {
2761    let field = assertion.field.as_deref().unwrap_or("");
2762
2763    enum Kind {
2764        IntCount,
2765        Bool,
2766        Str,
2767        IntTokens,
2768        Unsupported,
2769    }
2770
2771    let (expr, kind) = match field {
2772        "chunks" => ("chunks_count", Kind::IntCount),
2773        "stream_content" => ("stream_content", Kind::Str),
2774        "stream_complete" => ("stream_complete", Kind::Bool),
2775        "no_chunks_after_done" => ("no_chunks_after_done", Kind::Bool),
2776        "finish_reason" => ("finish_reason", Kind::Str),
2777        // tool_calls / tool_calls[0].function.name require accumulating across
2778        // delta chunks (the OpenAI SSE wire format spreads the array contents
2779        // over many chunks). The current C inline SSE parser only inspects the
2780        // *last* chunk's `choices`, which carries `finish_reason=tool_calls`
2781        // but no payload — so these assertions can't reliably evaluate. Skip
2782        // them, mirroring Python's `# skipped: field 'tool_calls' not available
2783        // on result type` outcome (Python's stream iterator doesn't expose them
2784        // either). Adding a delta-merge accumulator is its own follow-up.
2785        "tool_calls" | "tool_calls[0].function.name" => ("", Kind::Unsupported),
2786        "usage.total_tokens" => ("total_tokens", Kind::IntTokens),
2787        _ => ("", Kind::Unsupported),
2788    };
2789
2790    let atype = assertion.assertion_type.as_str();
2791    if atype == "not_error" || atype == "error" {
2792        return;
2793    }
2794
2795    if matches!(kind, Kind::Unsupported) {
2796        let _ = writeln!(
2797            out,
2798            "    /* skipped: streaming assertion on unsupported field '{field}' */"
2799        );
2800        return;
2801    }
2802
2803    match (atype, &kind) {
2804        ("count_min", Kind::IntCount) => {
2805            if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2806                let _ = writeln!(out, "    assert({expr} >= {n} && \"expected at least {n} chunks\");");
2807            }
2808        }
2809        ("equals", Kind::Str) => {
2810            if let Some(val) = &assertion.value {
2811                let c_val = json_to_c(val);
2812                let _ = writeln!(
2813                    out,
2814                    "    assert({expr} != NULL && str_trim_eq({expr}, {c_val}) == 0 && \"streaming equals assertion failed\");"
2815                );
2816            }
2817        }
2818        ("contains", Kind::Str) => {
2819            if let Some(val) = &assertion.value {
2820                let c_val = json_to_c(val);
2821                let _ = writeln!(
2822                    out,
2823                    "    assert({expr} != NULL && strstr({expr}, {c_val}) != NULL && \"streaming contains assertion failed\");"
2824                );
2825            }
2826        }
2827        ("not_empty", Kind::Str) => {
2828            let _ = writeln!(
2829                out,
2830                "    assert({expr} != NULL && strlen({expr}) > 0 && \"expected non-empty {field}\");"
2831            );
2832        }
2833        ("is_true", Kind::Bool) => {
2834            let _ = writeln!(out, "    assert({expr} && \"expected {field} to be true\");");
2835        }
2836        ("is_false", Kind::Bool) => {
2837            let _ = writeln!(out, "    assert(!{expr} && \"expected {field} to be false\");");
2838        }
2839        ("greater_than_or_equal", Kind::IntCount) | ("greater_than_or_equal", Kind::IntTokens) => {
2840            if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2841                let _ = writeln!(out, "    assert({expr} >= {n} && \"expected {expr} >= {n}\");");
2842            }
2843        }
2844        ("equals", Kind::IntCount) | ("equals", Kind::IntTokens) => {
2845            if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
2846                let _ = writeln!(out, "    assert({expr} == {n} && \"equals assertion failed\");");
2847            }
2848        }
2849        _ => {
2850            let _ = writeln!(
2851                out,
2852                "    /* skipped: streaming assertion '{atype}' on field '{field}' not supported */"
2853            );
2854        }
2855    }
2856}
2857
2858/// Emit chained FFI accessor calls for a nested resolved field path.
2859///
2860/// For a path like `metadata.document.title`, this generates:
2861/// ```c
2862/// HTMHtmlMetadata* metadata_handle = htm_conversion_result_metadata(result);
2863/// assert(metadata_handle != NULL);
2864/// HTMDocumentMetadata* doc_handle = htm_html_metadata_document(metadata_handle);
2865/// assert(doc_handle != NULL);
2866/// char* metadata_title = htm_document_metadata_title(doc_handle);
2867/// ```
2868///
2869/// The type chain is looked up from `fields_c_types` which maps
2870/// `"{parent_snake_type}.{field}"` -> `"PascalCaseType"`.
2871#[allow(clippy::too_many_arguments)]
2872fn emit_nested_accessor(
2873    out: &mut String,
2874    prefix: &str,
2875    resolved: &str,
2876    local_var: &str,
2877    result_var: &str,
2878    fields_c_types: &HashMap<String, String>,
2879    fields_enum: &HashSet<String>,
2880    intermediate_handles: &mut Vec<(String, String)>,
2881    result_type_name: &str,
2882    raw_field: &str,
2883) -> Option<String> {
2884    let segments: Vec<&str> = resolved.split('.').collect();
2885    let prefix_upper = prefix.to_uppercase();
2886
2887    // Walk the path, starting from the root result type.
2888    let mut current_snake_type = result_type_name.to_snake_case();
2889    let mut current_handle = result_var.to_string();
2890    // Set to true when we've traversed a `[]` array element accessor and subsequent
2891    // fields must be extracted via alef_json_get_string rather than FFI function calls.
2892    let mut json_extract_mode = false;
2893
2894    for (i, segment) in segments.iter().enumerate() {
2895        let is_leaf = i + 1 == segments.len();
2896
2897        // In JSON extraction mode, the current_handle is a JSON string and all
2898        // segments name keys to extract via alef_json_get_string (for primitive
2899        // leaves) or alef_json_get_object (for intermediate object hops).
2900        if json_extract_mode {
2901            // Decompose `field` or `field[N]`/`field[]`. Numeric indexing must
2902            // extract the Nth element so later key lookups don't ambiguously
2903            // pick the first occurrence (matters for fixtures with multiple
2904            // array elements like `data[0]`/`data[1]`).
2905            let (bare_segment, bracket_key): (&str, Option<&str>) = match segment.find('[') {
2906                Some(pos) => (&segment[..pos], Some(segment[pos + 1..].trim_end_matches(']'))),
2907                None => (segment, None),
2908            };
2909            let seg_snake = bare_segment.to_snake_case();
2910            if is_leaf {
2911                let _ = writeln!(
2912                    out,
2913                    "    char* {local_var} = alef_json_get_string({current_handle}, \"{seg_snake}\");"
2914                );
2915                return None; // JSON key leaf — char*.
2916            }
2917            // Intermediate JSON key — must be an object/array value. Use the
2918            // object extractor so the substring includes braces/brackets and
2919            // downstream primitive lookups against it find their keys
2920            // (alef_json_get_string would return NULL on non-string values).
2921            let json_var = format!("{seg_snake}_json");
2922            if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2923                let _ = writeln!(
2924                    out,
2925                    "    char* {json_var} = alef_json_get_object({current_handle}, \"{seg_snake}\");"
2926                );
2927                intermediate_handles.push((json_var.clone(), "free".to_string()));
2928            }
2929            // If the segment also includes a numeric index `[N]`, drill into
2930            // the Nth element of the extracted array; otherwise stay on the
2931            // object/array substring.
2932            if let Some(key) = bracket_key {
2933                if let Ok(idx) = key.parse::<usize>() {
2934                    let elem_var = format!("{seg_snake}_{idx}_json");
2935                    if !intermediate_handles.iter().any(|(h, _)| h == &elem_var) {
2936                        let _ = writeln!(
2937                            out,
2938                            "    char* {elem_var} = alef_json_array_get_index({json_var}, {idx});"
2939                        );
2940                        intermediate_handles.push((elem_var.clone(), "free".to_string()));
2941                    }
2942                    current_handle = elem_var;
2943                    continue;
2944                }
2945            }
2946            current_handle = json_var;
2947            continue;
2948        }
2949
2950        // Check for map access: "field[key]" or array element access: "field[]"
2951        if let Some(bracket_pos) = segment.find('[') {
2952            let field_name = &segment[..bracket_pos];
2953            let key = segment[bracket_pos + 1..].trim_end_matches(']');
2954            let field_snake = field_name.to_snake_case();
2955            let accessor_fn = format!("{prefix}_{current_snake_type}_{field_snake}");
2956
2957            // The accessor returns a char* (JSON object/array string).
2958            let json_var = format!("{field_snake}_json");
2959            if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
2960                let _ = writeln!(out, "    char* {json_var} = {accessor_fn}({current_handle});");
2961                let _ = writeln!(out, "    assert({json_var} != NULL);");
2962                // Track for freeing — use prefix_free_string since it's a char*.
2963                intermediate_handles.push((json_var.clone(), "free_string".to_string()));
2964            }
2965
2966            // Empty key `[]`: array-element substring access (any element matches).
2967            // Numeric key `[N]` (e.g. `choices[0]`, `data[1]`): extract the exact
2968            // Nth top-level element so subsequent key lookups don't ambiguously
2969            // pick the first occurrence — required for fixtures whose results
2970            // contain multiple array elements (e.g. `data[0].index`/`data[1].index`).
2971            if key.is_empty() {
2972                if !is_leaf {
2973                    current_handle = json_var;
2974                    json_extract_mode = true;
2975                    continue;
2976                }
2977                return None;
2978            }
2979            if let Ok(idx) = key.parse::<usize>() {
2980                let elem_var = format!("{field_snake}_{idx}_json");
2981                if !intermediate_handles.iter().any(|(h, _)| h == &elem_var) {
2982                    let _ = writeln!(
2983                        out,
2984                        "    char* {elem_var} = alef_json_array_get_index({json_var}, {idx});"
2985                    );
2986                    intermediate_handles.push((elem_var.clone(), "free".to_string()));
2987                }
2988                if !is_leaf {
2989                    current_handle = elem_var;
2990                    json_extract_mode = true;
2991                    continue;
2992                }
2993                // Trailing `[N]` — caller asserts on the element JSON.
2994                return None;
2995            }
2996
2997            // Named map key access: extract the key value from the JSON object.
2998            let _ = writeln!(
2999                out,
3000                "    char* {local_var} = alef_json_get_string({json_var}, \"{key}\");"
3001            );
3002            return None; // Map access leaf — char*.
3003        }
3004
3005        let seg_snake = segment.to_snake_case();
3006        let accessor_fn = format!("{prefix}_{current_snake_type}_{seg_snake}");
3007
3008        // Skip any assertion that touches a field marked "skip" in fields_c_types.
3009        if is_skipped_c_field(fields_c_types, &current_snake_type, &seg_snake) {
3010            return Some("__skip__".to_string()); // Sentinel: no accessor emitted, assertion skipped downstream.
3011        }
3012
3013        if is_leaf {
3014            // Leaf may be a primitive scalar (uint64_t, double, ...) when
3015            // configured in `fields_c_types`. Otherwise default to char*.
3016            let lookup_key = format!("{current_snake_type}.{seg_snake}");
3017            if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
3018                let _ = writeln!(out, "    {t} {local_var} = {accessor_fn}({current_handle});");
3019                return Some(t.clone());
3020            }
3021            // Opaque struct leaf: when fields_c_types maps "{parent}.{field}" to a
3022            // PascalCase type name (not a primitive, not "char*", not "skip"), the
3023            // accessor returns a struct pointer rather than a string.  Emit the typed
3024            // handle declaration and register it for freeing.  Example:
3025            //   "markdown_result.citations" = "CitationResult"
3026            //   → KCRAWLCitationResult* citations_handle = kcrawl_markdown_result_citations(handle);
3027            if let Some(opaque_type) = fields_c_types.get(&lookup_key).filter(|t| {
3028                *t != "char*"
3029                    && *t != "skip"
3030                    && !is_primitive_c_type(t)
3031                    && t.chars().next().is_some_and(|c| c.is_uppercase())
3032            }) {
3033                let handle_var = format!("{seg_snake}_handle");
3034                let opaque_snake = opaque_type.to_snake_case();
3035                if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
3036                    let _ = writeln!(
3037                        out,
3038                        "    {prefix_upper}{opaque_type}* {handle_var} = {accessor_fn}({current_handle});"
3039                    );
3040                    intermediate_handles.push((handle_var.clone(), opaque_snake));
3041                }
3042                // Treat the handle itself as the local_var for downstream assertions.
3043                // Map local_var → handle_var so render_assertion uses the handle name.
3044                if local_var != handle_var {
3045                    let _ = writeln!(out, "    {prefix_upper}{opaque_type}* {local_var} = {handle_var};");
3046                }
3047                return None; // opaque handle — no primitive return value
3048            }
3049            // Enum leaf: opaque enum pointer that needs `_to_string` conversion.
3050            if try_emit_enum_accessor(
3051                out,
3052                prefix,
3053                &prefix_upper,
3054                raw_field,
3055                &seg_snake,
3056                &current_snake_type,
3057                &accessor_fn,
3058                &current_handle,
3059                local_var,
3060                fields_c_types,
3061                fields_enum,
3062                intermediate_handles,
3063            ) {
3064                return None;
3065            }
3066            let _ = writeln!(out, "    char* {local_var} = {accessor_fn}({current_handle});");
3067        } else {
3068            // Intermediate field — check if it's a char* (JSON string/array) or an opaque handle.
3069            let lookup_key = format!("{current_snake_type}.{seg_snake}");
3070            let return_type_pascal = match fields_c_types.get(&lookup_key) {
3071                Some(t) => t.clone(),
3072                None => {
3073                    // Fallback: derive PascalCase from the segment name itself.
3074                    segment.to_pascal_case()
3075                }
3076            };
3077
3078            // Special case: intermediate char* fields (e.g. links, assets) are JSON
3079            // strings/arrays, not opaque handles. For a `.length` suffix, emit alef_json_array_count.
3080            if return_type_pascal == "char*" {
3081                let json_var = format!("{seg_snake}_json");
3082                if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
3083                    let _ = writeln!(out, "    char* {json_var} = {accessor_fn}({current_handle});");
3084                    intermediate_handles.push((json_var.clone(), "free_string".to_string()));
3085                }
3086                // If the next (and final) segment is "length", emit the count accessor.
3087                if i + 2 == segments.len() && segments[i + 1] == "length" {
3088                    let _ = writeln!(out, "    int {local_var} = alef_json_array_count({json_var});");
3089                    return Some("int".to_string());
3090                }
3091                current_snake_type = seg_snake.clone();
3092                current_handle = json_var;
3093                continue;
3094            }
3095
3096            let return_snake = return_type_pascal.to_snake_case();
3097            let handle_var = format!("{seg_snake}_handle");
3098
3099            // Only emit the handle if we haven't already (multiple fields may
3100            // share the same intermediate path prefix).
3101            if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
3102                let _ = writeln!(
3103                    out,
3104                    "    {prefix_upper}{return_type_pascal}* {handle_var} = \
3105                     {accessor_fn}({current_handle});"
3106                );
3107                let _ = writeln!(out, "    assert({handle_var} != NULL);");
3108                intermediate_handles.push((handle_var.clone(), return_snake.clone()));
3109            }
3110
3111            current_snake_type = return_snake;
3112            current_handle = handle_var;
3113        }
3114    }
3115    None
3116}
3117
3118/// Build the C argument string for the function call.
3119/// When `has_options_handle` is true, json_object args are replaced with
3120/// the `options_handle` pointer (which was constructed via `from_json`).
3121fn build_args_string_c(
3122    input: &serde_json::Value,
3123    args: &[crate::config::ArgMapping],
3124    has_options_handle: bool,
3125) -> String {
3126    if args.is_empty() {
3127        return json_to_c(input);
3128    }
3129
3130    let parts: Vec<String> = args
3131        .iter()
3132        .filter_map(|arg| {
3133            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
3134            let val = input.get(field);
3135            match val {
3136                // Field missing entirely and optional → pass NULL.
3137                None if arg.optional => Some("NULL".to_string()),
3138                // Field missing and required → skip (caller error, but don't crash).
3139                None => None,
3140                // Explicit null on optional arg → pass NULL.
3141                Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
3142                Some(v) => {
3143                    // For json_object args, use the options_handle pointer
3144                    // instead of the raw JSON string.
3145                    if arg.arg_type == "json_object" && has_options_handle && !v.is_null() {
3146                        Some("options_handle".to_string())
3147                    } else {
3148                        Some(json_to_c(v))
3149                    }
3150                }
3151            }
3152        })
3153        .collect();
3154
3155    parts.join(", ")
3156}
3157
3158#[allow(clippy::too_many_arguments)]
3159fn render_assertion(
3160    out: &mut String,
3161    assertion: &Assertion,
3162    result_var: &str,
3163    ffi_prefix: &str,
3164    _field_resolver: &FieldResolver,
3165    accessed_fields: &[(String, String, bool)],
3166    primitive_locals: &HashMap<String, String>,
3167    opaque_handle_locals: &HashMap<String, String>,
3168) {
3169    // Skip assertions on fields that don't exist on the result type.
3170    if let Some(f) = &assertion.field {
3171        if !f.is_empty() && !_field_resolver.is_valid_for_result(f) {
3172            let _ = writeln!(out, "    // skipped: field '{f}' not available on result type");
3173            return;
3174        }
3175    }
3176
3177    let field_expr = match &assertion.field {
3178        Some(f) if !f.is_empty() => {
3179            // Use the local variable extracted from the opaque handle.
3180            accessed_fields
3181                .iter()
3182                .find(|(k, _, _)| k == f)
3183                .map(|(_, local, _)| local.clone())
3184                .unwrap_or_else(|| result_var.to_string())
3185        }
3186        _ => result_var.to_string(),
3187    };
3188
3189    // If the field was marked with the "__skip__" sentinel (fields_c_types = "skip"),
3190    // the accessor was never emitted — skip the assertion silently.
3191    if primitive_locals.get(&field_expr).is_some_and(|t| t == "__skip__") {
3192        let _ = writeln!(out, "    // skipped: field '{field_expr}' not available in C FFI");
3193        return;
3194    }
3195
3196    let field_is_primitive = primitive_locals.contains_key(&field_expr);
3197    let field_primitive_type = primitive_locals.get(&field_expr).cloned();
3198    // Opaque-handle fields (e.g. `usage` → LITERLLMUsage*) cannot be treated
3199    // as C strings — `strlen` / `strcmp` on a struct pointer is undefined
3200    // behavior (SIGABRT in practice). `not_empty` / `is_empty` collapse to
3201    // NULL checks; other string assertions are skipped for these fields.
3202    let field_is_opaque_handle = opaque_handle_locals.contains_key(&field_expr);
3203    // Map-access fields are extracted via `alef_json_get_string` and end up
3204    // as char*. When the assertion expects a numeric or boolean value, we
3205    // emit a parsed/literal comparison rather than `strcmp`.
3206    let field_is_map_access = if let Some(f) = &assertion.field {
3207        accessed_fields.iter().any(|(k, _, m)| k == f && *m)
3208    } else {
3209        false
3210    };
3211
3212    // Check if the assertion field is optional — used to emit conditional assertions
3213    // for optional numeric fields (returns 0 when None, so 0 == "not set").
3214    // Check both the raw field name and its resolved alias.
3215    let assertion_field_is_optional = assertion
3216        .field
3217        .as_deref()
3218        .map(|f| {
3219            if f.is_empty() {
3220                return false;
3221            }
3222            if _field_resolver.is_optional(f) {
3223                return true;
3224            }
3225            // Also check the resolved alias (e.g. "robots.crawl_delay" → "crawl_delay").
3226            let resolved = _field_resolver.resolve(f);
3227            _field_resolver.is_optional(resolved)
3228        })
3229        .unwrap_or(false);
3230
3231    match assertion.assertion_type.as_str() {
3232        "equals" => {
3233            if let Some(expected) = &assertion.value {
3234                let c_val = json_to_c(expected);
3235                if field_is_primitive {
3236                    let cmp_val = if field_primitive_type.as_deref() == Some("bool") {
3237                        match expected.as_bool() {
3238                            Some(true) => "1".to_string(),
3239                            Some(false) => "0".to_string(),
3240                            None => c_val,
3241                        }
3242                    } else {
3243                        c_val
3244                    };
3245                    // For optional numeric fields, treat 0 as "not set" and allow it.
3246                    // This mirrors Go's nil-pointer check for optional fields.
3247                    let is_numeric = field_primitive_type.as_deref().map(|t| t != "bool").unwrap_or(false);
3248                    if assertion_field_is_optional && is_numeric {
3249                        let _ = writeln!(
3250                            out,
3251                            "    assert(({field_expr} == 0 || {field_expr} == {cmp_val}) && \"equals assertion failed\");"
3252                        );
3253                    } else {
3254                        let _ = writeln!(
3255                            out,
3256                            "    assert({field_expr} == {cmp_val} && \"equals assertion failed\");"
3257                        );
3258                    }
3259                } else if expected.is_string() {
3260                    let _ = writeln!(
3261                        out,
3262                        "    assert(str_trim_eq({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
3263                    );
3264                } else if field_is_map_access && expected.is_boolean() {
3265                    let lit = match expected.as_bool() {
3266                        Some(true) => "\"true\"",
3267                        _ => "\"false\"",
3268                    };
3269                    let _ = writeln!(
3270                        out,
3271                        "    assert({field_expr} != NULL && strcmp({field_expr}, {lit}) == 0 && \"equals assertion failed\");"
3272                    );
3273                } else if field_is_map_access && expected.is_number() {
3274                    if expected.is_f64() {
3275                        let _ = writeln!(
3276                            out,
3277                            "    assert({field_expr} != NULL && atof({field_expr}) == {c_val} && \"equals assertion failed\");"
3278                        );
3279                    } else {
3280                        let _ = writeln!(
3281                            out,
3282                            "    assert({field_expr} != NULL && atoll({field_expr}) == {c_val} && \"equals assertion failed\");"
3283                        );
3284                    }
3285                } else {
3286                    let _ = writeln!(
3287                        out,
3288                        "    assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
3289                    );
3290                }
3291            }
3292        }
3293        "contains" => {
3294            if let Some(expected) = &assertion.value {
3295                let c_val = json_to_c(expected);
3296                let _ = writeln!(
3297                    out,
3298                    "    assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
3299                );
3300            }
3301        }
3302        "contains_all" => {
3303            if let Some(values) = &assertion.values {
3304                for val in values {
3305                    let c_val = json_to_c(val);
3306                    let _ = writeln!(
3307                        out,
3308                        "    assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
3309                    );
3310                }
3311            }
3312        }
3313        "not_contains" => {
3314            if let Some(expected) = &assertion.value {
3315                let c_val = json_to_c(expected);
3316                let _ = writeln!(
3317                    out,
3318                    "    assert(({field_expr} == NULL || strstr({field_expr}, {c_val}) == NULL) && \"expected NOT to contain substring\");"
3319                );
3320            }
3321        }
3322        "not_empty" => {
3323            if field_is_opaque_handle {
3324                // Opaque struct handle: `strlen` on a struct pointer is UB.
3325                // Weaken to a non-null check — strictly weaker than the
3326                // original intent but won't false-trigger SIGABRT.
3327                let _ = writeln!(out, "    assert({field_expr} != NULL && \"expected non-null handle\");");
3328            } else {
3329                let _ = writeln!(
3330                    out,
3331                    "    assert({field_expr} != NULL && strlen({field_expr}) > 0 && \"expected non-empty value\");"
3332                );
3333            }
3334        }
3335        "is_empty" => {
3336            if field_is_opaque_handle {
3337                let _ = writeln!(out, "    assert({field_expr} == NULL && \"expected null handle\");");
3338            } else if assertion_field_is_optional || !field_is_primitive {
3339                // Optional string fields may return NULL — treat NULL as empty.
3340                let _ = writeln!(
3341                    out,
3342                    "    assert(({field_expr} == NULL || strlen({field_expr}) == 0) && \"expected empty value\");"
3343                );
3344            } else {
3345                let _ = writeln!(
3346                    out,
3347                    "    assert(strlen({field_expr}) == 0 && \"expected empty value\");"
3348                );
3349            }
3350        }
3351        "contains_any" => {
3352            if let Some(values) = &assertion.values {
3353                let _ = writeln!(out, "    {{");
3354                let _ = writeln!(out, "        int found = 0;");
3355                for val in values {
3356                    let c_val = json_to_c(val);
3357                    let _ = writeln!(
3358                        out,
3359                        "        if (strstr({field_expr}, {c_val}) != NULL) {{ found = 1; }}"
3360                    );
3361                }
3362                let _ = writeln!(
3363                    out,
3364                    "        assert(found && \"expected to contain at least one of the specified values\");"
3365                );
3366                let _ = writeln!(out, "    }}");
3367            }
3368        }
3369        "greater_than" => {
3370            if let Some(val) = &assertion.value {
3371                let c_val = json_to_c(val);
3372                if field_is_map_access && val.is_number() && !field_is_primitive {
3373                    let _ = writeln!(
3374                        out,
3375                        "    assert({field_expr} != NULL && atof({field_expr}) > {c_val} && \"expected greater than\");"
3376                    );
3377                } else {
3378                    let _ = writeln!(out, "    assert({field_expr} > {c_val} && \"expected greater than\");");
3379                }
3380            }
3381        }
3382        "less_than" => {
3383            if let Some(val) = &assertion.value {
3384                let c_val = json_to_c(val);
3385                if field_is_map_access && val.is_number() && !field_is_primitive {
3386                    let _ = writeln!(
3387                        out,
3388                        "    assert({field_expr} != NULL && atof({field_expr}) < {c_val} && \"expected less than\");"
3389                    );
3390                } else {
3391                    let _ = writeln!(out, "    assert({field_expr} < {c_val} && \"expected less than\");");
3392                }
3393            }
3394        }
3395        "greater_than_or_equal" => {
3396            if let Some(val) = &assertion.value {
3397                let c_val = json_to_c(val);
3398                if field_is_map_access && val.is_number() && !field_is_primitive {
3399                    let _ = writeln!(
3400                        out,
3401                        "    assert({field_expr} != NULL && atof({field_expr}) >= {c_val} && \"expected greater than or equal\");"
3402                    );
3403                } else {
3404                    let _ = writeln!(
3405                        out,
3406                        "    assert({field_expr} >= {c_val} && \"expected greater than or equal\");"
3407                    );
3408                }
3409            }
3410        }
3411        "less_than_or_equal" => {
3412            if let Some(val) = &assertion.value {
3413                let c_val = json_to_c(val);
3414                if field_is_map_access && val.is_number() && !field_is_primitive {
3415                    let _ = writeln!(
3416                        out,
3417                        "    assert({field_expr} != NULL && atof({field_expr}) <= {c_val} && \"expected less than or equal\");"
3418                    );
3419                } else {
3420                    let _ = writeln!(
3421                        out,
3422                        "    assert({field_expr} <= {c_val} && \"expected less than or equal\");"
3423                    );
3424                }
3425            }
3426        }
3427        "starts_with" => {
3428            if let Some(expected) = &assertion.value {
3429                let c_val = json_to_c(expected);
3430                let _ = writeln!(
3431                    out,
3432                    "    assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
3433                );
3434            }
3435        }
3436        "ends_with" => {
3437            if let Some(expected) = &assertion.value {
3438                let c_val = json_to_c(expected);
3439                let _ = writeln!(out, "    assert(strlen({field_expr}) >= strlen({c_val}) && ");
3440                let _ = writeln!(
3441                    out,
3442                    "           strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
3443                );
3444            }
3445        }
3446        "min_length" => {
3447            if let Some(val) = &assertion.value {
3448                if let Some(n) = val.as_u64() {
3449                    let _ = writeln!(
3450                        out,
3451                        "    assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
3452                    );
3453                }
3454            }
3455        }
3456        "max_length" => {
3457            if let Some(val) = &assertion.value {
3458                if let Some(n) = val.as_u64() {
3459                    let _ = writeln!(
3460                        out,
3461                        "    assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
3462                    );
3463                }
3464            }
3465        }
3466        "count_min" => {
3467            if let Some(val) = &assertion.value {
3468                if let Some(n) = val.as_u64() {
3469                    let _ = writeln!(out, "    {{");
3470                    let _ = writeln!(out, "        /* count_min: count top-level JSON array elements */");
3471                    let _ = writeln!(
3472                        out,
3473                        "        assert({field_expr} != NULL && \"expected non-null collection JSON\");"
3474                    );
3475                    let _ = writeln!(out, "        int elem_count = alef_json_array_count({field_expr});");
3476                    let _ = writeln!(
3477                        out,
3478                        "        assert(elem_count >= {n} && \"expected at least {n} elements\");"
3479                    );
3480                    let _ = writeln!(out, "    }}");
3481                }
3482            }
3483        }
3484        "count_equals" => {
3485            if let Some(val) = &assertion.value {
3486                if let Some(n) = val.as_u64() {
3487                    let _ = writeln!(out, "    {{");
3488                    let _ = writeln!(out, "        /* count_equals: count elements in array */");
3489                    let _ = writeln!(
3490                        out,
3491                        "        assert({field_expr} != NULL && \"expected non-null collection JSON\");"
3492                    );
3493                    let _ = writeln!(out, "        int elem_count = alef_json_array_count({field_expr});");
3494                    let _ = writeln!(out, "        assert(elem_count == {n} && \"expected {n} elements\");");
3495                    let _ = writeln!(out, "    }}");
3496                }
3497            }
3498        }
3499        "is_true" => {
3500            let _ = writeln!(out, "    assert({field_expr});");
3501        }
3502        "is_false" => {
3503            let _ = writeln!(out, "    assert(!{field_expr});");
3504        }
3505        "method_result" => {
3506            if let Some(method_name) = &assertion.method {
3507                render_method_result_assertion(
3508                    out,
3509                    result_var,
3510                    ffi_prefix,
3511                    method_name,
3512                    assertion.args.as_ref(),
3513                    assertion.return_type.as_deref(),
3514                    assertion.check.as_deref().unwrap_or("is_true"),
3515                    assertion.value.as_ref(),
3516                );
3517            } else {
3518                panic!("C e2e generator: method_result assertion missing 'method' field");
3519            }
3520        }
3521        "matches_regex" => {
3522            if let Some(expected) = &assertion.value {
3523                let c_val = json_to_c(expected);
3524                let _ = writeln!(out, "    {{");
3525                let _ = writeln!(out, "        regex_t _re;");
3526                let _ = writeln!(
3527                    out,
3528                    "        assert(regcomp(&_re, {c_val}, REG_EXTENDED) == 0 && \"regex compile failed\");"
3529                );
3530                let _ = writeln!(
3531                    out,
3532                    "        assert(regexec(&_re, {field_expr}, 0, NULL, 0) == 0 && \"expected value to match regex\");"
3533                );
3534                let _ = writeln!(out, "        regfree(&_re);");
3535                let _ = writeln!(out, "    }}");
3536            }
3537        }
3538        "not_error" => {
3539            // Already handled — the NULL check above covers this.
3540        }
3541        "error" => {
3542            // Handled at the test function level.
3543        }
3544        other => {
3545            panic!("C e2e generator: unsupported assertion type: {other}");
3546        }
3547    }
3548}
3549
3550/// Render a `method_result` assertion in C.
3551///
3552/// Dispatches generically using `{ffi_prefix}_{method_name}` for the FFI call.
3553/// The `return_type` fixture field controls how the return value is handled:
3554/// - `"string"` — the method returns a heap-allocated `char*`; the generator
3555///   emits a scoped block that asserts, then calls `free()`.
3556/// - absent/other — treated as a primitive integer (or pointer-as-bool); the
3557///   assertion is emitted inline without any heap management.
3558#[allow(clippy::too_many_arguments)]
3559fn render_method_result_assertion(
3560    out: &mut String,
3561    result_var: &str,
3562    ffi_prefix: &str,
3563    method_name: &str,
3564    args: Option<&serde_json::Value>,
3565    return_type: Option<&str>,
3566    check: &str,
3567    value: Option<&serde_json::Value>,
3568) {
3569    let call_expr = build_c_method_call(result_var, ffi_prefix, method_name, args);
3570
3571    if return_type == Some("string") {
3572        // Heap-allocated char* return: emit a scoped block, assert, then free.
3573        let _ = writeln!(out, "    {{");
3574        let _ = writeln!(out, "        char* _method_result = {call_expr};");
3575        if check == "is_error" {
3576            let _ = writeln!(
3577                out,
3578                "        assert(_method_result == NULL && \"expected method to return error\");"
3579            );
3580            let _ = writeln!(out, "    }}");
3581            return;
3582        }
3583        let _ = writeln!(
3584            out,
3585            "        assert(_method_result != NULL && \"method_result returned NULL\");"
3586        );
3587        match check {
3588            "contains" => {
3589                if let Some(val) = value {
3590                    let c_val = json_to_c(val);
3591                    let _ = writeln!(
3592                        out,
3593                        "        assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
3594                    );
3595                }
3596            }
3597            "equals" => {
3598                if let Some(val) = value {
3599                    let c_val = json_to_c(val);
3600                    let _ = writeln!(
3601                        out,
3602                        "        assert(str_trim_eq(_method_result, {c_val}) == 0 && \"method_result equals assertion failed\");"
3603                    );
3604                }
3605            }
3606            "is_true" => {
3607                let _ = writeln!(
3608                    out,
3609                    "        assert(_method_result != NULL && strlen(_method_result) > 0 && \"method_result is_true assertion failed\");"
3610                );
3611            }
3612            "count_min" => {
3613                if let Some(val) = value {
3614                    let n = val.as_u64().unwrap_or(0);
3615                    let _ = writeln!(out, "        int _elem_count = alef_json_array_count(_method_result);");
3616                    let _ = writeln!(
3617                        out,
3618                        "        assert(_elem_count >= {n} && \"method_result count_min assertion failed\");"
3619                    );
3620                }
3621            }
3622            other_check => {
3623                panic!("C e2e generator: unsupported method_result check type for string return: {other_check}");
3624            }
3625        }
3626        let _ = writeln!(out, "        free(_method_result);");
3627        let _ = writeln!(out, "    }}");
3628        return;
3629    }
3630
3631    // Primitive (integer / pointer-as-bool) return: inline assert, no heap management.
3632    match check {
3633        "equals" => {
3634            if let Some(val) = value {
3635                let c_val = json_to_c(val);
3636                let _ = writeln!(
3637                    out,
3638                    "    assert({call_expr} == {c_val} && \"method_result equals assertion failed\");"
3639                );
3640            }
3641        }
3642        "is_true" => {
3643            let _ = writeln!(
3644                out,
3645                "    assert({call_expr} && \"method_result is_true assertion failed\");"
3646            );
3647        }
3648        "is_false" => {
3649            let _ = writeln!(
3650                out,
3651                "    assert(!{call_expr} && \"method_result is_false assertion failed\");"
3652            );
3653        }
3654        "greater_than_or_equal" => {
3655            if let Some(val) = value {
3656                let n = val.as_u64().unwrap_or(0);
3657                let _ = writeln!(
3658                    out,
3659                    "    assert({call_expr} >= {n} && \"method_result >= {n} assertion failed\");"
3660                );
3661            }
3662        }
3663        "count_min" => {
3664            if let Some(val) = value {
3665                let n = val.as_u64().unwrap_or(0);
3666                let _ = writeln!(
3667                    out,
3668                    "    assert({call_expr} >= {n} && \"method_result count_min assertion failed\");"
3669                );
3670            }
3671        }
3672        other_check => {
3673            panic!("C e2e generator: unsupported method_result check type: {other_check}");
3674        }
3675    }
3676}
3677
3678/// Build a C call expression for a `method_result` assertion.
3679///
3680/// Uses generic dispatch: `{ffi_prefix}_{method_name}(result_var, args...)`.
3681/// Args from the fixture JSON object are emitted as positional C arguments in
3682/// insertion order, using best-effort type conversion (strings → C string literals,
3683/// numbers and booleans → verbatim literals).
3684fn build_c_method_call(
3685    result_var: &str,
3686    ffi_prefix: &str,
3687    method_name: &str,
3688    args: Option<&serde_json::Value>,
3689) -> String {
3690    let extra_args = if let Some(args_val) = args {
3691        args_val
3692            .as_object()
3693            .map(|obj| {
3694                obj.values()
3695                    .map(|v| match v {
3696                        serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
3697                        serde_json::Value::Bool(true) => "1".to_string(),
3698                        serde_json::Value::Bool(false) => "0".to_string(),
3699                        serde_json::Value::Number(n) => n.to_string(),
3700                        serde_json::Value::Null => "NULL".to_string(),
3701                        other => format!("\"{}\"", escape_c(&other.to_string())),
3702                    })
3703                    .collect::<Vec<_>>()
3704                    .join(", ")
3705            })
3706            .unwrap_or_default()
3707    } else {
3708        String::new()
3709    };
3710
3711    if extra_args.is_empty() {
3712        format!("{ffi_prefix}_{method_name}({result_var})")
3713    } else {
3714        format!("{ffi_prefix}_{method_name}({result_var}, {extra_args})")
3715    }
3716}
3717
3718/// Convert a `serde_json::Value` to a C literal string.
3719fn json_to_c(value: &serde_json::Value) -> String {
3720    match value {
3721        serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
3722        serde_json::Value::Bool(true) => "1".to_string(),
3723        serde_json::Value::Bool(false) => "0".to_string(),
3724        serde_json::Value::Number(n) => n.to_string(),
3725        serde_json::Value::Null => "NULL".to_string(),
3726        other => format!("\"{}\"", escape_c(&other.to_string())),
3727    }
3728}
3729
3730// ---------------------------------------------------------------------------
3731// Visitor test file generation for C FFI
3732// ---------------------------------------------------------------------------
3733
3734/// Generate `test_visitor.c` — one test function per visitor-bearing fixture.
3735///
3736/// Each test:
3737/// 1. Defines static C callback functions for each configured callback slot.
3738/// 2. Zero-initialises a `HTMHtmVisitorCallbacks` struct and wires each slot.
3739/// 3. Creates a visitor handle via `htm_visitor_create`.
3740/// 4. Creates an options handle via `htm_conversion_options_from_json`.
3741/// 5. Attaches the visitor via `htm_options_set_visitor_handle`.
3742/// 6. Calls `htm_convert(html, options)` and serialises the result to JSON.
3743/// 7. Extracts fields via `alef_json_get_string` and runs `contains`/`not_contains`
3744///    assertions with `assert(…)`.
3745/// 8. Frees all handles in reverse allocation order.
3746fn render_visitor_test_file(fixtures: &[&Fixture], header: &str, prefix: &str) -> String {
3747    use crate::fixture::CallbackAction;
3748
3749    let mut out = String::new();
3750    out.push_str(&hash::header(CommentStyle::Block));
3751    let _ = writeln!(out, "/* E2e tests for category: visitor */");
3752    let _ = writeln!(out);
3753    let _ = writeln!(out, "#include <assert.h>");
3754    let _ = writeln!(out, "#include <stdint.h>");
3755    let _ = writeln!(out, "#include <string.h>");
3756    let _ = writeln!(out, "#include <stdio.h>");
3757    let _ = writeln!(out, "#include <stdlib.h>");
3758    let _ = writeln!(out, "#include \"{header}\"");
3759    let _ = writeln!(out, "#include \"test_runner.h\"");
3760    let _ = writeln!(out);
3761
3762    let prefix_upper = prefix.to_uppercase();
3763
3764    for (i, fixture) in fixtures.iter().enumerate() {
3765        let fn_name = sanitize_ident(&fixture.id);
3766        let description = &fixture.description;
3767
3768        let visitor_spec = match &fixture.visitor {
3769            Some(v) => v,
3770            None => continue,
3771        };
3772
3773        let html = fixture.input.get("html").and_then(|v| v.as_str()).unwrap_or("");
3774        let html_escaped = escape_c(html);
3775
3776        let options_json = match fixture.input.get("options") {
3777            Some(opts) => serde_json::to_string(opts).unwrap_or_else(|_| "{}".to_string()),
3778            None => "{}".to_string(),
3779        };
3780        let options_escaped = escape_c(&options_json);
3781
3782        // Emit static callback functions for this fixture. Each callback is named
3783        // `c_visitor_<fixture_id>_<method>` to avoid collisions across fixtures.
3784        let mut sorted_callbacks: Vec<(&String, &CallbackAction)> = visitor_spec.callbacks.iter().collect();
3785        sorted_callbacks.sort_by(|a, b| a.0.cmp(b.0));
3786
3787        for (method, action) in &sorted_callbacks {
3788            let cb_name = format!("c_visitor_{fn_name}_{method}");
3789            let params = c_visitor_callback_params(method);
3790            let body = c_visitor_callback_body(method, action);
3791            let _ = writeln!(out, "static int32_t {cb_name}({params}) {{");
3792            out.push_str(&body);
3793            let _ = writeln!(out, "}}");
3794            let _ = writeln!(out);
3795        }
3796
3797        // Emit the test function.
3798        let _ = writeln!(out, "void test_{fn_name}(void) {{");
3799        let _ = writeln!(out, "    /* {description} */");
3800        let _ = writeln!(out);
3801
3802        // Build callbacks struct and wire each slot.
3803        let _ = writeln!(out, "    {prefix_upper}HtmVisitorCallbacks _callbacks;");
3804        let _ = writeln!(out, "    memset(&_callbacks, 0, sizeof(_callbacks));");
3805        for (method, _) in &sorted_callbacks {
3806            let cb_name = format!("c_visitor_{fn_name}_{method}");
3807            let _ = writeln!(out, "    _callbacks.{method} = {cb_name};");
3808        }
3809        let _ = writeln!(out);
3810
3811        // Create visitor handle.
3812        let _ = writeln!(
3813            out,
3814            "    {prefix_upper}HtmVisitor* _visitor = {prefix}_visitor_create(&_callbacks);"
3815        );
3816        let _ = writeln!(out, "    assert(_visitor != NULL && \"htm_visitor_create failed\");");
3817        let _ = writeln!(out);
3818
3819        // Create options handle.
3820        let _ = writeln!(
3821            out,
3822            "    {prefix_upper}ConversionOptions* _options = {prefix}_conversion_options_from_json(\"{options_escaped}\");"
3823        );
3824        let _ = writeln!(
3825            out,
3826            "    assert(_options != NULL && \"htm_conversion_options_from_json failed\");"
3827        );
3828        let _ = writeln!(out);
3829
3830        // Attach visitor to options.
3831        let _ = writeln!(out, "    {prefix}_options_set_visitor_handle(_options, _visitor);");
3832        let _ = writeln!(out);
3833
3834        // Call htm_convert.
3835        let _ = writeln!(
3836            out,
3837            "    {prefix_upper}ConversionResult* _result = {prefix}_convert(\"{html_escaped}\", _options);"
3838        );
3839        let _ = writeln!(out, "    assert(_result != NULL && \"htm_convert failed\");");
3840        let _ = writeln!(out);
3841
3842        // Serialise result to JSON and extract the content field.
3843        let _ = writeln!(out, "    char* _json = {prefix}_conversion_result_to_json(_result);");
3844        let _ = writeln!(out, "    assert(_json != NULL && \"result to_json failed\");");
3845        let _ = writeln!(out, "    char* _content = alef_json_get_string(_json, \"content\");");
3846        let _ = writeln!(out);
3847
3848        // Emit assertions (only contains/not_contains; visitor fixtures use only these).
3849        for assertion in &fixture.assertions {
3850            match assertion.assertion_type.as_str() {
3851                "contains" => {
3852                    if let Some(expected) = &assertion.value {
3853                        let c_val = json_to_c(expected);
3854                        let _ = writeln!(
3855                            out,
3856                            "    assert(_content != NULL && strstr(_content, {c_val}) != NULL && \"expected to contain substring\");"
3857                        );
3858                    }
3859                }
3860                "not_contains" => {
3861                    if let Some(expected) = &assertion.value {
3862                        let c_val = json_to_c(expected);
3863                        let _ = writeln!(
3864                            out,
3865                            "    assert((_content == NULL || strstr(_content, {c_val}) == NULL) && \"expected NOT to contain substring\");"
3866                        );
3867                    }
3868                }
3869                other => {
3870                    let _ = writeln!(
3871                        out,
3872                        "    /* assertion type '{other}' not supported in C visitor tests */"
3873                    );
3874                }
3875            }
3876        }
3877
3878        let _ = writeln!(out);
3879
3880        // Free in reverse allocation order.
3881        let _ = writeln!(out, "    free(_content);");
3882        let _ = writeln!(out, "    {prefix}_free_string(_json);");
3883        let _ = writeln!(out, "    {prefix}_conversion_result_free(_result);");
3884        let _ = writeln!(out, "    {prefix}_conversion_options_free(_options);");
3885        let _ = writeln!(out, "    {prefix}_visitor_free(_visitor);");
3886        let _ = writeln!(out, "}}");
3887
3888        if i + 1 < fixtures.len() {
3889            let _ = writeln!(out);
3890        }
3891    }
3892
3893    out
3894}
3895
3896/// C function-pointer parameter list for a given visitor callback method.
3897///
3898/// Mirrors the cbindgen-emitted `HTMHtmVisitorCallbacks` slot signatures from
3899/// `crates/html-to-markdown-ffi/include/html_to_markdown.h`.  Named parameters
3900/// are prefixed with `_` so the C compiler does not warn about unused params when
3901/// the callback body ignores them.
3902fn c_visitor_callback_params(method: &str) -> &'static str {
3903    match method {
3904        "visit_text" => {
3905            "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _text, char** out_custom, size_t* out_len"
3906        }
3907        "visit_element_start" => "const HTMHtmNodeContext* _ctx, void* _user_data, char** out_custom, size_t* out_len",
3908        "visit_element_end" => {
3909            "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _output, char** out_custom, size_t* out_len"
3910        }
3911        "visit_link" => {
3912            "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _href, const char* _text, const char* _title, char** out_custom, size_t* out_len"
3913        }
3914        "visit_image" => {
3915            "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _src, const char* _alt, const char* _title, char** out_custom, size_t* out_len"
3916        }
3917        "visit_heading" => {
3918            "const HTMHtmNodeContext* _ctx, void* _user_data, uint32_t _level, const char* _text, const char* _id, char** out_custom, size_t* out_len"
3919        }
3920        "visit_code_block" => {
3921            "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _lang, const char* _code, char** out_custom, size_t* out_len"
3922        }
3923        "visit_code_inline" => {
3924            "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _code, char** out_custom, size_t* out_len"
3925        }
3926        "visit_list_item" => {
3927            "const HTMHtmNodeContext* _ctx, void* _user_data, int32_t _ordered, const char* _marker, const char* _text, char** out_custom, size_t* out_len"
3928        }
3929        "visit_list_start" => {
3930            "const HTMHtmNodeContext* _ctx, void* _user_data, int32_t _ordered, char** out_custom, size_t* out_len"
3931        }
3932        "visit_list_end" => {
3933            "const HTMHtmNodeContext* _ctx, void* _user_data, int32_t _ordered, const char* _output, char** out_custom, size_t* out_len"
3934        }
3935        "visit_table_start" => "const HTMHtmNodeContext* _ctx, void* _user_data, char** out_custom, size_t* out_len",
3936        "visit_table_row" => {
3937            "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"
3938        }
3939        "visit_table_end" => {
3940            "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _output, char** out_custom, size_t* out_len"
3941        }
3942        "visit_blockquote" => {
3943            "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _content, size_t _depth, char** out_custom, size_t* out_len"
3944        }
3945        "visit_line_break" | "visit_horizontal_rule" | "visit_definition_list_start" | "visit_figure_start" => {
3946            "const HTMHtmNodeContext* _ctx, void* _user_data, char** out_custom, size_t* out_len"
3947        }
3948        "visit_custom_element" => {
3949            "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _tag_name, const char* _html, char** out_custom, size_t* out_len"
3950        }
3951        "visit_form" => {
3952            "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _action, const char* _method, char** out_custom, size_t* out_len"
3953        }
3954        "visit_input" => {
3955            "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _input_type, const char* _name, const char* _value, char** out_custom, size_t* out_len"
3956        }
3957        "visit_audio" | "visit_video" | "visit_iframe" => {
3958            "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _src, char** out_custom, size_t* out_len"
3959        }
3960        "visit_details" => {
3961            "const HTMHtmNodeContext* _ctx, void* _user_data, int32_t _open, char** out_custom, size_t* out_len"
3962        }
3963        "visit_figure_end" | "visit_definition_list_end" => {
3964            "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _output, char** out_custom, size_t* out_len"
3965        }
3966        // Default: single text payload (covers visit_strong, visit_emphasis,
3967        // visit_strikethrough, visit_underline, visit_subscript, visit_superscript,
3968        // visit_mark, visit_button, visit_summary, visit_figcaption,
3969        // visit_definition_term, visit_definition_description).
3970        _ => "const HTMHtmNodeContext* _ctx, void* _user_data, const char* _text, char** out_custom, size_t* out_len",
3971    }
3972}
3973
3974/// Build the body of a C visitor callback function for a given action.
3975///
3976/// Return values mirror the Rust `HTMVisitResult` discriminants:
3977///   0 = Continue, 1 = Skip, 2 = PreserveHtml, 3 = Custom.
3978///
3979/// For `Custom` and `CustomTemplate`, we heap-allocate a copy of the output string
3980/// with `strdup` (or a sprintf-allocated buffer) and pass its pointer and length back
3981/// via `out_custom`/`out_len`. The FFI runtime takes ownership and frees it.
3982fn c_visitor_callback_body(method: &str, action: &crate::fixture::CallbackAction) -> String {
3983    use crate::fixture::CallbackAction;
3984
3985    let mut out = String::new();
3986    // Suppress unused-parameter warnings for context and user_data — always ignored
3987    // in simple e2e test callbacks.
3988    let _ = writeln!(out, "    (void)_ctx;");
3989    let _ = writeln!(out, "    (void)_user_data;");
3990
3991    match action {
3992        CallbackAction::Skip => {
3993            let _ = writeln!(out, "    (void)out_custom;");
3994            let _ = writeln!(out, "    (void)out_len;");
3995            // Suppress method-specific params not used by Skip.
3996            for param in c_visitor_unused_params(method) {
3997                let _ = writeln!(out, "    (void){param};");
3998            }
3999            let _ = writeln!(out, "    return 1;");
4000        }
4001        CallbackAction::Continue => {
4002            let _ = writeln!(out, "    (void)out_custom;");
4003            let _ = writeln!(out, "    (void)out_len;");
4004            for param in c_visitor_unused_params(method) {
4005                let _ = writeln!(out, "    (void){param};");
4006            }
4007            let _ = writeln!(out, "    return 0;");
4008        }
4009        CallbackAction::PreserveHtml => {
4010            let _ = writeln!(out, "    (void)out_custom;");
4011            let _ = writeln!(out, "    (void)out_len;");
4012            for param in c_visitor_unused_params(method) {
4013                let _ = writeln!(out, "    (void){param};");
4014            }
4015            let _ = writeln!(out, "    return 2;");
4016        }
4017        CallbackAction::Custom { output } => {
4018            let escaped = escape_c(output);
4019            for param in c_visitor_unused_params(method) {
4020                let _ = writeln!(out, "    (void){param};");
4021            }
4022            let _ = writeln!(out, "    char* _buf = strdup(\"{escaped}\");");
4023            let _ = writeln!(out, "    if (out_custom) *out_custom = _buf;");
4024            let _ = writeln!(out, "    if (out_len) *out_len = _buf ? strlen(_buf) : 0;");
4025            let _ = writeln!(out, "    return 3;");
4026        }
4027        CallbackAction::CustomTemplate { template, .. } => {
4028            // Build a sprintf format string and map fixture placeholders to C params.
4029            let (c_fmt, placeholders) = c_visitor_template_to_sprintf(template);
4030            let escaped_fmt = escape_c(&c_fmt);
4031
4032            // Determine which method-specific params are used by the template.
4033            let used: std::collections::HashSet<&str> = placeholders.iter().map(|s| s.as_str()).collect();
4034            for param in c_visitor_unused_params(method) {
4035                let stripped = param.trim_start_matches('_');
4036                if !used.contains(stripped) {
4037                    let _ = writeln!(out, "    (void){param};");
4038                }
4039            }
4040
4041            if placeholders.is_empty() {
4042                let _ = writeln!(out, "    char* _buf = strdup(\"{escaped_fmt}\");");
4043            } else {
4044                // Compute the max output length. We over-estimate by adding 256 per
4045                // placeholder plus the template length.
4046                let max_len = template.len() + placeholders.len() * 256 + 64;
4047                let _ = writeln!(out, "    char* _buf = (char*)malloc({max_len});");
4048                let _ = writeln!(out, "    if (!_buf) {{ (void)out_custom; (void)out_len; return 0; }}");
4049                // Build the sprintf argument list.
4050                let args: Vec<String> = placeholders
4051                    .iter()
4052                    .map(|name| c_visitor_placeholder_to_arg(method, name))
4053                    .collect();
4054                let args_str = args.join(", ");
4055                let _ = writeln!(out, "    snprintf(_buf, {max_len}, \"{escaped_fmt}\", {args_str});");
4056            }
4057
4058            let _ = writeln!(out, "    if (out_custom) *out_custom = _buf;");
4059            let _ = writeln!(out, "    if (out_len) *out_len = _buf ? strlen(_buf) : 0;");
4060            let _ = writeln!(out, "    return 3;");
4061        }
4062    }
4063
4064    out
4065}
4066
4067/// List of method-specific typed C parameter names to suppress with `(void)` when
4068/// the callback body does not reference them.  Mirrors `unused_params_for` in
4069/// `zig_visitors.rs` but uses the C parameter names from `c_visitor_callback_params`.
4070fn c_visitor_unused_params(method: &str) -> Vec<&'static str> {
4071    match method {
4072        "visit_text" => vec!["_text"],
4073        "visit_element_start"
4074        | "visit_table_start"
4075        | "visit_line_break"
4076        | "visit_horizontal_rule"
4077        | "visit_definition_list_start"
4078        | "visit_figure_start" => vec![],
4079        "visit_element_end" | "visit_table_end" | "visit_figure_end" | "visit_definition_list_end" => {
4080            vec!["_output"]
4081        }
4082        "visit_link" => vec!["_href", "_text", "_title"],
4083        "visit_image" => vec!["_src", "_alt", "_title"],
4084        "visit_heading" => vec!["_level", "_text", "_id"],
4085        "visit_code_block" => vec!["_lang", "_code"],
4086        "visit_code_inline" => vec!["_code"],
4087        "visit_list_item" => vec!["_ordered", "_marker", "_text"],
4088        "visit_list_start" => vec!["_ordered"],
4089        "visit_list_end" => vec!["_ordered", "_output"],
4090        "visit_table_row" => vec!["_cells", "_cell_count", "_is_header"],
4091        "visit_blockquote" => vec!["_content", "_depth"],
4092        "visit_custom_element" => vec!["_tag_name", "_html"],
4093        "visit_form" => vec!["_action", "_method"],
4094        "visit_input" => vec!["_input_type", "_name", "_value"],
4095        "visit_audio" | "visit_video" | "visit_iframe" => vec!["_src"],
4096        "visit_details" => vec!["_open"],
4097        // Default: text-only methods.
4098        _ => vec!["_text"],
4099    }
4100}
4101
4102/// Convert a fixture `{placeholder}` template into a `printf`/`snprintf` format string
4103/// and an ordered list of placeholder names.  Integer placeholders use `%d` or `%u`;
4104/// everything else uses `%s`.
4105fn c_visitor_template_to_sprintf(template: &str) -> (String, Vec<String>) {
4106    let mut out = String::with_capacity(template.len());
4107    let mut placeholders: Vec<String> = Vec::new();
4108    let mut chars = template.chars().peekable();
4109    while let Some(ch) = chars.next() {
4110        match ch {
4111            '{' => {
4112                if chars.peek() == Some(&'{') {
4113                    chars.next();
4114                    out.push('{');
4115                    continue;
4116                }
4117                let mut name = String::new();
4118                while let Some(&peek) = chars.peek() {
4119                    if peek == '}' {
4120                        chars.next();
4121                        break;
4122                    }
4123                    name.push(peek);
4124                    chars.next();
4125                }
4126                let is_int = matches!(name.as_str(), "level" | "depth" | "ordered" | "open" | "is_header");
4127                if is_int {
4128                    out.push_str("%d");
4129                } else {
4130                    out.push_str("%s");
4131                }
4132                placeholders.push(name);
4133            }
4134            '}' => {
4135                if chars.peek() == Some(&'}') {
4136                    chars.next();
4137                }
4138                out.push('}');
4139            }
4140            '%' => {
4141                // Escape literal percent signs for printf.
4142                out.push_str("%%");
4143            }
4144            other => out.push(other),
4145        }
4146    }
4147    (out, placeholders)
4148}
4149
4150/// Map a fixture placeholder name (e.g. `href`, `text`) to the C expression that
4151/// yields the value for that parameter slot in the callback's sprintf call.
4152fn c_visitor_placeholder_to_arg(method: &str, name: &str) -> String {
4153    let int_placeholder = matches!(
4154        (method, name),
4155        ("visit_heading", "level")
4156            | ("visit_blockquote", "depth")
4157            | ("visit_list_item", "ordered")
4158            | ("visit_list_start", "ordered")
4159            | ("visit_list_end", "ordered")
4160            | ("visit_details", "open")
4161            | ("visit_table_row", "is_header")
4162    );
4163    if int_placeholder {
4164        return format!("_{name}");
4165    }
4166    // String parameters — use the named `_<name>` C param directly.
4167    // The C param is already a `const char*`; pass it directly to `%s`.
4168    // Guard against NULL to avoid UB in printf (some implementations crash on NULL %s).
4169    format!("(_{name} ? _{name} : \"\")")
4170}