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