Skip to main content

alef_e2e/codegen/
c.rs

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