Skip to main content

alef_e2e/codegen/
c.rs

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