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;
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    )
48}
49
50impl E2eCodegen for CCodegen {
51    fn generate(
52        &self,
53        groups: &[FixtureGroup],
54        e2e_config: &E2eConfig,
55        config: &ResolvedCrateConfig,
56    ) -> Result<Vec<GeneratedFile>> {
57        let lang = self.language_name();
58        let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
59
60        let mut files = Vec::new();
61
62        // Resolve default call config with overrides.
63        let call = &e2e_config.call;
64        let overrides = call.overrides.get(lang);
65        let result_var = &call.result_var;
66        let prefix = overrides
67            .and_then(|o| o.prefix.as_ref())
68            .cloned()
69            .or_else(|| config.ffi.as_ref().and_then(|ffi| ffi.prefix.as_ref()).cloned())
70            .unwrap_or_default();
71        let header = overrides
72            .and_then(|o| o.header.as_ref())
73            .cloned()
74            .unwrap_or_else(|| config.ffi_header_name());
75
76        // Resolve package config.
77        let c_pkg = e2e_config.resolve_package("c");
78        let lib_name = c_pkg
79            .as_ref()
80            .and_then(|p| p.name.as_ref())
81            .cloned()
82            .unwrap_or_else(|| config.ffi_lib_name());
83
84        // Filter active groups (with non-skipped fixtures).
85        let active_groups: Vec<(&FixtureGroup, Vec<&Fixture>)> = groups
86            .iter()
87            .filter_map(|group| {
88                let active: Vec<&Fixture> = group
89                    .fixtures
90                    .iter()
91                    .filter(|f| super::should_include_fixture(f, lang, e2e_config))
92                    .filter(|f| f.visitor.is_none())
93                    .collect();
94                if active.is_empty() { None } else { Some((group, active)) }
95            })
96            .collect();
97
98        // Resolve FFI crate path for local repo builds.
99        // Default to `../../crates/{name}-ffi` derived from the crate name so that
100        // projects like `liter-llm` resolve to `../../crates/liter-llm-ffi/include/`
101        // rather than the generic (incorrect) `../../crates/ffi`.
102        // When `[crates.output] ffi` is set explicitly, derive the crate path from
103        // that value so that renamed FFI crates (e.g. `ts-pack-core-ffi`) resolve
104        // correctly without any hardcoded special cases.
105        let ffi_crate_path = c_pkg
106            .as_ref()
107            .and_then(|p| p.path.as_ref())
108            .cloned()
109            .unwrap_or_else(|| config.ffi_crate_path());
110
111        // Generate Makefile.
112        let category_names: Vec<String> = active_groups
113            .iter()
114            .map(|(g, _)| sanitize_filename(&g.category))
115            .collect();
116        files.push(GeneratedFile {
117            path: output_base.join("Makefile"),
118            content: render_makefile(&category_names, &header, &ffi_crate_path, &lib_name),
119            generated_header: true,
120        });
121
122        // Generate download_ffi.sh for downloading prebuilt FFI from GitHub releases.
123        let github_repo = config.github_repo();
124        let version = config.resolved_version().unwrap_or_else(|| "0.0.0".to_string());
125        let ffi_pkg_name = e2e_config
126            .registry
127            .packages
128            .get("c")
129            .and_then(|p| p.name.as_ref())
130            .cloned()
131            .unwrap_or_else(|| lib_name.clone());
132        files.push(GeneratedFile {
133            path: output_base.join("download_ffi.sh"),
134            content: render_download_script(&github_repo, &version, &ffi_pkg_name),
135            generated_header: true,
136        });
137
138        // Generate test_runner.h.
139        files.push(GeneratedFile {
140            path: output_base.join("test_runner.h"),
141            content: render_test_runner_header(&active_groups),
142            generated_header: true,
143        });
144
145        // Generate main.c.
146        files.push(GeneratedFile {
147            path: output_base.join("main.c"),
148            content: render_main_c(&active_groups),
149            generated_header: true,
150        });
151
152        let field_resolver = FieldResolver::new(
153            &e2e_config.fields,
154            &e2e_config.fields_optional,
155            &e2e_config.result_fields,
156            &e2e_config.fields_array,
157            &std::collections::HashSet::new(),
158        );
159
160        // Generate per-category test files.
161        // Each fixture may reference a named call config (fixture.call), so we pass
162        // e2e_config to render_test_file so it can resolve per-fixture call settings.
163        for (group, active) in &active_groups {
164            let filename = format!("test_{}.c", sanitize_filename(&group.category));
165            let content = render_test_file(
166                &group.category,
167                active,
168                &header,
169                &prefix,
170                result_var,
171                e2e_config,
172                lang,
173                &field_resolver,
174            );
175            files.push(GeneratedFile {
176                path: output_base.join(filename),
177                content,
178                generated_header: true,
179            });
180        }
181
182        Ok(files)
183    }
184
185    fn language_name(&self) -> &'static str {
186        "c"
187    }
188}
189
190/// Resolve per-call-config C-specific settings for a given call config and lang.
191struct ResolvedCallInfo {
192    function_name: String,
193    result_type_name: String,
194    options_type_name: String,
195    client_factory: Option<String>,
196    args: Vec<crate::config::ArgMapping>,
197    raw_c_result_type: Option<String>,
198    c_free_fn: Option<String>,
199    result_is_option: bool,
200    /// When `true`, the FFI signature for this method follows the byte-buffer
201    /// out-pointer pattern: `int32_t fn(this, req, uint8_t** out_ptr,
202    /// uintptr_t* out_len, uintptr_t* out_cap)`. The C codegen emits out-param
203    /// declarations, a status-code check, and `<prefix>_free_bytes` rather
204    /// than treating the result as an opaque response handle.
205    result_is_bytes: bool,
206    /// Per-language `extra_args` from call overrides — verbatim trailing
207    /// arguments appended after the configured `args`. The C codegen passes
208    /// `NULL` for absent optional pointers via this mechanism.
209    extra_args: Vec<String>,
210}
211
212fn resolve_call_info(call: &CallConfig, lang: &str) -> ResolvedCallInfo {
213    let overrides = call.overrides.get(lang);
214    let function_name = overrides
215        .and_then(|o| o.function.as_ref())
216        .cloned()
217        .unwrap_or_else(|| call.function.clone());
218    // Fall back to the *base* (non-C-overridden) function name when no explicit
219    // result_type is set.  Using the C-overridden name (e.g. "htm_convert") would
220    // produce a doubled-prefix type like `HTMHtmConvert*`; the base name
221    // ("convert") yields the correct `HTMConvert*` shape.
222    let result_type_name = overrides
223        .and_then(|o| o.result_type.as_ref())
224        .cloned()
225        .unwrap_or_else(|| call.function.to_pascal_case());
226    let options_type_name = overrides
227        .and_then(|o| o.options_type.as_deref())
228        .unwrap_or("ConversionOptions")
229        .to_string();
230    let client_factory = overrides.and_then(|o| o.client_factory.as_ref()).cloned();
231    let raw_c_result_type = overrides.and_then(|o| o.raw_c_result_type.clone());
232    let c_free_fn = overrides.and_then(|o| o.c_free_fn.clone());
233    let result_is_option = overrides
234        .and_then(|o| if o.result_is_option { Some(true) } else { None })
235        .unwrap_or(call.result_is_option);
236    // result_is_bytes is read from either the call-level config (preferred —
237    // the byte-buffer FFI shape is identical across languages that use the
238    // same FFI crate) or the per-language override (back-compat with the
239    // pattern used by Java / PHP / etc.).
240    let result_is_bytes = call.result_is_bytes || overrides.is_some_and(|o| o.result_is_bytes);
241    let extra_args = overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
242    ResolvedCallInfo {
243        function_name,
244        result_type_name,
245        options_type_name,
246        client_factory,
247        args: call.args.clone(),
248        raw_c_result_type,
249        c_free_fn,
250        result_is_option,
251        result_is_bytes,
252        extra_args,
253    }
254}
255
256/// Resolve call info for a fixture, with fallback to default call's client_factory.
257///
258/// Named call configs (e.g. `[e2e.calls.embed]`) may not repeat the `client_factory`
259/// setting. We fall back to the default `[e2e.call]` override's client_factory so that
260/// all methods on the same client use the same pattern.
261fn resolve_fixture_call_info(fixture: &Fixture, e2e_config: &E2eConfig, lang: &str) -> ResolvedCallInfo {
262    let call = e2e_config.resolve_call(fixture.call.as_deref());
263    let mut info = resolve_call_info(call, lang);
264
265    // Fallback: if the named call has no client_factory override, inherit from the
266    // default call config so all calls use the same client pattern.
267    if info.client_factory.is_none() {
268        let default_overrides = e2e_config.call.overrides.get(lang);
269        if let Some(factory) = default_overrides.and_then(|o| o.client_factory.as_ref()) {
270            info.client_factory = Some(factory.clone());
271        }
272    }
273
274    info
275}
276
277fn render_makefile(categories: &[String], header_name: &str, ffi_crate_path: &str, lib_name: &str) -> String {
278    let mut out = String::new();
279    out.push_str(&hash::header(CommentStyle::Hash));
280    let _ = writeln!(out, "CC = gcc");
281    let _ = writeln!(out, "FFI_DIR = ffi");
282    let _ = writeln!(out);
283
284    // Rust's cdylib output normalizes hyphens to underscores in the filename
285    // (e.g. crate "html-to-markdown-ffi" → "libhtml_to_markdown_ffi.dylib").
286    // The -l linker flag must therefore use the underscore form, while the
287    // pkg-config package name retains the original form (as declared in the .pc file).
288    let link_lib_name = lib_name.replace('-', "_");
289
290    // 3-path fallback: ffi/ (download script) -> local repo build -> pkg-config.
291    let _ = writeln!(out, "ifneq ($(wildcard $(FFI_DIR)/include/{header_name}),)");
292    let _ = writeln!(out, "    CFLAGS = -Wall -Wextra -I. -I$(FFI_DIR)/include");
293    let _ = writeln!(
294        out,
295        "    LDFLAGS = -L$(FFI_DIR)/lib -l{link_lib_name} -Wl,-rpath,$(FFI_DIR)/lib"
296    );
297    let _ = writeln!(out, "else ifneq ($(wildcard {ffi_crate_path}/include/{header_name}),)");
298    let _ = writeln!(out, "    CFLAGS = -Wall -Wextra -I. -I{ffi_crate_path}/include");
299    let _ = writeln!(
300        out,
301        "    LDFLAGS = -L../../target/release -l{link_lib_name} -Wl,-rpath,../../target/release"
302    );
303    let _ = writeln!(out, "else");
304    let _ = writeln!(
305        out,
306        "    CFLAGS = -Wall -Wextra -I. $(shell pkg-config --cflags {lib_name} 2>/dev/null)"
307    );
308    let _ = writeln!(out, "    LDFLAGS = $(shell pkg-config --libs {lib_name} 2>/dev/null)");
309    let _ = writeln!(out, "endif");
310    let _ = writeln!(out);
311
312    let src_files: Vec<String> = categories.iter().map(|c| format!("test_{c}.c")).collect();
313    let srcs = src_files.join(" ");
314
315    let _ = writeln!(out, "SRCS = main.c {srcs}");
316    let _ = writeln!(out, "TARGET = run_tests");
317    let _ = writeln!(out);
318    let _ = writeln!(out, ".PHONY: all clean test");
319    let _ = writeln!(out);
320    let _ = writeln!(out, "all: $(TARGET)");
321    let _ = writeln!(out);
322    let _ = writeln!(out, "$(TARGET): $(SRCS)");
323    let _ = writeln!(out, "\t$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)");
324    let _ = writeln!(out);
325    // The `test:` target spawns the e2e mock-server binary, captures its
326    // assigned MOCK_SERVER_URL line on stdout, exports it for the test process,
327    // runs the suite, then tears the server down. This mirrors the per-language
328    // conftest/setup machinery used by Python, Ruby, Java, etc.
329    let _ = writeln!(out, "MOCK_SERVER_BIN ?= ../rust/target/release/mock-server");
330    let _ = writeln!(out, "FIXTURES_DIR ?= ../../fixtures");
331    let _ = writeln!(out);
332    let _ = writeln!(out, "test: $(TARGET)");
333    let _ = writeln!(out, "\t@if [ -n \"$$MOCK_SERVER_URL\" ]; then \\");
334    let _ = writeln!(out, "\t\t./$(TARGET); \\");
335    let _ = writeln!(out, "\telse \\");
336    let _ = writeln!(out, "\t\tif [ ! -x \"$(MOCK_SERVER_BIN)\" ]; then \\");
337    let _ = writeln!(
338        out,
339        "\t\t\techo \"mock-server binary not found at $(MOCK_SERVER_BIN); run: cargo build --manifest-path ../rust/Cargo.toml --bin mock-server --release\" >&2; \\"
340    );
341    let _ = writeln!(out, "\t\t\texit 1; \\");
342    let _ = writeln!(out, "\t\tfi; \\");
343    let _ = writeln!(out, "\t\trm -f mock_server.stdout mock_server.stdin; \\");
344    let _ = writeln!(out, "\t\tmkfifo mock_server.stdin; \\");
345    let _ = writeln!(
346        out,
347        "\t\t\"$(MOCK_SERVER_BIN)\" \"$(FIXTURES_DIR)\" <mock_server.stdin >mock_server.stdout 2>&1 & \\"
348    );
349    let _ = writeln!(out, "\t\tMOCK_PID=$$!; \\");
350    let _ = writeln!(out, "\t\texec 9>mock_server.stdin; \\");
351    let _ = writeln!(out, "\t\tMOCK_URL=\"\"; \\");
352    let _ = writeln!(out, "\t\tfor _ in $$(seq 1 50); do \\");
353    let _ = writeln!(out, "\t\t\tif [ -s mock_server.stdout ]; then \\");
354    let _ = writeln!(
355        out,
356        "\t\t\t\tMOCK_URL=$$(grep -o 'MOCK_SERVER_URL=[^ ]*' mock_server.stdout | head -1 | cut -d= -f2); \\"
357    );
358    let _ = writeln!(out, "\t\t\t\tif [ -n \"$$MOCK_URL\" ]; then break; fi; \\");
359    let _ = writeln!(out, "\t\t\tfi; \\");
360    let _ = writeln!(out, "\t\t\tsleep 0.1; \\");
361    let _ = writeln!(out, "\t\tdone; \\");
362    let _ = writeln!(
363        out,
364        "\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; \\"
365    );
366    let _ = writeln!(out, "\t\tMOCK_SERVER_URL=\"$$MOCK_URL\" ./$(TARGET); STATUS=$$?; \\");
367    let _ = writeln!(out, "\t\texec 9>&-; \\");
368    let _ = writeln!(out, "\t\tkill $$MOCK_PID 2>/dev/null || true; \\");
369    let _ = writeln!(out, "\t\trm -f mock_server.stdout mock_server.stdin; \\");
370    let _ = writeln!(out, "\t\texit $$STATUS; \\");
371    let _ = writeln!(out, "\tfi");
372    let _ = writeln!(out);
373    let _ = writeln!(out, "clean:");
374    let _ = writeln!(out, "\trm -f $(TARGET) mock_server.stdout mock_server.stdin");
375    out
376}
377
378fn render_download_script(github_repo: &str, version: &str, ffi_pkg_name: &str) -> String {
379    let mut out = String::new();
380    let _ = writeln!(out, "#!/usr/bin/env bash");
381    out.push_str(&hash::header(CommentStyle::Hash));
382    let _ = writeln!(out, "set -euo pipefail");
383    let _ = writeln!(out);
384    let _ = writeln!(out, "REPO_URL=\"{github_repo}\"");
385    let _ = writeln!(out, "VERSION=\"{version}\"");
386    let _ = writeln!(out, "FFI_PKG_NAME=\"{ffi_pkg_name}\"");
387    let _ = writeln!(out, "FFI_DIR=\"ffi\"");
388    let _ = writeln!(out);
389    let _ = writeln!(out, "# Detect OS and architecture.");
390    let _ = writeln!(out, "OS=\"$(uname -s | tr '[:upper:]' '[:lower:]')\"");
391    let _ = writeln!(out, "ARCH=\"$(uname -m)\"");
392    let _ = writeln!(out);
393    let _ = writeln!(out, "case \"$ARCH\" in");
394    let _ = writeln!(out, "x86_64 | amd64) ARCH=\"x86_64\" ;;");
395    let _ = writeln!(out, "arm64 | aarch64) ARCH=\"aarch64\" ;;");
396    let _ = writeln!(out, "*)");
397    let _ = writeln!(out, "  echo \"Unsupported architecture: $ARCH\" >&2");
398    let _ = writeln!(out, "  exit 1");
399    let _ = writeln!(out, "  ;;");
400    let _ = writeln!(out, "esac");
401    let _ = writeln!(out);
402    let _ = writeln!(out, "case \"$OS\" in");
403    let _ = writeln!(out, "linux) TRIPLE=\"${{ARCH}}-unknown-linux-gnu\" ;;");
404    let _ = writeln!(out, "darwin) TRIPLE=\"${{ARCH}}-apple-darwin\" ;;");
405    let _ = writeln!(out, "*)");
406    let _ = writeln!(out, "  echo \"Unsupported OS: $OS\" >&2");
407    let _ = writeln!(out, "  exit 1");
408    let _ = writeln!(out, "  ;;");
409    let _ = writeln!(out, "esac");
410    let _ = writeln!(out);
411    let _ = writeln!(out, "ARCHIVE=\"${{FFI_PKG_NAME}}-${{TRIPLE}}.tar.gz\"");
412    let _ = writeln!(
413        out,
414        "URL=\"${{REPO_URL}}/releases/download/v${{VERSION}}/${{ARCHIVE}}\""
415    );
416    let _ = writeln!(out);
417    let _ = writeln!(out, "echo \"Downloading ${{ARCHIVE}} from v${{VERSION}}...\"");
418    let _ = writeln!(out, "mkdir -p \"$FFI_DIR\"");
419    let _ = writeln!(out, "curl -fSL \"$URL\" | tar xz -C \"$FFI_DIR\"");
420    let _ = writeln!(out, "echo \"FFI library extracted to $FFI_DIR/\"");
421    out
422}
423
424fn render_test_runner_header(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
425    let mut out = String::new();
426    out.push_str(&hash::header(CommentStyle::Block));
427    let _ = writeln!(out, "#ifndef TEST_RUNNER_H");
428    let _ = writeln!(out, "#define TEST_RUNNER_H");
429    let _ = writeln!(out);
430    let _ = writeln!(out, "#include <string.h>");
431    let _ = writeln!(out, "#include <stdlib.h>");
432    let _ = writeln!(out);
433    // Trim helper for comparing strings that may have trailing whitespace/newlines.
434    let _ = writeln!(out, "/**");
435    let _ = writeln!(
436        out,
437        " * Compare a string against an expected value, trimming trailing whitespace."
438    );
439    let _ = writeln!(
440        out,
441        " * Returns 0 if the trimmed actual string equals the expected string."
442    );
443    let _ = writeln!(out, " */");
444    let _ = writeln!(
445        out,
446        "static inline int str_trim_eq(const char *actual, const char *expected) {{"
447    );
448    let _ = writeln!(
449        out,
450        "    if (actual == NULL || expected == NULL) return actual != expected;"
451    );
452    let _ = writeln!(out, "    size_t alen = strlen(actual);");
453    let _ = writeln!(
454        out,
455        "    while (alen > 0 && (actual[alen-1] == ' ' || actual[alen-1] == '\\n' || actual[alen-1] == '\\r' || actual[alen-1] == '\\t')) alen--;"
456    );
457    let _ = writeln!(out, "    size_t elen = strlen(expected);");
458    let _ = writeln!(out, "    if (alen != elen) return 1;");
459    let _ = writeln!(out, "    return memcmp(actual, expected, elen);");
460    let _ = writeln!(out, "}}");
461    let _ = writeln!(out);
462
463    let _ = writeln!(out, "/**");
464    let _ = writeln!(
465        out,
466        " * Extract a string value for a given key from a JSON object string."
467    );
468    let _ = writeln!(
469        out,
470        " * Returns a heap-allocated copy of the value, or NULL if not found."
471    );
472    let _ = writeln!(out, " * Caller must free() the returned string.");
473    let _ = writeln!(out, " */");
474    let _ = writeln!(
475        out,
476        "static inline char *alef_json_get_string(const char *json, const char *key) {{"
477    );
478    let _ = writeln!(out, "    if (json == NULL || key == NULL) return NULL;");
479    let _ = writeln!(out, "    /* Build search pattern: \"key\":  */");
480    let _ = writeln!(out, "    size_t key_len = strlen(key);");
481    let _ = writeln!(out, "    char *pattern = (char *)malloc(key_len + 5);");
482    let _ = writeln!(out, "    if (!pattern) return NULL;");
483    let _ = writeln!(out, "    pattern[0] = '\"';");
484    let _ = writeln!(out, "    memcpy(pattern + 1, key, key_len);");
485    let _ = writeln!(out, "    pattern[key_len + 1] = '\"';");
486    let _ = writeln!(out, "    pattern[key_len + 2] = ':';");
487    let _ = writeln!(out, "    pattern[key_len + 3] = '\\0';");
488    let _ = writeln!(out, "    const char *found = strstr(json, pattern);");
489    let _ = writeln!(out, "    free(pattern);");
490    let _ = writeln!(out, "    if (!found) return NULL;");
491    let _ = writeln!(out, "    found += key_len + 3; /* skip past \"key\": */");
492    let _ = writeln!(out, "    while (*found == ' ' || *found == '\\t') found++;");
493    let _ = writeln!(out, "    if (*found != '\"') return NULL; /* not a string value */");
494    let _ = writeln!(out, "    found++; /* skip opening quote */");
495    let _ = writeln!(out, "    const char *end = found;");
496    let _ = writeln!(out, "    while (*end && *end != '\"') {{");
497    let _ = writeln!(out, "        if (*end == '\\\\') {{ end++; if (*end) end++; }}");
498    let _ = writeln!(out, "        else end++;");
499    let _ = writeln!(out, "    }}");
500    let _ = writeln!(out, "    size_t val_len = (size_t)(end - found);");
501    let _ = writeln!(out, "    char *result_str = (char *)malloc(val_len + 1);");
502    let _ = writeln!(out, "    if (!result_str) return NULL;");
503    let _ = writeln!(out, "    memcpy(result_str, found, val_len);");
504    let _ = writeln!(out, "    result_str[val_len] = '\\0';");
505    let _ = writeln!(out, "    return result_str;");
506    let _ = writeln!(out, "}}");
507    let _ = writeln!(out);
508    let _ = writeln!(out, "/**");
509    let _ = writeln!(out, " * Count top-level elements in a JSON array string.");
510    let _ = writeln!(out, " * Returns 0 for empty arrays (\"[]\") or NULL input.");
511    let _ = writeln!(out, " */");
512    let _ = writeln!(out, "static inline int alef_json_array_count(const char *json) {{");
513    let _ = writeln!(out, "    if (json == NULL) return 0;");
514    let _ = writeln!(out, "    /* Skip leading whitespace */");
515    let _ = writeln!(
516        out,
517        "    while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
518    );
519    let _ = writeln!(out, "    if (*json != '[') return 0;");
520    let _ = writeln!(out, "    json++;");
521    let _ = writeln!(out, "    /* Skip whitespace after '[' */");
522    let _ = writeln!(
523        out,
524        "    while (*json == ' ' || *json == '\\t' || *json == '\\n') json++;"
525    );
526    let _ = writeln!(out, "    if (*json == ']') return 0;");
527    let _ = writeln!(out, "    int count = 1;");
528    let _ = writeln!(out, "    int depth = 0;");
529    let _ = writeln!(out, "    int in_string = 0;");
530    let _ = writeln!(
531        out,
532        "    for (; *json && !(*json == ']' && depth == 0 && !in_string); json++) {{"
533    );
534    let _ = writeln!(out, "        if (*json == '\\\\' && in_string) {{ json++; continue; }}");
535    let _ = writeln!(
536        out,
537        "        if (*json == '\"') {{ in_string = !in_string; continue; }}"
538    );
539    let _ = writeln!(out, "        if (in_string) continue;");
540    let _ = writeln!(out, "        if (*json == '[' || *json == '{{') depth++;");
541    let _ = writeln!(out, "        else if (*json == ']' || *json == '}}') depth--;");
542    let _ = writeln!(out, "        else if (*json == ',' && depth == 0) count++;");
543    let _ = writeln!(out, "    }}");
544    let _ = writeln!(out, "    return count;");
545    let _ = writeln!(out, "}}");
546    let _ = writeln!(out);
547
548    for (group, fixtures) in active_groups {
549        let _ = writeln!(out, "/* Tests for category: {} */", group.category);
550        for fixture in fixtures {
551            let fn_name = sanitize_ident(&fixture.id);
552            let _ = writeln!(out, "void test_{fn_name}(void);");
553        }
554        let _ = writeln!(out);
555    }
556
557    let _ = writeln!(out, "#endif /* TEST_RUNNER_H */");
558    out
559}
560
561fn render_main_c(active_groups: &[(&FixtureGroup, Vec<&Fixture>)]) -> String {
562    let mut out = String::new();
563    out.push_str(&hash::header(CommentStyle::Block));
564    let _ = writeln!(out, "#include <stdio.h>");
565    let _ = writeln!(out, "#include \"test_runner.h\"");
566    let _ = writeln!(out);
567    let _ = writeln!(out, "int main(void) {{");
568    let _ = writeln!(out, "    int passed = 0;");
569    let _ = writeln!(out);
570
571    for (group, fixtures) in active_groups {
572        let _ = writeln!(out, "    /* Category: {} */", group.category);
573        for fixture in fixtures {
574            let fn_name = sanitize_ident(&fixture.id);
575            let _ = writeln!(out, "    printf(\"  Running test_{fn_name}...\");");
576            let _ = writeln!(out, "    test_{fn_name}();");
577            let _ = writeln!(out, "    printf(\" PASSED\\n\");");
578            let _ = writeln!(out, "    passed++;");
579        }
580        let _ = writeln!(out);
581    }
582
583    let _ = writeln!(out, "    printf(\"\\nResults: %d passed, 0 failed\\n\", passed);");
584    let _ = writeln!(out, "    return 0;");
585    let _ = writeln!(out, "}}");
586    out
587}
588
589#[allow(clippy::too_many_arguments)]
590fn render_test_file(
591    category: &str,
592    fixtures: &[&Fixture],
593    header: &str,
594    prefix: &str,
595    result_var: &str,
596    e2e_config: &E2eConfig,
597    lang: &str,
598    field_resolver: &FieldResolver,
599) -> String {
600    let mut out = String::new();
601    out.push_str(&hash::header(CommentStyle::Block));
602    let _ = writeln!(out, "/* E2e tests for category: {category} */");
603    let _ = writeln!(out);
604    let _ = writeln!(out, "#include <assert.h>");
605    let _ = writeln!(out, "#include <string.h>");
606    let _ = writeln!(out, "#include <stdio.h>");
607    let _ = writeln!(out, "#include <stdlib.h>");
608    let _ = writeln!(out, "#include \"{header}\"");
609    let _ = writeln!(out, "#include \"test_runner.h\"");
610    let _ = writeln!(out);
611
612    for (i, fixture) in fixtures.iter().enumerate() {
613        // Visitor fixtures are filtered out before render_test_file is called.
614        // This guard is a safety net in case a fixture reaches here unexpectedly.
615        if fixture.visitor.is_some() {
616            panic!(
617                "C e2e generator: visitor pattern not supported for fixture: {}",
618                fixture.id
619            );
620        }
621
622        let call_info = resolve_fixture_call_info(fixture, e2e_config, lang);
623        render_test_function(
624            &mut out,
625            fixture,
626            prefix,
627            &call_info.function_name,
628            result_var,
629            &call_info.args,
630            field_resolver,
631            &e2e_config.fields_c_types,
632            &call_info.result_type_name,
633            &call_info.options_type_name,
634            call_info.client_factory.as_deref(),
635            call_info.raw_c_result_type.as_deref(),
636            call_info.c_free_fn.as_deref(),
637            call_info.result_is_option,
638            call_info.result_is_bytes,
639            &call_info.extra_args,
640        );
641        if i + 1 < fixtures.len() {
642            let _ = writeln!(out);
643        }
644    }
645
646    out
647}
648
649#[allow(clippy::too_many_arguments)]
650fn render_test_function(
651    out: &mut String,
652    fixture: &Fixture,
653    prefix: &str,
654    function_name: &str,
655    result_var: &str,
656    args: &[crate::config::ArgMapping],
657    field_resolver: &FieldResolver,
658    fields_c_types: &HashMap<String, String>,
659    result_type_name: &str,
660    options_type_name: &str,
661    client_factory: Option<&str>,
662    raw_c_result_type: Option<&str>,
663    c_free_fn: Option<&str>,
664    result_is_option: bool,
665    result_is_bytes: bool,
666    extra_args: &[String],
667) {
668    let fn_name = sanitize_ident(&fixture.id);
669    let description = &fixture.description;
670
671    let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
672
673    let _ = writeln!(out, "void test_{fn_name}(void) {{");
674    let _ = writeln!(out, "    /* {description} */");
675
676    let prefix_upper = prefix.to_uppercase();
677
678    // Streaming pattern: chat_stream uses an FFI iterator handle instead of a
679    // single response. Emit start/next/free loop and aggregate per-chunk data
680    // into local vars (chunks_count, stream_content, stream_complete) so fixture
681    // assertions on pseudo-fields resolve to those locals rather than to
682    // non-existent accessor functions on a single chunk handle.
683    if client_factory.is_some() && function_name == "chat_stream" {
684        render_chat_stream_test_function(out, fixture, prefix, result_var, args, options_type_name, expects_error);
685        return;
686    }
687
688    // Byte-buffer pattern: methods like `speech` and `file_content` return raw
689    // bytes via the out-pointer FFI shape:
690    //   `int32_t fn(this, req, uint8_t** out_ptr, uintptr_t* out_len, uintptr_t* out_cap)`
691    // rather than as an opaque `*Response` handle. The C codegen must declare
692    // the out-params, check the int32_t status code, and free with
693    // `<prefix>_free_bytes` rather than emitting non-existent
694    // `<prefix>_<response>_audio` / `_content` accessors.
695    if let Some(factory) = client_factory {
696        if result_is_bytes {
697            render_bytes_test_function(
698                out,
699                fixture,
700                prefix,
701                function_name,
702                result_var,
703                args,
704                options_type_name,
705                result_type_name,
706                factory,
707                expects_error,
708            );
709            return;
710        }
711    }
712
713    // Client pattern: used when client_factory is configured (e.g. liter-llm).
714    // Builds typed request handles from json_object args, creates a client via the
715    // factory function, calls {prefix}_default_client_{function_name}(client, req),
716    // then frees result, request handles, and client.
717    if let Some(factory) = client_factory {
718        let mut request_handle_vars: Vec<(String, String)> = Vec::new(); // (arg_name, var_name)
719        // Inline argument expressions appended after request handles in the
720        // method call (e.g. literal C strings for `string` args, `NULL` for
721        // optional pointer args). Order matches the position in `args`.
722        let mut inline_method_args: Vec<String> = Vec::new();
723
724        for arg in args {
725            if arg.arg_type == "json_object" {
726                // Prefer options_type from the C override when set, since the result
727                // type isn't always a clean strip-Response/append-Request transform
728                // (e.g. transcribe -> Create**Transcription**Request, not TranscriptionRequest).
729                // Fall back to deriving from result_type for backward-compat cases.
730                let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
731                    options_type_name.to_string()
732                } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
733                    format!("{}Request", stripped)
734                } else {
735                    format!("{result_type_name}Request")
736                };
737                let request_type_snake = request_type_pascal.to_snake_case();
738                let var_name = format!("{request_type_snake}_handle");
739
740                let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
741                let json_val = if field.is_empty() || field == "input" {
742                    Some(&fixture.input)
743                } else {
744                    fixture.input.get(field)
745                };
746
747                if let Some(val) = json_val {
748                    if !val.is_null() {
749                        let normalized = super::normalize_json_keys_to_snake_case(val);
750                        let json_str = serde_json::to_string(&normalized).unwrap_or_default();
751                        let escaped = escape_c(&json_str);
752                        let _ = writeln!(
753                            out,
754                            "    {prefix_upper}{request_type_pascal}* {var_name} = \
755                             {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
756                        );
757                        let _ = writeln!(out, "    assert({var_name} != NULL && \"failed to build request\");");
758                        request_handle_vars.push((arg.name.clone(), var_name));
759                    }
760                }
761            } else if arg.arg_type == "string" {
762                // String arg: read fixture input, emit as a C string literal inline.
763                let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
764                let val = fixture.input.get(field);
765                match val {
766                    Some(v) if v.is_string() => {
767                        let s = v.as_str().unwrap_or_default();
768                        let escaped = escape_c(s);
769                        inline_method_args.push(format!("\"{escaped}\""));
770                    }
771                    Some(serde_json::Value::Null) | None if arg.optional => {
772                        inline_method_args.push("NULL".to_string());
773                    }
774                    None => {
775                        inline_method_args.push("\"\"".to_string());
776                    }
777                    Some(other) => {
778                        let s = serde_json::to_string(other).unwrap_or_default();
779                        let escaped = escape_c(&s);
780                        inline_method_args.push(format!("\"{escaped}\""));
781                    }
782                }
783            } else if arg.optional {
784                // Optional non-string, non-json_object arg: pass NULL.
785                inline_method_args.push("NULL".to_string());
786            }
787        }
788
789        let fixture_id = &fixture.id;
790        if fixture.needs_mock_server() {
791            let _ = writeln!(out, "    const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
792            let _ = writeln!(out, "    assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
793            let _ = writeln!(out, "    char base_url[1024];");
794            let _ = writeln!(
795                out,
796                "    snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
797            );
798            let _ = writeln!(
799                out,
800                "    {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", base_url, 0, 0, NULL);"
801            );
802        } else {
803            let _ = writeln!(
804                out,
805                "    {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, 0, 0, NULL);"
806            );
807        }
808        let _ = writeln!(out, "    assert(client != NULL && \"failed to create client\");");
809
810        let method_args = if request_handle_vars.is_empty() && inline_method_args.is_empty() && extra_args.is_empty() {
811            String::new()
812        } else {
813            let handles: Vec<String> = request_handle_vars.iter().map(|(_, v)| v.clone()).collect();
814            let parts: Vec<String> = handles
815                .into_iter()
816                .chain(inline_method_args.iter().cloned())
817                .chain(extra_args.iter().cloned())
818                .collect();
819            format!(", {}", parts.join(", "))
820        };
821
822        let call_fn = format!("{prefix}_default_client_{function_name}");
823
824        if expects_error {
825            let _ = writeln!(
826                out,
827                "    {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
828            );
829            for (_, var_name) in &request_handle_vars {
830                let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
831                let _ = writeln!(out, "    {prefix}_{req_snake}_free({var_name});");
832            }
833            let _ = writeln!(out, "    {prefix}_default_client_free(client);");
834            let _ = writeln!(out, "    assert({result_var} == NULL && \"expected call to fail\");");
835            let _ = writeln!(out, "}}");
836            return;
837        }
838
839        let _ = writeln!(
840            out,
841            "    {prefix_upper}{result_type_name}* {result_var} = {call_fn}(client{method_args});"
842        );
843        let _ = writeln!(out, "    assert({result_var} != NULL && \"expected call to succeed\");");
844
845        let mut intermediate_handles: Vec<(String, String)> = Vec::new();
846        let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
847        // Locals declared as primitive C scalars (uint64_t, double, bool, ...).
848        // Locals not present here default to char* (heap-allocated accessor result).
849        let mut primitive_locals: HashMap<String, String> = HashMap::new();
850
851        for assertion in &fixture.assertions {
852            if let Some(f) = &assertion.field {
853                if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
854                    let resolved = field_resolver.resolve(f);
855                    let local_var = f.replace(['.', '['], "_").replace(']', "");
856                    let has_map_access = resolved.contains('[');
857                    if resolved.contains('.') {
858                        let leaf_primitive = emit_nested_accessor(
859                            out,
860                            prefix,
861                            resolved,
862                            &local_var,
863                            result_var,
864                            fields_c_types,
865                            &mut intermediate_handles,
866                            result_type_name,
867                        );
868                        if let Some(prim) = leaf_primitive {
869                            primitive_locals.insert(local_var.clone(), prim);
870                        }
871                    } else {
872                        let result_type_snake = result_type_name.to_snake_case();
873                        let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
874                        let lookup_key = format!("{result_type_snake}.{resolved}");
875                        if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
876                            let _ = writeln!(out, "    {t} {local_var} = {accessor_fn}({result_var});");
877                            primitive_locals.insert(local_var.clone(), t.clone());
878                        } else {
879                            let _ = writeln!(out, "    char* {local_var} = {accessor_fn}({result_var});");
880                        }
881                    }
882                    accessed_fields.push((f.clone(), local_var, has_map_access));
883                }
884            }
885        }
886
887        for assertion in &fixture.assertions {
888            render_assertion(
889                out,
890                assertion,
891                result_var,
892                prefix,
893                field_resolver,
894                &accessed_fields,
895                &primitive_locals,
896            );
897        }
898
899        for (_f, local_var, from_json) in &accessed_fields {
900            if primitive_locals.contains_key(local_var) {
901                continue;
902            }
903            if *from_json {
904                let _ = writeln!(out, "    free({local_var});");
905            } else {
906                let _ = writeln!(out, "    {prefix}_free_string({local_var});");
907            }
908        }
909        for (handle_var, snake_type) in intermediate_handles.iter().rev() {
910            if snake_type == "free_string" {
911                let _ = writeln!(out, "    {prefix}_free_string({handle_var});");
912            } else {
913                let _ = writeln!(out, "    {prefix}_{snake_type}_free({handle_var});");
914            }
915        }
916        let result_type_snake = result_type_name.to_snake_case();
917        let _ = writeln!(out, "    {prefix}_{result_type_snake}_free({result_var});");
918        for (_, var_name) in &request_handle_vars {
919            let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
920            let _ = writeln!(out, "    {prefix}_{req_snake}_free({var_name});");
921        }
922        let _ = writeln!(out, "    {prefix}_default_client_free(client);");
923        let _ = writeln!(out, "}}");
924        return;
925    }
926
927    // Raw C result type path: functions returning a primitive C type (char*, int32_t,
928    // uintptr_t) rather than an opaque handle pointer.
929    if let Some(raw_type) = raw_c_result_type {
930        // Build argument string. Void-arg functions pass nothing.
931        let args_str = if args.is_empty() {
932            String::new()
933        } else {
934            let parts: Vec<String> = args
935                .iter()
936                .filter_map(|arg| {
937                    let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
938                    let val = fixture.input.get(field);
939                    match val {
940                        None if arg.optional => Some("NULL".to_string()),
941                        None => None,
942                        Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
943                        Some(v) => Some(json_to_c(v)),
944                    }
945                })
946                .collect();
947            parts.join(", ")
948        };
949
950        // Declare result variable.
951        let _ = writeln!(out, "    {raw_type} {result_var} = {function_name}({args_str});");
952
953        // not_error assertion.
954        let has_not_error = fixture.assertions.iter().any(|a| a.assertion_type == "not_error");
955        if has_not_error {
956            match raw_type {
957                "char*" if !result_is_option => {
958                    let _ = writeln!(out, "    assert({result_var} != NULL && \"expected call to succeed\");");
959                }
960                "int32_t" => {
961                    let _ = writeln!(out, "    assert({result_var} >= 0 && \"expected call to succeed\");");
962                }
963                "uintptr_t" => {
964                    let _ = writeln!(
965                        out,
966                        "    assert({prefix}_last_error_code() == 0 && \"expected call to succeed\");"
967                    );
968                }
969                _ => {}
970            }
971        }
972
973        // Other assertions.
974        for assertion in &fixture.assertions {
975            match assertion.assertion_type.as_str() {
976                "not_error" | "error" => {} // handled above / not applicable
977                "not_empty" => {
978                    let _ = writeln!(
979                        out,
980                        "    assert({result_var} != NULL && strlen({result_var}) > 0 && \"expected non-empty value\");"
981                    );
982                }
983                "is_empty" => {
984                    if result_is_option && raw_type == "char*" {
985                        let _ = writeln!(
986                            out,
987                            "    assert({result_var} == NULL && \"expected empty/null value\");"
988                        );
989                    } else {
990                        let _ = writeln!(
991                            out,
992                            "    assert(strlen({result_var}) == 0 && \"expected empty value\");"
993                        );
994                    }
995                }
996                "count_min" => {
997                    if let Some(val) = &assertion.value {
998                        if let Some(n) = val.as_u64() {
999                            match raw_type {
1000                                "char*" => {
1001                                    let _ = writeln!(out, "    {{");
1002                                    let _ = writeln!(
1003                                        out,
1004                                        "        assert({result_var} != NULL && \"expected non-null JSON array\");"
1005                                    );
1006                                    let _ =
1007                                        writeln!(out, "        int elem_count = alef_json_array_count({result_var});");
1008                                    let _ = writeln!(
1009                                        out,
1010                                        "        assert(elem_count >= {n} && \"expected at least {n} elements\");"
1011                                    );
1012                                    let _ = writeln!(out, "    }}");
1013                                }
1014                                _ => {
1015                                    let _ = writeln!(
1016                                        out,
1017                                        "    assert((size_t){result_var} >= {n} && \"expected at least {n} elements\");"
1018                                    );
1019                                }
1020                            }
1021                        }
1022                    }
1023                }
1024                "greater_than_or_equal" => {
1025                    if let Some(val) = &assertion.value {
1026                        let c_val = json_to_c(val);
1027                        let _ = writeln!(
1028                            out,
1029                            "    assert({result_var} >= {c_val} && \"expected greater than or equal\");"
1030                        );
1031                    }
1032                }
1033                "contains" => {
1034                    if let Some(val) = &assertion.value {
1035                        let c_val = json_to_c(val);
1036                        let _ = writeln!(
1037                            out,
1038                            "    assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
1039                        );
1040                    }
1041                }
1042                "contains_all" => {
1043                    if let Some(values) = &assertion.values {
1044                        for val in values {
1045                            let c_val = json_to_c(val);
1046                            let _ = writeln!(
1047                                out,
1048                                "    assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
1049                            );
1050                        }
1051                    }
1052                }
1053                "equals" => {
1054                    if let Some(val) = &assertion.value {
1055                        let c_val = json_to_c(val);
1056                        if val.is_string() {
1057                            let _ = writeln!(
1058                                out,
1059                                "    assert({result_var} != NULL && str_trim_eq({result_var}, {c_val}) == 0 && \"equals assertion failed\");"
1060                            );
1061                        } else {
1062                            let _ = writeln!(
1063                                out,
1064                                "    assert({result_var} == {c_val} && \"equals assertion failed\");"
1065                            );
1066                        }
1067                    }
1068                }
1069                "not_contains" => {
1070                    if let Some(val) = &assertion.value {
1071                        let c_val = json_to_c(val);
1072                        let _ = writeln!(
1073                            out,
1074                            "    assert(strstr({result_var}, {c_val}) == NULL && \"expected NOT to contain substring\");"
1075                        );
1076                    }
1077                }
1078                "starts_with" => {
1079                    if let Some(val) = &assertion.value {
1080                        let c_val = json_to_c(val);
1081                        let _ = writeln!(
1082                            out,
1083                            "    assert(strncmp({result_var}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
1084                        );
1085                    }
1086                }
1087                "is_true" => {
1088                    let _ = writeln!(out, "    assert({result_var});");
1089                }
1090                "is_false" => {
1091                    let _ = writeln!(out, "    assert(!{result_var});");
1092                }
1093                other => {
1094                    panic!("C e2e raw-result generator: unsupported assertion type: {other}");
1095                }
1096            }
1097        }
1098
1099        // Free char* results.
1100        if raw_type == "char*" {
1101            let free_fn = c_free_fn
1102                .map(|s| s.to_string())
1103                .unwrap_or_else(|| format!("{prefix}_free_string"));
1104            if result_is_option {
1105                let _ = writeln!(out, "    if ({result_var} != NULL) {{ {free_fn}({result_var}); }}");
1106            } else {
1107                let _ = writeln!(out, "    {free_fn}({result_var});");
1108            }
1109        }
1110
1111        let _ = writeln!(out, "}}");
1112        return;
1113    }
1114
1115    // Legacy (non-client) path: call the function directly.
1116    // Used for libraries like html-to-markdown that expose standalone FFI functions.
1117
1118    // Use the function name directly — the override already includes the prefix
1119    // (e.g. "htm_convert"), so we must NOT prepend it again.
1120    let prefixed_fn = function_name.to_string();
1121
1122    // For json_object args, emit a from_json call to construct the options handle.
1123    let mut has_options_handle = false;
1124    for arg in args {
1125        if arg.arg_type == "json_object" {
1126            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1127            if let Some(val) = fixture.input.get(field) {
1128                if !val.is_null() {
1129                    // Fixture keys are camelCase; the FFI htm_conversion_options_from_json
1130                    // deserializes into the Rust ConversionOptions type which uses default
1131                    // serde (snake_case). Normalize keys before serializing.
1132                    let normalized = super::normalize_json_keys_to_snake_case(val);
1133                    let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1134                    let escaped = escape_c(&json_str);
1135                    let upper = prefix.to_uppercase();
1136                    let options_type_pascal = options_type_name;
1137                    let options_type_snake = options_type_name.to_snake_case();
1138                    let _ = writeln!(
1139                        out,
1140                        "    {upper}{options_type_pascal}* options_handle = {prefix}_{options_type_snake}_from_json(\"{escaped}\");"
1141                    );
1142                    has_options_handle = true;
1143                }
1144            }
1145        }
1146    }
1147
1148    let args_str = build_args_string_c(&fixture.input, args, has_options_handle);
1149
1150    if expects_error {
1151        let _ = writeln!(
1152            out,
1153            "    {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
1154        );
1155        if has_options_handle {
1156            let options_type_snake = options_type_name.to_snake_case();
1157            let _ = writeln!(out, "    {prefix}_{options_type_snake}_free(options_handle);");
1158        }
1159        let _ = writeln!(out, "    assert({result_var} == NULL && \"expected call to fail\");");
1160        let _ = writeln!(out, "}}");
1161        return;
1162    }
1163
1164    // The FFI returns an opaque handle; extract the content string from it.
1165    let _ = writeln!(
1166        out,
1167        "    {prefix_upper}{result_type_name}* {result_var} = {prefixed_fn}({args_str});"
1168    );
1169    let _ = writeln!(out, "    assert({result_var} != NULL && \"expected call to succeed\");");
1170
1171    // Collect fields accessed by assertions so we can emit accessor calls.
1172    // C FFI uses the opaque handle pattern: {prefix}_conversion_result_{field}(handle).
1173    // For nested paths we generate chained FFI accessor calls using the type
1174    // chain from `fields_c_types`.
1175    // Each entry: (fixture_field, local_var, from_json_extract).
1176    // `from_json_extract` is true when the variable was extracted from a JSON
1177    // map via alef_json_get_string and needs free() instead of {prefix}_free_string().
1178    let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
1179    // Track intermediate handles emitted so we can free them and avoid duplicates.
1180    // Each entry: (handle_var_name, snake_type_name) — freed in reverse order.
1181    let mut intermediate_handles: Vec<(String, String)> = Vec::new();
1182    // Locals declared as primitive C scalars (uint64_t, double, bool, ...).
1183    let mut primitive_locals: HashMap<String, String> = HashMap::new();
1184
1185    for assertion in &fixture.assertions {
1186        if let Some(f) = &assertion.field {
1187            if !f.is_empty() && !accessed_fields.iter().any(|(k, _, _)| k == f) {
1188                let resolved = field_resolver.resolve(f);
1189                let local_var = f.replace(['.', '['], "_").replace(']', "");
1190                let has_map_access = resolved.contains('[');
1191
1192                if resolved.contains('.') {
1193                    let leaf_primitive = emit_nested_accessor(
1194                        out,
1195                        prefix,
1196                        resolved,
1197                        &local_var,
1198                        result_var,
1199                        fields_c_types,
1200                        &mut intermediate_handles,
1201                        result_type_name,
1202                    );
1203                    if let Some(prim) = leaf_primitive {
1204                        primitive_locals.insert(local_var.clone(), prim);
1205                    }
1206                } else {
1207                    let result_type_snake = result_type_name.to_snake_case();
1208                    let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
1209                    let lookup_key = format!("{result_type_snake}.{resolved}");
1210                    if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1211                        let _ = writeln!(out, "    {t} {local_var} = {accessor_fn}({result_var});");
1212                        primitive_locals.insert(local_var.clone(), t.clone());
1213                    } else {
1214                        let _ = writeln!(out, "    char* {local_var} = {accessor_fn}({result_var});");
1215                    }
1216                }
1217                accessed_fields.push((f.clone(), local_var.clone(), has_map_access));
1218            }
1219        }
1220    }
1221
1222    for assertion in &fixture.assertions {
1223        render_assertion(
1224            out,
1225            assertion,
1226            result_var,
1227            prefix,
1228            field_resolver,
1229            &accessed_fields,
1230            &primitive_locals,
1231        );
1232    }
1233
1234    // Free extracted leaf strings.
1235    for (_f, local_var, from_json) in &accessed_fields {
1236        if primitive_locals.contains_key(local_var) {
1237            continue;
1238        }
1239        if *from_json {
1240            let _ = writeln!(out, "    free({local_var});");
1241        } else {
1242            let _ = writeln!(out, "    {prefix}_free_string({local_var});");
1243        }
1244    }
1245    // Free intermediate handles in reverse order.
1246    for (handle_var, snake_type) in intermediate_handles.iter().rev() {
1247        if snake_type == "free_string" {
1248            // free_string handles are freed with the free_string function directly.
1249            let _ = writeln!(out, "    {prefix}_free_string({handle_var});");
1250        } else {
1251            let _ = writeln!(out, "    {prefix}_{snake_type}_free({handle_var});");
1252        }
1253    }
1254    if has_options_handle {
1255        let options_type_snake = options_type_name.to_snake_case();
1256        let _ = writeln!(out, "    {prefix}_{options_type_snake}_free(options_handle);");
1257    }
1258    let result_type_snake = result_type_name.to_snake_case();
1259    let _ = writeln!(out, "    {prefix}_{result_type_snake}_free({result_var});");
1260    let _ = writeln!(out, "}}");
1261}
1262
1263/// Emit a byte-buffer test function for FFI methods returning raw bytes via
1264/// the out-pointer pattern (e.g. `speech`, `file_content`).
1265///
1266/// FFI signature shape:
1267/// ```c
1268/// int32_t {prefix}_default_client_{fn}(
1269///     const Client *this_,
1270///     const Request *req,                /* present when args is non-empty */
1271///     uint8_t **out_ptr,
1272///     uintptr_t *out_len,
1273///     uintptr_t *out_cap);
1274/// ```
1275///
1276/// Emits:
1277/// - request handle build (same as the standard client pattern)
1278/// - `uint8_t *out_ptr = NULL; uintptr_t out_len = 0, out_cap = 0;`
1279/// - call with `&out_ptr, &out_len, &out_cap`
1280/// - status assertion: `status == 0` on success, `status != 0` on expected error
1281/// - per-assertion: `not_empty` / `not_null` collapse to `out_len > 0` because
1282///   the pseudo "audio" / "content" field is the byte buffer itself
1283/// - `{prefix}_free_bytes(out_ptr, out_len, out_cap)` after assertions
1284#[allow(clippy::too_many_arguments)]
1285fn render_bytes_test_function(
1286    out: &mut String,
1287    fixture: &Fixture,
1288    prefix: &str,
1289    function_name: &str,
1290    _result_var: &str,
1291    args: &[crate::config::ArgMapping],
1292    options_type_name: &str,
1293    result_type_name: &str,
1294    factory: &str,
1295    expects_error: bool,
1296) {
1297    let prefix_upper = prefix.to_uppercase();
1298    let mut request_handle_vars: Vec<(String, String)> = Vec::new();
1299    let mut string_arg_exprs: Vec<String> = Vec::new();
1300
1301    for arg in args {
1302        match arg.arg_type.as_str() {
1303            "json_object" => {
1304                let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
1305                    options_type_name.to_string()
1306                } else if let Some(stripped) = result_type_name.strip_suffix("Response") {
1307                    format!("{}Request", stripped)
1308                } else {
1309                    format!("{result_type_name}Request")
1310                };
1311                let request_type_snake = request_type_pascal.to_snake_case();
1312                let var_name = format!("{request_type_snake}_handle");
1313
1314                let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1315                let json_val = if field.is_empty() || field == "input" {
1316                    Some(&fixture.input)
1317                } else {
1318                    fixture.input.get(field)
1319                };
1320
1321                if let Some(val) = json_val {
1322                    if !val.is_null() {
1323                        let normalized = super::normalize_json_keys_to_snake_case(val);
1324                        let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1325                        let escaped = escape_c(&json_str);
1326                        let _ = writeln!(
1327                            out,
1328                            "    {prefix_upper}{request_type_pascal}* {var_name} = \
1329                             {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
1330                        );
1331                        let _ = writeln!(out, "    assert({var_name} != NULL && \"failed to build request\");");
1332                        request_handle_vars.push((arg.name.clone(), var_name));
1333                    }
1334                }
1335            }
1336            "string" => {
1337                // Pass string args (e.g. file_id for file_content) directly as
1338                // C string literals.
1339                let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1340                let val = fixture.input.get(field);
1341                let expr = match val {
1342                    Some(serde_json::Value::String(s)) => format!("\"{}\"", escape_c(s)),
1343                    Some(serde_json::Value::Null) | None if arg.optional => "NULL".to_string(),
1344                    Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "NULL".to_string()),
1345                    None => "NULL".to_string(),
1346                };
1347                string_arg_exprs.push(expr);
1348            }
1349            _ => {
1350                // Other arg types are not currently exercised by byte-buffer
1351                // methods; pass NULL so the call shape compiles.
1352                string_arg_exprs.push("NULL".to_string());
1353            }
1354        }
1355    }
1356
1357    let fixture_id = &fixture.id;
1358    if fixture.needs_mock_server() {
1359        let _ = writeln!(out, "    const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1360        let _ = writeln!(out, "    assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
1361        let _ = writeln!(out, "    char base_url[1024];");
1362        let _ = writeln!(
1363            out,
1364            "    snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
1365        );
1366        let _ = writeln!(
1367            out,
1368            "    {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", base_url, 0, 0, NULL);"
1369        );
1370    } else {
1371        let _ = writeln!(
1372            out,
1373            "    {prefix_upper}DefaultClient* client = {prefix}_{factory}(\"test-key\", NULL, 0, 0, NULL);"
1374        );
1375    }
1376    let _ = writeln!(out, "    assert(client != NULL && \"failed to create client\");");
1377
1378    // Out-params for the byte buffer.
1379    let _ = writeln!(out, "    uint8_t* out_ptr = NULL;");
1380    let _ = writeln!(out, "    uintptr_t out_len = 0;");
1381    let _ = writeln!(out, "    uintptr_t out_cap = 0;");
1382
1383    // Build the comma-separated argument list: handles, then string args.
1384    let mut method_args: Vec<String> = Vec::new();
1385    for (_, v) in &request_handle_vars {
1386        method_args.push(v.clone());
1387    }
1388    method_args.extend(string_arg_exprs.iter().cloned());
1389    let extra_args = if method_args.is_empty() {
1390        String::new()
1391    } else {
1392        format!(", {}", method_args.join(", "))
1393    };
1394
1395    let call_fn = format!("{prefix}_default_client_{function_name}");
1396    let _ = writeln!(
1397        out,
1398        "    int32_t status = {call_fn}(client{extra_args}, &out_ptr, &out_len, &out_cap);"
1399    );
1400
1401    if expects_error {
1402        for (_, var_name) in &request_handle_vars {
1403            let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
1404            let _ = writeln!(out, "    {prefix}_{req_snake}_free({var_name});");
1405        }
1406        let _ = writeln!(out, "    {prefix}_default_client_free(client);");
1407        let _ = writeln!(out, "    assert(status != 0 && \"expected call to fail\");");
1408        // free_bytes accepts a NULL ptr (no-op), so it is safe regardless of
1409        // whether the failed call wrote out_ptr.
1410        let _ = writeln!(out, "    {prefix}_free_bytes(out_ptr, out_len, out_cap);");
1411        let _ = writeln!(out, "}}");
1412        return;
1413    }
1414
1415    let _ = writeln!(out, "    assert(status == 0 && \"expected call to succeed\");");
1416
1417    // Render assertions. For byte-buffer methods, the only meaningful per-field
1418    // assertions are presence/length checks on the buffer itself. Field names
1419    // (e.g. "audio", "content") are pseudo-fields — collapse them all to
1420    // `out_len > 0`.
1421    let mut emitted_len_check = false;
1422    for assertion in &fixture.assertions {
1423        match assertion.assertion_type.as_str() {
1424            "not_error" => {
1425                // Already covered by the status == 0 assertion above.
1426            }
1427            "not_empty" | "not_null" => {
1428                if !emitted_len_check {
1429                    let _ = writeln!(out, "    assert(out_len > 0 && \"expected non-empty value\");");
1430                    emitted_len_check = true;
1431                }
1432            }
1433            _ => {
1434                // Other assertion shapes (equals, contains, ...) don't apply to
1435                // raw bytes; emit a comment so the test stays readable but does
1436                // not emit broken accessor calls.
1437                let _ = writeln!(
1438                    out,
1439                    "    /* skipped: assertion '{}' not meaningful on raw byte buffer */",
1440                    assertion.assertion_type
1441                );
1442            }
1443        }
1444    }
1445
1446    let _ = writeln!(out, "    {prefix}_free_bytes(out_ptr, out_len, out_cap);");
1447    for (_, var_name) in &request_handle_vars {
1448        let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
1449        let _ = writeln!(out, "    {prefix}_{req_snake}_free({var_name});");
1450    }
1451    let _ = writeln!(out, "    {prefix}_default_client_free(client);");
1452    let _ = writeln!(out, "}}");
1453}
1454
1455/// Emit a chat-stream test function that drives the FFI iterator handle.
1456///
1457/// Calls `{prefix}_default_client_chat_stream_start` to obtain an opaque handle,
1458/// loops over `{prefix}_default_client_chat_stream_next` until it returns null,
1459/// and aggregates per-chunk data into local variables (`chunks_count`,
1460/// `stream_content`, `stream_complete`, `last_choices_json`, ...). Fixture
1461/// assertions on streaming pseudo-fields (`chunks`, `stream_content`,
1462/// `stream_complete`, `no_chunks_after_done`, `finish_reason`, `tool_calls`,
1463/// `tool_calls[0].function.name`, `usage.total_tokens`) are translated to
1464/// assertions on these locals.
1465fn render_chat_stream_test_function(
1466    out: &mut String,
1467    fixture: &Fixture,
1468    prefix: &str,
1469    result_var: &str,
1470    args: &[crate::config::ArgMapping],
1471    options_type_name: &str,
1472    expects_error: bool,
1473) {
1474    let prefix_upper = prefix.to_uppercase();
1475
1476    let mut request_var: Option<String> = None;
1477    for arg in args {
1478        if arg.arg_type == "json_object" {
1479            let request_type_pascal = if !options_type_name.is_empty() && options_type_name != "ConversionOptions" {
1480                options_type_name.to_string()
1481            } else {
1482                "ChatCompletionRequest".to_string()
1483            };
1484            let request_type_snake = request_type_pascal.to_snake_case();
1485            let var_name = format!("{request_type_snake}_handle");
1486
1487            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1488            let json_val = if field.is_empty() || field == "input" {
1489                Some(&fixture.input)
1490            } else {
1491                fixture.input.get(field)
1492            };
1493
1494            if let Some(val) = json_val {
1495                if !val.is_null() {
1496                    let normalized = super::normalize_json_keys_to_snake_case(val);
1497                    let json_str = serde_json::to_string(&normalized).unwrap_or_default();
1498                    let escaped = escape_c(&json_str);
1499                    let _ = writeln!(
1500                        out,
1501                        "    {prefix_upper}{request_type_pascal}* {var_name} = \
1502                         {prefix}_{request_type_snake}_from_json(\"{escaped}\");"
1503                    );
1504                    let _ = writeln!(out, "    assert({var_name} != NULL && \"failed to build request\");");
1505                    request_var = Some(var_name);
1506                    break;
1507                }
1508            }
1509        }
1510    }
1511
1512    let req_handle = request_var.clone().unwrap_or_else(|| "NULL".to_string());
1513    let req_snake = request_var
1514        .as_ref()
1515        .and_then(|v| v.strip_suffix("_handle"))
1516        .unwrap_or("chat_completion_request")
1517        .to_string();
1518
1519    let fixture_id = &fixture.id;
1520    if fixture.needs_mock_server() {
1521        let _ = writeln!(out, "    const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
1522        let _ = writeln!(out, "    assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
1523        let _ = writeln!(out, "    char base_url[1024];");
1524        let _ = writeln!(
1525            out,
1526            "    snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
1527        );
1528        let _ = writeln!(
1529            out,
1530            "    {prefix_upper}DefaultClient* client = {prefix}_create_client(\"test-key\", base_url, 0, 0, NULL);"
1531        );
1532    } else {
1533        let _ = writeln!(
1534            out,
1535            "    {prefix_upper}DefaultClient* client = {prefix}_create_client(\"test-key\", NULL, 0, 0, NULL);"
1536        );
1537    }
1538    let _ = writeln!(out, "    assert(client != NULL && \"failed to create client\");");
1539
1540    let _ = writeln!(
1541        out,
1542        "    {prefix_upper}LiterllmDefaultClientChatStreamStreamHandle* stream_handle = \
1543         {prefix}_default_client_chat_stream_start(client, {req_handle});"
1544    );
1545
1546    if expects_error {
1547        let _ = writeln!(
1548            out,
1549            "    assert(stream_handle == NULL && \"expected stream-start to fail\");"
1550        );
1551        if request_var.is_some() {
1552            let _ = writeln!(out, "    {prefix}_{req_snake}_free({req_handle});");
1553        }
1554        let _ = writeln!(out, "    {prefix}_default_client_free(client);");
1555        let _ = writeln!(out, "}}");
1556        return;
1557    }
1558
1559    let _ = writeln!(
1560        out,
1561        "    assert(stream_handle != NULL && \"expected stream-start to succeed\");"
1562    );
1563
1564    let _ = writeln!(out, "    size_t chunks_count = 0;");
1565    let _ = writeln!(out, "    char* stream_content = (char*)malloc(1);");
1566    let _ = writeln!(out, "    assert(stream_content != NULL);");
1567    let _ = writeln!(out, "    stream_content[0] = '\\0';");
1568    let _ = writeln!(out, "    size_t stream_content_len = 0;");
1569    let _ = writeln!(out, "    int stream_complete = 0;");
1570    let _ = writeln!(out, "    int no_chunks_after_done = 1;");
1571    let _ = writeln!(out, "    char* last_choices_json = NULL;");
1572    let _ = writeln!(out, "    uint64_t total_tokens = 0;");
1573    let _ = writeln!(out);
1574
1575    let _ = writeln!(out, "    while (1) {{");
1576    let _ = writeln!(
1577        out,
1578        "        {prefix_upper}ChatCompletionChunk* {result_var} = \
1579         {prefix}_default_client_chat_stream_next(stream_handle);"
1580    );
1581    let _ = writeln!(out, "        if ({result_var} == NULL) {{");
1582    let _ = writeln!(
1583        out,
1584        "            if ({prefix}_last_error_code() == 0) {{ stream_complete = 1; }}"
1585    );
1586    let _ = writeln!(out, "            break;");
1587    let _ = writeln!(out, "        }}");
1588    let _ = writeln!(out, "        chunks_count++;");
1589    let _ = writeln!(
1590        out,
1591        "        char* choices_json = {prefix}_chat_completion_chunk_choices({result_var});"
1592    );
1593    let _ = writeln!(out, "        if (choices_json != NULL) {{");
1594    let _ = writeln!(
1595        out,
1596        "            const char* d = strstr(choices_json, \"\\\"content\\\":\");"
1597    );
1598    let _ = writeln!(out, "            if (d != NULL) {{");
1599    let _ = writeln!(out, "                d += 10;");
1600    let _ = writeln!(out, "                while (*d == ' ' || *d == '\\t') d++;");
1601    let _ = writeln!(out, "                if (*d == '\"') {{");
1602    let _ = writeln!(out, "                    d++;");
1603    let _ = writeln!(out, "                    const char* e = d;");
1604    let _ = writeln!(out, "                    while (*e && *e != '\"') {{");
1605    let _ = writeln!(
1606        out,
1607        "                        if (*e == '\\\\' && *(e+1)) e += 2; else e++;"
1608    );
1609    let _ = writeln!(out, "                    }}");
1610    let _ = writeln!(out, "                    size_t add = (size_t)(e - d);");
1611    let _ = writeln!(out, "                    if (add > 0) {{");
1612    let _ = writeln!(
1613        out,
1614        "                        char* nc = (char*)realloc(stream_content, stream_content_len + add + 1);"
1615    );
1616    let _ = writeln!(out, "                        if (nc != NULL) {{");
1617    let _ = writeln!(out, "                            stream_content = nc;");
1618    let _ = writeln!(
1619        out,
1620        "                            memcpy(stream_content + stream_content_len, d, add);"
1621    );
1622    let _ = writeln!(out, "                            stream_content_len += add;");
1623    let _ = writeln!(
1624        out,
1625        "                            stream_content[stream_content_len] = '\\0';"
1626    );
1627    let _ = writeln!(out, "                        }}");
1628    let _ = writeln!(out, "                    }}");
1629    let _ = writeln!(out, "                }}");
1630    let _ = writeln!(out, "            }}");
1631    let _ = writeln!(
1632        out,
1633        "            if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
1634    );
1635    let _ = writeln!(out, "            last_choices_json = choices_json;");
1636    let _ = writeln!(out, "        }}");
1637    let _ = writeln!(
1638        out,
1639        "        {prefix_upper}Usage* usage_handle = {prefix}_chat_completion_chunk_usage({result_var});"
1640    );
1641    let _ = writeln!(out, "        if (usage_handle != NULL) {{");
1642    let _ = writeln!(
1643        out,
1644        "            total_tokens = (uint64_t){prefix}_usage_total_tokens(usage_handle);"
1645    );
1646    let _ = writeln!(out, "            {prefix}_usage_free(usage_handle);");
1647    let _ = writeln!(out, "        }}");
1648    let _ = writeln!(out, "        {prefix}_chat_completion_chunk_free({result_var});");
1649    let _ = writeln!(out, "    }}");
1650    let _ = writeln!(out, "    {prefix}_default_client_chat_stream_free(stream_handle);");
1651    let _ = writeln!(out);
1652
1653    let _ = writeln!(out, "    char* finish_reason = NULL;");
1654    let _ = writeln!(out, "    char* tool_calls_json = NULL;");
1655    let _ = writeln!(out, "    char* tool_calls_0_function_name = NULL;");
1656    let _ = writeln!(out, "    if (last_choices_json != NULL) {{");
1657    let _ = writeln!(
1658        out,
1659        "        finish_reason = alef_json_get_string(last_choices_json, \"finish_reason\");"
1660    );
1661    let _ = writeln!(
1662        out,
1663        "        const char* tc = strstr(last_choices_json, \"\\\"tool_calls\\\":\");"
1664    );
1665    let _ = writeln!(out, "        if (tc != NULL) {{");
1666    let _ = writeln!(out, "            tc += 13;");
1667    let _ = writeln!(out, "            while (*tc == ' ' || *tc == '\\t') tc++;");
1668    let _ = writeln!(out, "            if (*tc == '[') {{");
1669    let _ = writeln!(out, "                int depth = 0;");
1670    let _ = writeln!(out, "                const char* end = tc;");
1671    let _ = writeln!(out, "                int in_str = 0;");
1672    let _ = writeln!(out, "                for (; *end; end++) {{");
1673    let _ = writeln!(
1674        out,
1675        "                    if (*end == '\\\\' && in_str) {{ if (*(end+1)) end++; continue; }}"
1676    );
1677    let _ = writeln!(
1678        out,
1679        "                    if (*end == '\"') {{ in_str = !in_str; continue; }}"
1680    );
1681    let _ = writeln!(out, "                    if (in_str) continue;");
1682    let _ = writeln!(out, "                    if (*end == '[' || *end == '{{') depth++;");
1683    let _ = writeln!(
1684        out,
1685        "                    else if (*end == ']' || *end == '}}') {{ depth--; if (depth == 0) {{ end++; break; }} }}"
1686    );
1687    let _ = writeln!(out, "                }}");
1688    let _ = writeln!(out, "                size_t tlen = (size_t)(end - tc);");
1689    let _ = writeln!(out, "                tool_calls_json = (char*)malloc(tlen + 1);");
1690    let _ = writeln!(out, "                if (tool_calls_json != NULL) {{");
1691    let _ = writeln!(out, "                    memcpy(tool_calls_json, tc, tlen);");
1692    let _ = writeln!(out, "                    tool_calls_json[tlen] = '\\0';");
1693    let _ = writeln!(
1694        out,
1695        "                    const char* fn = strstr(tool_calls_json, \"\\\"function\\\"\");"
1696    );
1697    let _ = writeln!(out, "                    if (fn != NULL) {{");
1698    let _ = writeln!(
1699        out,
1700        "                        const char* np = strstr(fn, \"\\\"name\\\":\");"
1701    );
1702    let _ = writeln!(out, "                        if (np != NULL) {{");
1703    let _ = writeln!(out, "                            np += 7;");
1704    let _ = writeln!(
1705        out,
1706        "                            while (*np == ' ' || *np == '\\t') np++;"
1707    );
1708    let _ = writeln!(out, "                            if (*np == '\"') {{");
1709    let _ = writeln!(out, "                                np++;");
1710    let _ = writeln!(out, "                                const char* ne = np;");
1711    let _ = writeln!(
1712        out,
1713        "                                while (*ne && *ne != '\"') {{ if (*ne == '\\\\' && *(ne+1)) ne += 2; else ne++; }}"
1714    );
1715    let _ = writeln!(out, "                                size_t nlen = (size_t)(ne - np);");
1716    let _ = writeln!(
1717        out,
1718        "                                tool_calls_0_function_name = (char*)malloc(nlen + 1);"
1719    );
1720    let _ = writeln!(
1721        out,
1722        "                                if (tool_calls_0_function_name != NULL) {{"
1723    );
1724    let _ = writeln!(
1725        out,
1726        "                                    memcpy(tool_calls_0_function_name, np, nlen);"
1727    );
1728    let _ = writeln!(
1729        out,
1730        "                                    tool_calls_0_function_name[nlen] = '\\0';"
1731    );
1732    let _ = writeln!(out, "                                }}");
1733    let _ = writeln!(out, "                            }}");
1734    let _ = writeln!(out, "                        }}");
1735    let _ = writeln!(out, "                    }}");
1736    let _ = writeln!(out, "                }}");
1737    let _ = writeln!(out, "            }}");
1738    let _ = writeln!(out, "        }}");
1739    let _ = writeln!(out, "    }}");
1740    let _ = writeln!(out);
1741
1742    for assertion in &fixture.assertions {
1743        emit_chat_stream_assertion(out, assertion);
1744    }
1745
1746    let _ = writeln!(out, "    free(stream_content);");
1747    let _ = writeln!(
1748        out,
1749        "    if (last_choices_json != NULL) {prefix}_free_string(last_choices_json);"
1750    );
1751    let _ = writeln!(out, "    if (finish_reason != NULL) free(finish_reason);");
1752    let _ = writeln!(out, "    if (tool_calls_json != NULL) free(tool_calls_json);");
1753    let _ = writeln!(
1754        out,
1755        "    if (tool_calls_0_function_name != NULL) free(tool_calls_0_function_name);"
1756    );
1757    if request_var.is_some() {
1758        let _ = writeln!(out, "    {prefix}_{req_snake}_free({req_handle});");
1759    }
1760    let _ = writeln!(out, "    {prefix}_default_client_free(client);");
1761    let _ = writeln!(
1762        out,
1763        "    /* suppress unused */ (void)total_tokens; (void)no_chunks_after_done; \
1764         (void)stream_complete; (void)chunks_count; (void)stream_content_len;"
1765    );
1766    let _ = writeln!(out, "}}");
1767}
1768
1769/// Emit a single fixture assertion for a chat-stream test, mapping fixture
1770/// pseudo-field references (`chunks`, `stream_content`, `stream_complete`, ...)
1771/// to the local aggregator variables built by [`render_chat_stream_test_function`].
1772fn emit_chat_stream_assertion(out: &mut String, assertion: &Assertion) {
1773    let field = assertion.field.as_deref().unwrap_or("");
1774
1775    enum Kind {
1776        IntCount,
1777        Bool,
1778        Str,
1779        IntTokens,
1780        Unsupported,
1781    }
1782
1783    let (expr, kind) = match field {
1784        "chunks" => ("chunks_count", Kind::IntCount),
1785        "stream_content" => ("stream_content", Kind::Str),
1786        "stream_complete" => ("stream_complete", Kind::Bool),
1787        "no_chunks_after_done" => ("no_chunks_after_done", Kind::Bool),
1788        "finish_reason" => ("finish_reason", Kind::Str),
1789        "tool_calls" => ("tool_calls_json", Kind::Str),
1790        "tool_calls[0].function.name" => ("tool_calls_0_function_name", Kind::Str),
1791        "usage.total_tokens" => ("total_tokens", Kind::IntTokens),
1792        _ => ("", Kind::Unsupported),
1793    };
1794
1795    let atype = assertion.assertion_type.as_str();
1796    if atype == "not_error" || atype == "error" {
1797        return;
1798    }
1799
1800    if matches!(kind, Kind::Unsupported) {
1801        let _ = writeln!(
1802            out,
1803            "    /* skipped: streaming assertion on unsupported field '{field}' */"
1804        );
1805        return;
1806    }
1807
1808    match (atype, &kind) {
1809        ("count_min", Kind::IntCount) => {
1810            if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1811                let _ = writeln!(out, "    assert({expr} >= {n} && \"expected at least {n} chunks\");");
1812            }
1813        }
1814        ("equals", Kind::Str) => {
1815            if let Some(val) = &assertion.value {
1816                let c_val = json_to_c(val);
1817                let _ = writeln!(
1818                    out,
1819                    "    assert({expr} != NULL && str_trim_eq({expr}, {c_val}) == 0 && \"streaming equals assertion failed\");"
1820                );
1821            }
1822        }
1823        ("contains", Kind::Str) => {
1824            if let Some(val) = &assertion.value {
1825                let c_val = json_to_c(val);
1826                let _ = writeln!(
1827                    out,
1828                    "    assert({expr} != NULL && strstr({expr}, {c_val}) != NULL && \"streaming contains assertion failed\");"
1829                );
1830            }
1831        }
1832        ("not_empty", Kind::Str) => {
1833            let _ = writeln!(
1834                out,
1835                "    assert({expr} != NULL && strlen({expr}) > 0 && \"expected non-empty {field}\");"
1836            );
1837        }
1838        ("is_true", Kind::Bool) => {
1839            let _ = writeln!(out, "    assert({expr} && \"expected {field} to be true\");");
1840        }
1841        ("is_false", Kind::Bool) => {
1842            let _ = writeln!(out, "    assert(!{expr} && \"expected {field} to be false\");");
1843        }
1844        ("greater_than_or_equal", Kind::IntCount) | ("greater_than_or_equal", Kind::IntTokens) => {
1845            if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1846                let _ = writeln!(out, "    assert({expr} >= {n} && \"expected {expr} >= {n}\");");
1847            }
1848        }
1849        ("equals", Kind::IntCount) | ("equals", Kind::IntTokens) => {
1850            if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1851                let _ = writeln!(out, "    assert({expr} == {n} && \"equals assertion failed\");");
1852            }
1853        }
1854        _ => {
1855            let _ = writeln!(
1856                out,
1857                "    /* skipped: streaming assertion '{atype}' on field '{field}' not supported */"
1858            );
1859        }
1860    }
1861}
1862
1863/// Emit chained FFI accessor calls for a nested resolved field path.
1864///
1865/// For a path like `metadata.document.title`, this generates:
1866/// ```c
1867/// HTMHtmlMetadata* metadata_handle = htm_conversion_result_metadata(result);
1868/// assert(metadata_handle != NULL);
1869/// HTMDocumentMetadata* doc_handle = htm_html_metadata_document(metadata_handle);
1870/// assert(doc_handle != NULL);
1871/// char* metadata_title = htm_document_metadata_title(doc_handle);
1872/// ```
1873///
1874/// The type chain is looked up from `fields_c_types` which maps
1875/// `"{parent_snake_type}.{field}"` -> `"PascalCaseType"`.
1876#[allow(clippy::too_many_arguments)]
1877fn emit_nested_accessor(
1878    out: &mut String,
1879    prefix: &str,
1880    resolved: &str,
1881    local_var: &str,
1882    result_var: &str,
1883    fields_c_types: &HashMap<String, String>,
1884    intermediate_handles: &mut Vec<(String, String)>,
1885    result_type_name: &str,
1886) -> Option<String> {
1887    let segments: Vec<&str> = resolved.split('.').collect();
1888    let prefix_upper = prefix.to_uppercase();
1889
1890    // Walk the path, starting from the root result type.
1891    let mut current_snake_type = result_type_name.to_snake_case();
1892    let mut current_handle = result_var.to_string();
1893
1894    for (i, segment) in segments.iter().enumerate() {
1895        let is_leaf = i + 1 == segments.len();
1896
1897        // Check for map access: "field[key]"
1898        if let Some(bracket_pos) = segment.find('[') {
1899            let field_name = &segment[..bracket_pos];
1900            let key = segment[bracket_pos + 1..].trim_end_matches(']');
1901            let field_snake = field_name.to_snake_case();
1902            let accessor_fn = format!("{prefix}_{current_snake_type}_{field_snake}");
1903
1904            // The map accessor returns a char* (JSON object string).
1905            // Use alef_json_get_string to extract the key value.
1906            let json_var = format!("{field_snake}_json");
1907            if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
1908                let _ = writeln!(out, "    char* {json_var} = {accessor_fn}({current_handle});");
1909                let _ = writeln!(out, "    assert({json_var} != NULL);");
1910                // Track for freeing — use prefix_free_string since it's a char*.
1911                intermediate_handles.push((json_var.clone(), "free_string".to_string()));
1912            }
1913            // Extract the key from the JSON map.
1914            let _ = writeln!(
1915                out,
1916                "    char* {local_var} = alef_json_get_string({json_var}, \"{key}\");"
1917            );
1918            return None; // Map access leaf — char*.
1919        }
1920
1921        let seg_snake = segment.to_snake_case();
1922        let accessor_fn = format!("{prefix}_{current_snake_type}_{seg_snake}");
1923
1924        if is_leaf {
1925            // Leaf may be a primitive scalar (uint64_t, double, ...) when
1926            // configured in `fields_c_types`. Otherwise default to char*.
1927            let lookup_key = format!("{current_snake_type}.{seg_snake}");
1928            if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
1929                let _ = writeln!(out, "    {t} {local_var} = {accessor_fn}({current_handle});");
1930                return Some(t.clone());
1931            }
1932            let _ = writeln!(out, "    char* {local_var} = {accessor_fn}({current_handle});");
1933        } else {
1934            // Intermediate field returns an opaque handle.
1935            let lookup_key = format!("{current_snake_type}.{seg_snake}");
1936            let return_type_pascal = match fields_c_types.get(&lookup_key) {
1937                Some(t) => t.clone(),
1938                None => {
1939                    // Fallback: derive PascalCase from the segment name itself.
1940                    segment.to_pascal_case()
1941                }
1942            };
1943            let return_snake = return_type_pascal.to_snake_case();
1944            let handle_var = format!("{seg_snake}_handle");
1945
1946            // Only emit the handle if we haven't already (multiple fields may
1947            // share the same intermediate path prefix).
1948            if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
1949                let _ = writeln!(
1950                    out,
1951                    "    {prefix_upper}{return_type_pascal}* {handle_var} = \
1952                     {accessor_fn}({current_handle});"
1953                );
1954                let _ = writeln!(out, "    assert({handle_var} != NULL);");
1955                intermediate_handles.push((handle_var.clone(), return_snake.clone()));
1956            }
1957
1958            current_snake_type = return_snake;
1959            current_handle = handle_var;
1960        }
1961    }
1962    None
1963}
1964
1965/// Build the C argument string for the function call.
1966/// When `has_options_handle` is true, json_object args are replaced with
1967/// the `options_handle` pointer (which was constructed via `from_json`).
1968fn build_args_string_c(
1969    input: &serde_json::Value,
1970    args: &[crate::config::ArgMapping],
1971    has_options_handle: bool,
1972) -> String {
1973    if args.is_empty() {
1974        return json_to_c(input);
1975    }
1976
1977    let parts: Vec<String> = args
1978        .iter()
1979        .filter_map(|arg| {
1980            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1981            let val = input.get(field);
1982            match val {
1983                // Field missing entirely and optional → pass NULL.
1984                None if arg.optional => Some("NULL".to_string()),
1985                // Field missing and required → skip (caller error, but don't crash).
1986                None => None,
1987                // Explicit null on optional arg → pass NULL.
1988                Some(v) if v.is_null() && arg.optional => Some("NULL".to_string()),
1989                Some(v) => {
1990                    // For json_object args, use the options_handle pointer
1991                    // instead of the raw JSON string.
1992                    if arg.arg_type == "json_object" && has_options_handle && !v.is_null() {
1993                        Some("options_handle".to_string())
1994                    } else {
1995                        Some(json_to_c(v))
1996                    }
1997                }
1998            }
1999        })
2000        .collect();
2001
2002    parts.join(", ")
2003}
2004
2005fn render_assertion(
2006    out: &mut String,
2007    assertion: &Assertion,
2008    result_var: &str,
2009    ffi_prefix: &str,
2010    _field_resolver: &FieldResolver,
2011    accessed_fields: &[(String, String, bool)],
2012    primitive_locals: &HashMap<String, String>,
2013) {
2014    // Skip assertions on fields that don't exist on the result type.
2015    if let Some(f) = &assertion.field {
2016        if !f.is_empty() && !_field_resolver.is_valid_for_result(f) {
2017            let _ = writeln!(out, "    // skipped: field '{f}' not available on result type");
2018            return;
2019        }
2020    }
2021
2022    let field_expr = match &assertion.field {
2023        Some(f) if !f.is_empty() => {
2024            // Use the local variable extracted from the opaque handle.
2025            accessed_fields
2026                .iter()
2027                .find(|(k, _, _)| k == f)
2028                .map(|(_, local, _)| local.clone())
2029                .unwrap_or_else(|| result_var.to_string())
2030        }
2031        _ => result_var.to_string(),
2032    };
2033
2034    let field_is_primitive = primitive_locals.contains_key(&field_expr);
2035    let field_primitive_type = primitive_locals.get(&field_expr).cloned();
2036    // Map-access fields are extracted via `alef_json_get_string` and end up
2037    // as char*. When the assertion expects a numeric or boolean value, we
2038    // emit a parsed/literal comparison rather than `strcmp`.
2039    let field_is_map_access = if let Some(f) = &assertion.field {
2040        accessed_fields.iter().any(|(k, _, m)| k == f && *m)
2041    } else {
2042        false
2043    };
2044
2045    match assertion.assertion_type.as_str() {
2046        "equals" => {
2047            if let Some(expected) = &assertion.value {
2048                let c_val = json_to_c(expected);
2049                if field_is_primitive {
2050                    let cmp_val = if field_primitive_type.as_deref() == Some("bool") {
2051                        match expected.as_bool() {
2052                            Some(true) => "1".to_string(),
2053                            Some(false) => "0".to_string(),
2054                            None => c_val,
2055                        }
2056                    } else {
2057                        c_val
2058                    };
2059                    let _ = writeln!(
2060                        out,
2061                        "    assert({field_expr} == {cmp_val} && \"equals assertion failed\");"
2062                    );
2063                } else if expected.is_string() {
2064                    let _ = writeln!(
2065                        out,
2066                        "    assert(str_trim_eq({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
2067                    );
2068                } else if field_is_map_access && expected.is_boolean() {
2069                    let lit = match expected.as_bool() {
2070                        Some(true) => "\"true\"",
2071                        _ => "\"false\"",
2072                    };
2073                    let _ = writeln!(
2074                        out,
2075                        "    assert({field_expr} != NULL && strcmp({field_expr}, {lit}) == 0 && \"equals assertion failed\");"
2076                    );
2077                } else if field_is_map_access && expected.is_number() {
2078                    if expected.is_f64() {
2079                        let _ = writeln!(
2080                            out,
2081                            "    assert({field_expr} != NULL && atof({field_expr}) == {c_val} && \"equals assertion failed\");"
2082                        );
2083                    } else {
2084                        let _ = writeln!(
2085                            out,
2086                            "    assert({field_expr} != NULL && atoll({field_expr}) == {c_val} && \"equals assertion failed\");"
2087                        );
2088                    }
2089                } else {
2090                    let _ = writeln!(
2091                        out,
2092                        "    assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
2093                    );
2094                }
2095            }
2096        }
2097        "contains" => {
2098            if let Some(expected) = &assertion.value {
2099                let c_val = json_to_c(expected);
2100                let _ = writeln!(
2101                    out,
2102                    "    assert(strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
2103                );
2104            }
2105        }
2106        "contains_all" => {
2107            if let Some(values) = &assertion.values {
2108                for val in values {
2109                    let c_val = json_to_c(val);
2110                    let _ = writeln!(
2111                        out,
2112                        "    assert(strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
2113                    );
2114                }
2115            }
2116        }
2117        "not_contains" => {
2118            if let Some(expected) = &assertion.value {
2119                let c_val = json_to_c(expected);
2120                let _ = writeln!(
2121                    out,
2122                    "    assert(strstr({field_expr}, {c_val}) == NULL && \"expected NOT to contain substring\");"
2123                );
2124            }
2125        }
2126        "not_empty" => {
2127            let _ = writeln!(
2128                out,
2129                "    assert({field_expr} != NULL && strlen({field_expr}) > 0 && \"expected non-empty value\");"
2130            );
2131        }
2132        "is_empty" => {
2133            let _ = writeln!(
2134                out,
2135                "    assert(strlen({field_expr}) == 0 && \"expected empty value\");"
2136            );
2137        }
2138        "contains_any" => {
2139            if let Some(values) = &assertion.values {
2140                let _ = writeln!(out, "    {{");
2141                let _ = writeln!(out, "        int found = 0;");
2142                for val in values {
2143                    let c_val = json_to_c(val);
2144                    let _ = writeln!(
2145                        out,
2146                        "        if (strstr({field_expr}, {c_val}) != NULL) {{ found = 1; }}"
2147                    );
2148                }
2149                let _ = writeln!(
2150                    out,
2151                    "        assert(found && \"expected to contain at least one of the specified values\");"
2152                );
2153                let _ = writeln!(out, "    }}");
2154            }
2155        }
2156        "greater_than" => {
2157            if let Some(val) = &assertion.value {
2158                let c_val = json_to_c(val);
2159                if field_is_map_access && val.is_number() && !field_is_primitive {
2160                    let _ = writeln!(
2161                        out,
2162                        "    assert({field_expr} != NULL && atof({field_expr}) > {c_val} && \"expected greater than\");"
2163                    );
2164                } else {
2165                    let _ = writeln!(out, "    assert({field_expr} > {c_val} && \"expected greater than\");");
2166                }
2167            }
2168        }
2169        "less_than" => {
2170            if let Some(val) = &assertion.value {
2171                let c_val = json_to_c(val);
2172                if field_is_map_access && val.is_number() && !field_is_primitive {
2173                    let _ = writeln!(
2174                        out,
2175                        "    assert({field_expr} != NULL && atof({field_expr}) < {c_val} && \"expected less than\");"
2176                    );
2177                } else {
2178                    let _ = writeln!(out, "    assert({field_expr} < {c_val} && \"expected less than\");");
2179                }
2180            }
2181        }
2182        "greater_than_or_equal" => {
2183            if let Some(val) = &assertion.value {
2184                let c_val = json_to_c(val);
2185                if field_is_map_access && val.is_number() && !field_is_primitive {
2186                    let _ = writeln!(
2187                        out,
2188                        "    assert({field_expr} != NULL && atof({field_expr}) >= {c_val} && \"expected greater than or equal\");"
2189                    );
2190                } else {
2191                    let _ = writeln!(
2192                        out,
2193                        "    assert({field_expr} >= {c_val} && \"expected greater than or equal\");"
2194                    );
2195                }
2196            }
2197        }
2198        "less_than_or_equal" => {
2199            if let Some(val) = &assertion.value {
2200                let c_val = json_to_c(val);
2201                if field_is_map_access && val.is_number() && !field_is_primitive {
2202                    let _ = writeln!(
2203                        out,
2204                        "    assert({field_expr} != NULL && atof({field_expr}) <= {c_val} && \"expected less than or equal\");"
2205                    );
2206                } else {
2207                    let _ = writeln!(
2208                        out,
2209                        "    assert({field_expr} <= {c_val} && \"expected less than or equal\");"
2210                    );
2211                }
2212            }
2213        }
2214        "starts_with" => {
2215            if let Some(expected) = &assertion.value {
2216                let c_val = json_to_c(expected);
2217                let _ = writeln!(
2218                    out,
2219                    "    assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
2220                );
2221            }
2222        }
2223        "ends_with" => {
2224            if let Some(expected) = &assertion.value {
2225                let c_val = json_to_c(expected);
2226                let _ = writeln!(out, "    assert(strlen({field_expr}) >= strlen({c_val}) && ");
2227                let _ = writeln!(
2228                    out,
2229                    "           strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
2230                );
2231            }
2232        }
2233        "min_length" => {
2234            if let Some(val) = &assertion.value {
2235                if let Some(n) = val.as_u64() {
2236                    let _ = writeln!(
2237                        out,
2238                        "    assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
2239                    );
2240                }
2241            }
2242        }
2243        "max_length" => {
2244            if let Some(val) = &assertion.value {
2245                if let Some(n) = val.as_u64() {
2246                    let _ = writeln!(
2247                        out,
2248                        "    assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
2249                    );
2250                }
2251            }
2252        }
2253        "count_min" => {
2254            if let Some(val) = &assertion.value {
2255                if let Some(n) = val.as_u64() {
2256                    let _ = writeln!(out, "    {{");
2257                    let _ = writeln!(out, "        /* count_min: count top-level JSON array elements */");
2258                    let _ = writeln!(
2259                        out,
2260                        "        assert({field_expr} != NULL && \"expected non-null collection JSON\");"
2261                    );
2262                    let _ = writeln!(out, "        int elem_count = alef_json_array_count({field_expr});");
2263                    let _ = writeln!(
2264                        out,
2265                        "        assert(elem_count >= {n} && \"expected at least {n} elements\");"
2266                    );
2267                    let _ = writeln!(out, "    }}");
2268                }
2269            }
2270        }
2271        "count_equals" => {
2272            if let Some(val) = &assertion.value {
2273                if let Some(n) = val.as_u64() {
2274                    let _ = writeln!(out, "    {{");
2275                    let _ = writeln!(out, "        /* count_equals: count elements in array */");
2276                    let _ = writeln!(
2277                        out,
2278                        "        assert({field_expr} != NULL && \"expected non-null collection JSON\");"
2279                    );
2280                    let _ = writeln!(out, "        int elem_count = alef_json_array_count({field_expr});");
2281                    let _ = writeln!(out, "        assert(elem_count == {n} && \"expected {n} elements\");");
2282                    let _ = writeln!(out, "    }}");
2283                }
2284            }
2285        }
2286        "is_true" => {
2287            let _ = writeln!(out, "    assert({field_expr});");
2288        }
2289        "is_false" => {
2290            let _ = writeln!(out, "    assert(!{field_expr});");
2291        }
2292        "method_result" => {
2293            if let Some(method_name) = &assertion.method {
2294                render_method_result_assertion(
2295                    out,
2296                    result_var,
2297                    ffi_prefix,
2298                    method_name,
2299                    assertion.args.as_ref(),
2300                    assertion.return_type.as_deref(),
2301                    assertion.check.as_deref().unwrap_or("is_true"),
2302                    assertion.value.as_ref(),
2303                );
2304            } else {
2305                panic!("C e2e generator: method_result assertion missing 'method' field");
2306            }
2307        }
2308        "matches_regex" => {
2309            if let Some(expected) = &assertion.value {
2310                let c_val = json_to_c(expected);
2311                let _ = writeln!(out, "    {{");
2312                let _ = writeln!(out, "        regex_t _re;");
2313                let _ = writeln!(
2314                    out,
2315                    "        assert(regcomp(&_re, {c_val}, REG_EXTENDED) == 0 && \"regex compile failed\");"
2316                );
2317                let _ = writeln!(
2318                    out,
2319                    "        assert(regexec(&_re, {field_expr}, 0, NULL, 0) == 0 && \"expected value to match regex\");"
2320                );
2321                let _ = writeln!(out, "        regfree(&_re);");
2322                let _ = writeln!(out, "    }}");
2323            }
2324        }
2325        "not_error" => {
2326            // Already handled — the NULL check above covers this.
2327        }
2328        "error" => {
2329            // Handled at the test function level.
2330        }
2331        other => {
2332            panic!("C e2e generator: unsupported assertion type: {other}");
2333        }
2334    }
2335}
2336
2337/// Render a `method_result` assertion in C.
2338///
2339/// Dispatches generically using `{ffi_prefix}_{method_name}` for the FFI call.
2340/// The `return_type` fixture field controls how the return value is handled:
2341/// - `"string"` — the method returns a heap-allocated `char*`; the generator
2342///   emits a scoped block that asserts, then calls `free()`.
2343/// - absent/other — treated as a primitive integer (or pointer-as-bool); the
2344///   assertion is emitted inline without any heap management.
2345#[allow(clippy::too_many_arguments)]
2346fn render_method_result_assertion(
2347    out: &mut String,
2348    result_var: &str,
2349    ffi_prefix: &str,
2350    method_name: &str,
2351    args: Option<&serde_json::Value>,
2352    return_type: Option<&str>,
2353    check: &str,
2354    value: Option<&serde_json::Value>,
2355) {
2356    let call_expr = build_c_method_call(result_var, ffi_prefix, method_name, args);
2357
2358    if return_type == Some("string") {
2359        // Heap-allocated char* return: emit a scoped block, assert, then free.
2360        let _ = writeln!(out, "    {{");
2361        let _ = writeln!(out, "        char* _method_result = {call_expr};");
2362        if check == "is_error" {
2363            let _ = writeln!(
2364                out,
2365                "        assert(_method_result == NULL && \"expected method to return error\");"
2366            );
2367            let _ = writeln!(out, "    }}");
2368            return;
2369        }
2370        let _ = writeln!(
2371            out,
2372            "        assert(_method_result != NULL && \"method_result returned NULL\");"
2373        );
2374        match check {
2375            "contains" => {
2376                if let Some(val) = value {
2377                    let c_val = json_to_c(val);
2378                    let _ = writeln!(
2379                        out,
2380                        "        assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
2381                    );
2382                }
2383            }
2384            "equals" => {
2385                if let Some(val) = value {
2386                    let c_val = json_to_c(val);
2387                    let _ = writeln!(
2388                        out,
2389                        "        assert(str_trim_eq(_method_result, {c_val}) == 0 && \"method_result equals assertion failed\");"
2390                    );
2391                }
2392            }
2393            "is_true" => {
2394                let _ = writeln!(
2395                    out,
2396                    "        assert(_method_result != NULL && strlen(_method_result) > 0 && \"method_result is_true assertion failed\");"
2397                );
2398            }
2399            "count_min" => {
2400                if let Some(val) = value {
2401                    let n = val.as_u64().unwrap_or(0);
2402                    let _ = writeln!(out, "        int _elem_count = alef_json_array_count(_method_result);");
2403                    let _ = writeln!(
2404                        out,
2405                        "        assert(_elem_count >= {n} && \"method_result count_min assertion failed\");"
2406                    );
2407                }
2408            }
2409            other_check => {
2410                panic!("C e2e generator: unsupported method_result check type for string return: {other_check}");
2411            }
2412        }
2413        let _ = writeln!(out, "        free(_method_result);");
2414        let _ = writeln!(out, "    }}");
2415        return;
2416    }
2417
2418    // Primitive (integer / pointer-as-bool) return: inline assert, no heap management.
2419    match check {
2420        "equals" => {
2421            if let Some(val) = value {
2422                let c_val = json_to_c(val);
2423                let _ = writeln!(
2424                    out,
2425                    "    assert({call_expr} == {c_val} && \"method_result equals assertion failed\");"
2426                );
2427            }
2428        }
2429        "is_true" => {
2430            let _ = writeln!(
2431                out,
2432                "    assert({call_expr} && \"method_result is_true assertion failed\");"
2433            );
2434        }
2435        "is_false" => {
2436            let _ = writeln!(
2437                out,
2438                "    assert(!{call_expr} && \"method_result is_false assertion failed\");"
2439            );
2440        }
2441        "greater_than_or_equal" => {
2442            if let Some(val) = value {
2443                let n = val.as_u64().unwrap_or(0);
2444                let _ = writeln!(
2445                    out,
2446                    "    assert({call_expr} >= {n} && \"method_result >= {n} assertion failed\");"
2447                );
2448            }
2449        }
2450        "count_min" => {
2451            if let Some(val) = value {
2452                let n = val.as_u64().unwrap_or(0);
2453                let _ = writeln!(
2454                    out,
2455                    "    assert({call_expr} >= {n} && \"method_result count_min assertion failed\");"
2456                );
2457            }
2458        }
2459        other_check => {
2460            panic!("C e2e generator: unsupported method_result check type: {other_check}");
2461        }
2462    }
2463}
2464
2465/// Build a C call expression for a `method_result` assertion.
2466///
2467/// Uses generic dispatch: `{ffi_prefix}_{method_name}(result_var, args...)`.
2468/// Args from the fixture JSON object are emitted as positional C arguments in
2469/// insertion order, using best-effort type conversion (strings → C string literals,
2470/// numbers and booleans → verbatim literals).
2471fn build_c_method_call(
2472    result_var: &str,
2473    ffi_prefix: &str,
2474    method_name: &str,
2475    args: Option<&serde_json::Value>,
2476) -> String {
2477    let extra_args = if let Some(args_val) = args {
2478        args_val
2479            .as_object()
2480            .map(|obj| {
2481                obj.values()
2482                    .map(|v| match v {
2483                        serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
2484                        serde_json::Value::Bool(true) => "1".to_string(),
2485                        serde_json::Value::Bool(false) => "0".to_string(),
2486                        serde_json::Value::Number(n) => n.to_string(),
2487                        serde_json::Value::Null => "NULL".to_string(),
2488                        other => format!("\"{}\"", escape_c(&other.to_string())),
2489                    })
2490                    .collect::<Vec<_>>()
2491                    .join(", ")
2492            })
2493            .unwrap_or_default()
2494    } else {
2495        String::new()
2496    };
2497
2498    if extra_args.is_empty() {
2499        format!("{ffi_prefix}_{method_name}({result_var})")
2500    } else {
2501        format!("{ffi_prefix}_{method_name}({result_var}, {extra_args})")
2502    }
2503}
2504
2505/// Convert a `serde_json::Value` to a C literal string.
2506fn json_to_c(value: &serde_json::Value) -> String {
2507    match value {
2508        serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
2509        serde_json::Value::Bool(true) => "1".to_string(),
2510        serde_json::Value::Bool(false) => "0".to_string(),
2511        serde_json::Value::Number(n) => n.to_string(),
2512        serde_json::Value::Null => "NULL".to_string(),
2513        other => format!("\"{}\"", escape_c(&other.to_string())),
2514    }
2515}