Skip to main content

alef_e2e/codegen/
c.rs

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