Skip to main content

alef_e2e/codegen/
go.rs

1//! Go e2e test generator using testing.T.
2
3use crate::config::E2eConfig;
4use crate::escape::{go_string_literal, sanitize_filename};
5use crate::field_access::FieldResolver;
6use crate::fixture::{Assertion, CallbackAction, Fixture, FixtureGroup, ValidationErrorExpectation};
7use alef_codegen::naming::{go_param_name, to_go_name};
8use alef_core::backend::GeneratedFile;
9use alef_core::config::Language;
10use alef_core::config::ResolvedCrateConfig;
11use alef_core::hash::{self, CommentStyle};
12use anyhow::Result;
13use heck::ToUpperCamelCase;
14use std::fmt::Write as FmtWrite;
15use std::path::PathBuf;
16
17use super::E2eCodegen;
18use super::client;
19
20/// Go e2e code generator.
21pub struct GoCodegen;
22
23impl E2eCodegen for GoCodegen {
24    fn generate(
25        &self,
26        groups: &[FixtureGroup],
27        e2e_config: &E2eConfig,
28        config: &ResolvedCrateConfig,
29        _type_defs: &[alef_core::ir::TypeDef],
30        enums: &[alef_core::ir::EnumDef],
31    ) -> Result<Vec<GeneratedFile>> {
32        let lang = self.language_name();
33        let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
34
35        let mut files = Vec::new();
36
37        // Identify data-enum (sum-type) names: enums where at least one variant has named fields.
38        // These require special unmarshaling in Go (via Unmarshal<Type> discriminator).
39        let data_enum_names: std::collections::HashSet<&str> = enums
40            .iter()
41            .filter(|e| {
42                e.variants
43                    .iter()
44                    .any(|v| !v.fields.is_empty() && v.fields.iter().any(|f| !f.name.is_empty()))
45            })
46            .map(|e| e.name.as_str())
47            .collect();
48
49        // Resolve call config with overrides (for module path and import alias).
50        let call = &e2e_config.call;
51        let overrides = call.overrides.get(lang);
52        let configured_go_module_path = config.go.as_ref().and_then(|go| go.module.as_ref()).cloned();
53        let module_path = overrides
54            .and_then(|o| o.module.as_ref())
55            .cloned()
56            .or_else(|| configured_go_module_path.clone())
57            .unwrap_or_else(|| call.module.clone());
58        let import_alias = overrides
59            .and_then(|o| o.alias.as_ref())
60            .cloned()
61            .unwrap_or_else(|| "pkg".to_string());
62
63        // Resolve package config.
64        let go_pkg = e2e_config.resolve_package("go");
65        let go_module_path = go_pkg
66            .as_ref()
67            .and_then(|p| p.module.as_ref())
68            .cloned()
69            .or_else(|| configured_go_module_path.clone())
70            .unwrap_or_else(|| module_path.clone());
71        let replace_path = go_pkg
72            .as_ref()
73            .and_then(|p| p.path.as_ref())
74            .cloned()
75            .or_else(|| Some(format!("../../{}", config.package_dir(Language::Go))));
76        let go_version = go_pkg
77            .as_ref()
78            .and_then(|p| p.version.as_ref())
79            .cloned()
80            .unwrap_or_else(|| {
81                config
82                    .resolved_version()
83                    .map(|v| format!("v{v}"))
84                    .unwrap_or_else(|| "v0.0.0".to_string())
85            });
86        // Generate go.mod. In registry mode, omit the `replace` directive so the
87        // module is fetched from the Go module proxy.
88        let effective_replace = match e2e_config.dep_mode {
89            crate::config::DependencyMode::Registry => None,
90            crate::config::DependencyMode::Local => replace_path.as_deref().map(String::from),
91        };
92        // In local mode with a `replace` directive the version in `require` is a
93        // placeholder.  Go requires that for a major-version module path (`/vN`, N ≥ 2)
94        // the placeholder version must start with `vN.`, e.g. `v3.0.0`.  A version like
95        // `v0.0.0` is rejected with "should be v3, not v0".  Fix the placeholder when the
96        // module path ends with `/vN` and the configured version doesn't match.
97        let effective_go_version = if effective_replace.is_some() {
98            fix_go_major_version(&go_module_path, &go_version)
99        } else {
100            go_version.clone()
101        };
102        files.push(GeneratedFile {
103            path: output_base.join("go.mod"),
104            content: render_go_mod(&go_module_path, effective_replace.as_deref(), &effective_go_version),
105            generated_header: false,
106        });
107
108        // Determine if any fixture needs jsonString helper across all groups.
109        let emits_executable_test =
110            |fixture: &Fixture| fixture.is_http_test() || fixture_has_go_callable(fixture, e2e_config);
111        let needs_json_stringify = groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
112            emits_executable_test(f)
113                && f.assertions.iter().any(|a| {
114                    matches!(
115                        a.assertion_type.as_str(),
116                        "contains" | "contains_all" | "contains_any" | "not_contains"
117                    ) && {
118                        if a.field.as_ref().is_none_or(|f| f.is_empty()) {
119                            e2e_config
120                                .resolve_call_for_fixture(
121                                    f.call.as_deref(),
122                                    &f.id,
123                                    &f.resolved_category(),
124                                    &f.tags,
125                                    &f.input,
126                                )
127                                .result_is_array
128                        } else {
129                            let cc = e2e_config.resolve_call_for_fixture(
130                                f.call.as_deref(),
131                                &f.id,
132                                &f.resolved_category(),
133                                &f.tags,
134                                &f.input,
135                            );
136                            let per_call_resolver = FieldResolver::new(
137                                e2e_config.effective_fields(cc),
138                                e2e_config.effective_fields_optional(cc),
139                                e2e_config.effective_result_fields(cc),
140                                e2e_config.effective_fields_array(cc),
141                                &std::collections::HashSet::new(),
142                            );
143                            let resolved_name = per_call_resolver.resolve(a.field.as_deref().unwrap_or(""));
144                            per_call_resolver.is_array(resolved_name)
145                        }
146                    }
147                })
148        });
149
150        // Generate helpers_test.go with jsonString if needed, emitted exactly once.
151        if needs_json_stringify {
152            files.push(GeneratedFile {
153                path: output_base.join("helpers_test.go"),
154                content: render_helpers_test_go(),
155                generated_header: true,
156            });
157        }
158
159        // Generate main_test.go with TestMain when:
160        // 1. Any fixture needs the mock server (has mock_response), or
161        // 2. Any fixture is client_factory-based (reads MOCK_SERVER_URL), or
162        // 3. Any fixture is file-based (requires test_documents directory setup).
163        //
164        // TestMain runs before all tests and changes to the test_documents directory,
165        // ensuring that relative file paths like "pdf/fake_memo.pdf" resolve correctly.
166        let has_file_fixtures = groups
167            .iter()
168            .flat_map(|g| g.fixtures.iter())
169            .any(|f| f.http.is_none() && !f.needs_mock_server());
170
171        let needs_main_test = has_file_fixtures
172            || groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
173                if f.needs_mock_server() {
174                    return true;
175                }
176                let cc = e2e_config.resolve_call_for_fixture(
177                    f.call.as_deref(),
178                    &f.id,
179                    &f.resolved_category(),
180                    &f.tags,
181                    &f.input,
182                );
183                let go_override = cc.overrides.get("go").or_else(|| e2e_config.call.overrides.get("go"));
184                go_override.and_then(|o| o.client_factory.as_deref()).is_some()
185            });
186
187        if needs_main_test {
188            files.push(GeneratedFile {
189                path: output_base.join("main_test.go"),
190                content: render_main_test_go(&e2e_config.test_documents_dir),
191                generated_header: true,
192            });
193        }
194
195        // Generate test files per category.
196        for group in groups {
197            let active: Vec<&Fixture> = group
198                .fixtures
199                .iter()
200                .filter(|f| super::should_include_fixture(f, lang, e2e_config))
201                .collect();
202
203            if active.is_empty() {
204                continue;
205            }
206
207            let filename = format!("{}_test.go", sanitize_filename(&group.category));
208            let content = render_test_file(
209                &group.category,
210                &active,
211                &module_path,
212                &import_alias,
213                e2e_config,
214                &config.adapters,
215                &data_enum_names,
216            );
217            files.push(GeneratedFile {
218                path: output_base.join(filename),
219                content,
220                generated_header: true,
221            });
222        }
223
224        Ok(files)
225    }
226
227    fn language_name(&self) -> &'static str {
228        "go"
229    }
230}
231
232/// Fix a Go module version so it is valid for a major-version module path.
233///
234/// Go requires that a module path ending in `/vN` (N ≥ 2) uses a version
235/// whose major component matches N.  In local-replace mode we use a synthetic
236/// placeholder version; if that placeholder (e.g. `v0.0.0`) doesn't match the
237/// major suffix, fix it to `vN.0.0` so `go mod` accepts the go.mod.
238fn fix_go_major_version(module_path: &str, version: &str) -> String {
239    // Extract `/vN` suffix from the module path (N must be ≥ 2).
240    let major = module_path
241        .rsplit('/')
242        .next()
243        .and_then(|seg| seg.strip_prefix('v'))
244        .and_then(|n| n.parse::<u64>().ok())
245        .filter(|&n| n >= 2);
246
247    let Some(n) = major else {
248        return version.to_string();
249    };
250
251    // If the version already starts with `vN.`, it is valid — leave it alone.
252    let expected_prefix = format!("v{n}.");
253    if version.starts_with(&expected_prefix) {
254        return version.to_string();
255    }
256
257    format!("v{n}.0.0")
258}
259
260fn render_go_mod(go_module_path: &str, replace_path: Option<&str>, version: &str) -> String {
261    let mut out = String::new();
262    let _ = writeln!(out, "module e2e_go");
263    let _ = writeln!(out);
264    let _ = writeln!(out, "go 1.26");
265    let _ = writeln!(out);
266    let _ = writeln!(out, "require (");
267    let _ = writeln!(out, "\t{go_module_path} {version}");
268    let _ = writeln!(out, "\tgithub.com/stretchr/testify v1.11.1");
269    let _ = writeln!(out, ")");
270
271    if let Some(path) = replace_path {
272        let _ = writeln!(out);
273        let _ = writeln!(out, "replace {go_module_path} => {path}");
274    }
275
276    out
277}
278
279/// Generate `main_test.go` that starts the mock HTTP server before all tests run.
280///
281/// The binary is expected at `../rust/target/release/mock-server` relative to the Go e2e
282/// directory.  The server prints `MOCK_SERVER_URL=http://...` on stdout; we read that line
283/// and export the variable so all test files can call `os.Getenv("MOCK_SERVER_URL")`.
284fn render_main_test_go(test_documents_dir: &str) -> String {
285    // NOTE: the generated-file header is injected by the caller (generated_header: true).
286    let mut out = String::new();
287    let _ = writeln!(out, "package e2e_test");
288    let _ = writeln!(out);
289    let _ = writeln!(out, "import (");
290    let _ = writeln!(out, "\t\"bufio\"");
291    let _ = writeln!(out, "\t\"encoding/json\"");
292    let _ = writeln!(out, "\t\"io\"");
293    let _ = writeln!(out, "\t\"os\"");
294    let _ = writeln!(out, "\t\"os/exec\"");
295    let _ = writeln!(out, "\t\"path/filepath\"");
296    let _ = writeln!(out, "\t\"runtime\"");
297    let _ = writeln!(out, "\t\"strings\"");
298    let _ = writeln!(out, "\t\"testing\"");
299    let _ = writeln!(out, ")");
300    let _ = writeln!(out);
301    let _ = writeln!(out, "func TestMain(m *testing.M) {{");
302    let _ = writeln!(out, "\t_, filename, _, _ := runtime.Caller(0)");
303    let _ = writeln!(out, "\tdir := filepath.Dir(filename)");
304    let _ = writeln!(out);
305    let _ = writeln!(
306        out,
307        "\t// Change to the configured test-documents directory (if it exists) so that fixture"
308    );
309    let _ = writeln!(
310        out,
311        "\t// file paths like \"pdf/fake_memo.pdf\" resolve correctly when running go test"
312    );
313    let _ = writeln!(
314        out,
315        "\t// from e2e/go/. Repos without document fixtures (web crawler, network clients) do"
316    );
317    let _ = writeln!(out, "\t// not ship this directory — skip chdir and run from e2e/go/.");
318    let _ = writeln!(
319        out,
320        "\ttestDocumentsDir := filepath.Join(dir, \"..\", \"..\", \"{test_documents_dir}\")"
321    );
322    let _ = writeln!(
323        out,
324        "\tif info, err := os.Stat(testDocumentsDir); err == nil && info.IsDir() {{"
325    );
326    let _ = writeln!(out, "\t\tif err := os.Chdir(testDocumentsDir); err != nil {{");
327    let _ = writeln!(out, "\t\t\tpanic(err)");
328    let _ = writeln!(out, "\t\t}}");
329    let _ = writeln!(out, "\t}}");
330    let _ = writeln!(out);
331    let _ = writeln!(out, "\t// Start the mock HTTP server if it exists.");
332    let _ = writeln!(
333        out,
334        "\tmockServerBin := filepath.Join(dir, \"..\", \"rust\", \"target\", \"release\", \"mock-server\")"
335    );
336    let _ = writeln!(out, "\tif _, err := os.Stat(mockServerBin); err == nil {{");
337    let _ = writeln!(
338        out,
339        "\t\tfixturesDir := filepath.Join(dir, \"..\", \"..\", \"fixtures\")"
340    );
341    let _ = writeln!(out, "\t\tcmd := exec.Command(mockServerBin, fixturesDir)");
342    let _ = writeln!(out, "\t\tcmd.Stderr = os.Stderr");
343    let _ = writeln!(out, "\t\tstdout, err := cmd.StdoutPipe()");
344    let _ = writeln!(out, "\t\tif err != nil {{");
345    let _ = writeln!(out, "\t\t\tpanic(err)");
346    let _ = writeln!(out, "\t\t}}");
347    let _ = writeln!(out, "\t\t// Keep a writable pipe to the mock-server's stdin so the");
348    let _ = writeln!(
349        out,
350        "\t\t// server does not see EOF and exit immediately. The mock-server"
351    );
352    let _ = writeln!(out, "\t\t// blocks reading stdin until the parent closes the pipe.");
353    let _ = writeln!(out, "\t\tstdin, err := cmd.StdinPipe()");
354    let _ = writeln!(out, "\t\tif err != nil {{");
355    let _ = writeln!(out, "\t\t\tpanic(err)");
356    let _ = writeln!(out, "\t\t}}");
357    let _ = writeln!(out, "\t\tif err := cmd.Start(); err != nil {{");
358    let _ = writeln!(out, "\t\t\tpanic(err)");
359    let _ = writeln!(out, "\t\t}}");
360    let _ = writeln!(out, "\t\tscanner := bufio.NewScanner(stdout)");
361    let _ = writeln!(out, "\t\tfor scanner.Scan() {{");
362    let _ = writeln!(out, "\t\t\tline := scanner.Text()");
363    let _ = writeln!(out, "\t\t\tif strings.HasPrefix(line, \"MOCK_SERVER_URL=\") {{");
364    let _ = writeln!(
365        out,
366        "\t\t\t\t_ = os.Setenv(\"MOCK_SERVER_URL\", strings.TrimPrefix(line, \"MOCK_SERVER_URL=\"))"
367    );
368    let _ = writeln!(out, "\t\t\t}} else if strings.HasPrefix(line, \"MOCK_SERVERS=\") {{");
369    let _ = writeln!(out, "\t\t\t\t_jsonVal := strings.TrimPrefix(line, \"MOCK_SERVERS=\")");
370    let _ = writeln!(out, "\t\t\t\t_ = os.Setenv(\"MOCK_SERVERS\", _jsonVal)");
371    let _ = writeln!(
372        out,
373        "\t\t\t\t// Parse the JSON map and set per-fixture env vars (MOCK_SERVER_<FIXTURE_ID>)."
374    );
375    let _ = writeln!(out, "\t\t\t\tvar _perFixture map[string]string");
376    let _ = writeln!(
377        out,
378        "\t\t\t\tif err := json.Unmarshal([]byte(_jsonVal), &_perFixture); err == nil {{"
379    );
380    let _ = writeln!(out, "\t\t\t\t\tfor _fid, _furl := range _perFixture {{");
381    let _ = writeln!(
382        out,
383        "\t\t\t\t\t\t_ = os.Setenv(\"MOCK_SERVER_\"+strings.ToUpper(_fid), _furl)"
384    );
385    let _ = writeln!(out, "\t\t\t\t\t}}");
386    let _ = writeln!(out, "\t\t\t\t}}");
387    let _ = writeln!(out, "\t\t\t\tbreak");
388    let _ = writeln!(out, "\t\t\t}} else if os.Getenv(\"MOCK_SERVER_URL\") != \"\" {{");
389    let _ = writeln!(out, "\t\t\t\tbreak");
390    let _ = writeln!(out, "\t\t\t}}");
391    let _ = writeln!(out, "\t\t}}");
392    let _ = writeln!(out, "\t\tgo func() {{ _, _ = io.Copy(io.Discard, stdout) }}()");
393    let _ = writeln!(out, "\t\tcode := m.Run()");
394    let _ = writeln!(out, "\t\t_ = stdin.Close()");
395    let _ = writeln!(out, "\t\t_ = cmd.Process.Signal(os.Interrupt)");
396    let _ = writeln!(out, "\t\t_ = cmd.Wait()");
397    let _ = writeln!(out, "\t\tos.Exit(code)");
398    let _ = writeln!(out, "\t}} else {{");
399    let _ = writeln!(out, "\t\tcode := m.Run()");
400    let _ = writeln!(out, "\t\tos.Exit(code)");
401    let _ = writeln!(out, "\t}}");
402    let _ = writeln!(out, "}}");
403    out
404}
405
406/// Generate `helpers_test.go` with the jsonString helper function.
407/// This is emitted once per package to avoid duplicate function definitions.
408fn render_helpers_test_go() -> String {
409    let mut out = String::new();
410    let _ = writeln!(out, "package e2e_test");
411    let _ = writeln!(out);
412    let _ = writeln!(out, "import \"encoding/json\"");
413    let _ = writeln!(out);
414    let _ = writeln!(out, "// jsonString converts a value to its JSON string representation.");
415    let _ = writeln!(
416        out,
417        "// Array fields use jsonString instead of fmt.Sprint to preserve structure."
418    );
419    let _ = writeln!(out, "func jsonString(value any) string {{");
420    let _ = writeln!(out, "\tencoded, err := json.Marshal(value)");
421    let _ = writeln!(out, "\tif err != nil {{");
422    let _ = writeln!(out, "\t\treturn \"\"");
423    let _ = writeln!(out, "\t}}");
424    let _ = writeln!(out, "\treturn string(encoded)");
425    let _ = writeln!(out, "}}");
426    out
427}
428
429fn render_test_file(
430    category: &str,
431    fixtures: &[&Fixture],
432    go_module_path: &str,
433    import_alias: &str,
434    e2e_config: &crate::config::E2eConfig,
435    adapters: &[alef_core::config::AdapterConfig],
436    data_enum_names: &std::collections::HashSet<&str>,
437) -> String {
438    let mut out = String::new();
439    let emits_executable_test =
440        |fixture: &Fixture| fixture.is_http_test() || fixture_has_go_callable(fixture, e2e_config);
441
442    // Go convention: generated file marker must appear before the package declaration.
443    out.push_str(&hash::header(CommentStyle::DoubleSlash));
444    let _ = writeln!(out);
445
446    // Determine if any fixture actually uses the pkg import.
447    // Fixtures without mock_response are emitted as t.Skip() stubs and don't reference the
448    // package — omit the import when no fixture needs it to avoid the Go "imported and not
449    // used" compile error. Visitor fixtures reference the package types (NodeContext,
450    // VisitResult, VisitResult* helpers) in struct method signatures emitted at file scope,
451    // so they also require the import even when the test body itself is a Skip stub.
452    // Direct-callable fixtures (non-HTTP, non-mock, with a resolved Go function) also
453    // reference the package when a Go override function is configured.
454    let needs_pkg = fixtures
455        .iter()
456        .any(|f| fixture_has_go_callable(f, e2e_config) || f.is_http_test() || f.visitor.is_some());
457
458    // Determine if we need the "os" import (mock_url args, HTTP fixtures, or
459    // client_factory fixtures that read MOCK_SERVER_URL via os.Getenv).
460    let needs_os = fixtures.iter().any(|f| {
461        if f.is_http_test() {
462            return true;
463        }
464        if !emits_executable_test(f) {
465            return false;
466        }
467        let call_config =
468            e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
469        let go_override = call_config
470            .overrides
471            .get("go")
472            .or_else(|| e2e_config.call.overrides.get("go"));
473        if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
474            return true;
475        }
476        let call_args = &call_config.args;
477        // Need "os" for mock_url / mock_url_list args, or for bytes args with a string
478        // fixture value (fixture-relative path loaded via os.ReadFile at test-run time).
479        if call_args
480            .iter()
481            .any(|a| a.arg_type == "mock_url" || a.arg_type == "mock_url_list")
482        {
483            return true;
484        }
485        call_args.iter().any(|a| {
486            if a.arg_type != "bytes" {
487                return false;
488            }
489            // alef.toml field paths are dotted (e.g. "input.data"). The fixture's `input`
490            // field already strips the "input." prefix, so we walk the remaining segments.
491            let mut current = &f.input;
492            let path = a.field.strip_prefix("input.").unwrap_or(&a.field);
493            for segment in path.split('.') {
494                match current.get(segment) {
495                    Some(next) => current = next,
496                    None => return false,
497                }
498            }
499            current.is_string()
500        })
501    });
502
503    // Note: file_path args are passed directly as relative strings — the e2e/go
504    // TestMain in main_test.go already chdir's into the repo-root test_documents/.
505    let needs_filepath = false;
506
507    let _needs_json_stringify = fixtures.iter().any(|f| {
508        emits_executable_test(f)
509            && f.assertions.iter().any(|a| {
510                matches!(
511                    a.assertion_type.as_str(),
512                    "contains" | "contains_all" | "contains_any" | "not_contains"
513                ) && {
514                    // Check if this assertion operates on an array field.
515                    // If no field is specified, check if the result itself is an array.
516                    if a.field.as_ref().is_none_or(|f| f.is_empty()) {
517                        // No field specified: check if result is an array
518                        e2e_config
519                            .resolve_call_for_fixture(
520                                f.call.as_deref(),
521                                &f.id,
522                                &f.resolved_category(),
523                                &f.tags,
524                                &f.input,
525                            )
526                            .result_is_array
527                    } else {
528                        // Field specified: check if that field is an array
529                        let cc = e2e_config.resolve_call_for_fixture(
530                            f.call.as_deref(),
531                            &f.id,
532                            &f.resolved_category(),
533                            &f.tags,
534                            &f.input,
535                        );
536                        let per_call_resolver = FieldResolver::new(
537                            e2e_config.effective_fields(cc),
538                            e2e_config.effective_fields_optional(cc),
539                            e2e_config.effective_result_fields(cc),
540                            e2e_config.effective_fields_array(cc),
541                            &std::collections::HashSet::new(),
542                        );
543                        let resolved_name = per_call_resolver.resolve(a.field.as_deref().unwrap_or(""));
544                        per_call_resolver.is_array(resolved_name)
545                    }
546                }
547            })
548    });
549
550    // Determine if we need "encoding/json" (handle args with non-null config,
551    // json_object args that will be unmarshalled into a typed struct, or HTTP
552    // body/partial/validation-error assertions that use json.Unmarshal).
553    let needs_json = fixtures.iter().any(|f| {
554        // HTTP body assertions use json.Unmarshal for Object/Array bodies;
555        // partial body and validation-error assertions always use json.Unmarshal.
556        if let Some(http) = &f.http {
557            let body_needs_json = http
558                .expected_response
559                .body
560                .as_ref()
561                .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
562            let partial_needs_json = http.expected_response.body_partial.is_some();
563            let ve_needs_json = http
564                .expected_response
565                .validation_errors
566                .as_ref()
567                .is_some_and(|v| !v.is_empty());
568            if body_needs_json || partial_needs_json || ve_needs_json {
569                return true;
570            }
571        }
572        if !emits_executable_test(f) {
573            return false;
574        }
575
576        let call =
577            e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
578        let call_args = &call.args;
579        // handle args with non-null config value
580        let has_handle = call_args.iter().any(|a| a.arg_type == "handle") && {
581            call_args.iter().filter(|a| a.arg_type == "handle").any(|a| {
582                let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
583                let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
584                !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
585            })
586        };
587        // json_object args with options_type or array values (will use JSON unmarshal)
588        let go_override = call.overrides.get("go");
589        let opts_type = go_override.and_then(|o| o.options_type.as_deref()).or_else(|| {
590            e2e_config
591                .call
592                .overrides
593                .get("go")
594                .and_then(|o| o.options_type.as_deref())
595        });
596        let has_json_obj = call_args.iter().any(|a| {
597            if a.arg_type != "json_object" {
598                return false;
599            }
600            let v = if a.field == "input" {
601                &f.input
602            } else {
603                let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
604                f.input.get(field).unwrap_or(&serde_json::Value::Null)
605            };
606            if v.is_array() {
607                return true;
608            } // array → []string unmarshal
609            opts_type.is_some() && v.is_object() && !v.as_object().is_some_and(|o| o.is_empty())
610        });
611        has_handle || has_json_obj
612    });
613
614    // No runtime base64 calls remain in generated Go code. Bytes args with string values
615    // are now loaded via os.ReadFile (see needs_os) and HTTP body byte arrays are
616    // base64-encoded at codegen time and embedded as literal strings in the json.Unmarshal
617    // call, which doesn't require the `encoding/base64` import in the test file.
618    let needs_base64 = false;
619
620    // Effective `result_is_simple` for a resolved call config — mirrors the per-test
621    // body logic (Go override OR call-level OR rust override). When set, the assertion
622    // renderer ignores assertion fields and operates on the result variable directly
623    // (e.g. `fmt.Sprint(result)`), so import detection must treat such field-bearing
624    // assertions as usable regardless of whether the field is valid on the result type.
625    let call_result_is_simple = |cc: &alef_core::config::e2e::CallConfig| -> bool {
626        cc.overrides.get("go").is_some_and(|o| o.result_is_simple)
627            || cc.result_is_simple
628            || cc.overrides.get("rust").map(|o| o.result_is_simple).unwrap_or(false)
629    };
630
631    // Determine if we need the "fmt" import (CustomTemplate visitor actions
632    // with placeholders, string assertions rendered through fmt.Sprint, or
633    // optional field variable bindings using fmt.Sprintf).
634    // Note: jsonString is now in helpers_test.go (uses encoding/json, not fmt),
635    // so individual test files do NOT need fmt just for calling jsonString.
636    let needs_fmt = fixtures.iter().any(|f| {
637        // Check CustomTemplate visitor actions
638        if f.visitor.as_ref().is_some_and(|v| {
639            v.callbacks.values().any(|action| {
640                if let CallbackAction::CustomTemplate { template, .. } = action {
641                    template.contains('{')
642                } else {
643                    false
644                }
645            })
646        }) {
647            return true;
648        }
649
650        if !emits_executable_test(f) {
651            return false;
652        }
653
654        // Check if any assertion uses fmt.Sprint (contains assertions with non-array fields)
655        if f.assertions.iter().any(|a| {
656            matches!(
657                a.assertion_type.as_str(),
658                "contains" | "contains_all" | "contains_any" | "not_contains"
659            ) && {
660                if a.field.as_ref().is_none_or(|f| f.is_empty()) {
661                    // No field: fmt.Sprint only if result is not an array
662                    !e2e_config
663                        .resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input)
664                        .result_is_array
665                } else {
666                    // Field specified: fmt.Sprint only if that field is not an array
667                    // and the field is actually valid for the result type.
668                    let field = a.field.as_deref().unwrap_or("");
669                    let cc = e2e_config.resolve_call_for_fixture(
670                        f.call.as_deref(),
671                        &f.id,
672                        &f.resolved_category(),
673                        &f.tags,
674                        &f.input,
675                    );
676                    let per_call_resolver = FieldResolver::new(
677                        e2e_config.effective_fields(cc),
678                        e2e_config.effective_fields_optional(cc),
679                        e2e_config.effective_result_fields(cc),
680                        e2e_config.effective_fields_array(cc),
681                        &std::collections::HashSet::new(),
682                    );
683                    let resolved_name = per_call_resolver.resolve(field);
684                    // For result_is_simple calls the renderer emits `fmt.Sprint(result)`
685                    // ignoring the field, so the field need not be valid on the result type.
686                    !per_call_resolver.is_array(resolved_name)
687                        && (call_result_is_simple(cc) || per_call_resolver.is_valid_for_result(field))
688                }
689            }
690        }) {
691            return true;
692        }
693
694        // Check if any assertion has an optional string field (triggers fmt.Sprintf in variable binding)
695        f.assertions.iter().any(|a| {
696            if let Some(field) = &a.field {
697                if !field.is_empty() && a.value.as_ref().is_some_and(|v| v.is_string()) {
698                    let cc = e2e_config.resolve_call_for_fixture(
699                        f.call.as_deref(),
700                        &f.id,
701                        &f.resolved_category(),
702                        &f.tags,
703                        &f.input,
704                    );
705                    let per_call_resolver = FieldResolver::new(
706                        e2e_config.effective_fields(cc),
707                        e2e_config.effective_fields_optional(cc),
708                        e2e_config.effective_result_fields(cc),
709                        e2e_config.effective_fields_array(cc),
710                        &std::collections::HashSet::new(),
711                    );
712                    let resolved = per_call_resolver.resolve(field);
713                    // Need fmt if field is optional, not an array, not a map, and valid for result
714                    per_call_resolver.is_optional(resolved)
715                        && !per_call_resolver.is_array(resolved)
716                        && !per_call_resolver.has_map_access(field)
717                        && per_call_resolver.is_valid_for_result(field)
718                } else {
719                    false
720                }
721            } else {
722                false
723            }
724        })
725    });
726
727    // Determine if we need the "strings" import.
728    // Only count assertions whose fields are actually valid for the result type.
729    // Also check if any fixture has mock_url_list args (needs strings.HasPrefix).
730    let needs_strings = fixtures.iter().any(|f| {
731        if !emits_executable_test(f) {
732            return false;
733        }
734        // Check for mock_url_list in call args
735        let cc =
736            e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
737        if cc.args.iter().any(|arg| arg.arg_type == "mock_url_list") {
738            return true;
739        }
740        let per_call_resolver = FieldResolver::new(
741            e2e_config.effective_fields(cc),
742            e2e_config.effective_fields_optional(cc),
743            e2e_config.effective_result_fields(cc),
744            e2e_config.effective_fields_array(cc),
745            &std::collections::HashSet::new(),
746        );
747        f.assertions.iter().any(|a| {
748            let type_needs_strings = if a.assertion_type == "equals" {
749                // equals with string values needs strings.TrimSpace
750                a.value.as_ref().is_some_and(|v| v.is_string())
751            } else {
752                matches!(
753                    a.assertion_type.as_str(),
754                    "contains" | "contains_all" | "contains_any" | "not_contains" | "starts_with" | "ends_with"
755                )
756            };
757            // For result_is_simple calls the renderer emits `strings.Contains(...)`
758            // against the result variable directly, ignoring the assertion field.
759            let simple_result = call_result_is_simple(cc);
760            let field_valid = a
761                .field
762                .as_ref()
763                .map(|f| f.is_empty() || simple_result || per_call_resolver.is_valid_for_result(f))
764                .unwrap_or(true);
765            type_needs_strings && field_valid
766        })
767    });
768
769    // The testify `assert` import is decided purely empirically below: after the
770    // file body is rendered into a buffer, we emit the import only if the body
771    // actually contains an `assert.` reference. A predictive heuristic over
772    // assertion shapes/categories produced false positives (importing testify in
773    // files whose tests only use `t.Errorf`/`t.Fatalf`), which fails the Go
774    // `imported and not used` compile check.
775
776    // Determine if we need "net/http" and "io" (HTTP server tests via HTTP client).
777    let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
778    let needs_http = has_http_fixtures;
779    // io.ReadAll is emitted for every HTTP fixture (render_call always reads the body).
780    let needs_io = has_http_fixtures;
781
782    // Determine if we need "reflect" (for HTTP response body JSON comparison
783    // and partial-body assertions, both of which use reflect.DeepEqual).
784    let needs_reflect = fixtures.iter().any(|f| {
785        if let Some(http) = &f.http {
786            let body_needs_reflect = http
787                .expected_response
788                .body
789                .as_ref()
790                .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
791            let partial_needs_reflect = http.expected_response.body_partial.is_some();
792            body_needs_reflect || partial_needs_reflect
793        } else {
794            false
795        }
796    });
797
798    // Render the file body (visitor structs + test functions) into a separate buffer
799    // first so we can scan the actual emitted code for `assert.` usage. This avoids
800    // false positives in the heuristic-based needs_assert detection: imports are emitted
801    // only when the rendered code actually uses them.
802    let mut body = String::new();
803    for fixture in fixtures.iter() {
804        if let Some(visitor_spec) = &fixture.visitor {
805            let struct_name = visitor_struct_name(&fixture.id);
806            emit_go_visitor_struct(&mut body, &struct_name, visitor_spec, import_alias);
807            let _ = writeln!(body);
808        }
809    }
810    for (i, fixture) in fixtures.iter().enumerate() {
811        render_test_function(&mut body, fixture, import_alias, e2e_config, adapters, data_enum_names);
812        if i + 1 < fixtures.len() {
813            let _ = writeln!(body);
814        }
815    }
816    let needs_assert = body.contains("assert.");
817
818    let _ = writeln!(out, "// E2e tests for category: {category}");
819    let _ = writeln!(out, "package e2e_test");
820    let _ = writeln!(out);
821    let _ = writeln!(out, "import (");
822    if needs_base64 {
823        let _ = writeln!(out, "\t\"encoding/base64\"");
824    }
825    if needs_json || needs_reflect {
826        let _ = writeln!(out, "\t\"encoding/json\"");
827    }
828    if needs_fmt {
829        let _ = writeln!(out, "\t\"fmt\"");
830    }
831    if needs_io {
832        let _ = writeln!(out, "\t\"io\"");
833    }
834    if needs_http {
835        let _ = writeln!(out, "\t\"net/http\"");
836    }
837    if needs_os {
838        let _ = writeln!(out, "\t\"os\"");
839    }
840    let _ = needs_filepath; // reserved for future use; currently always false
841    if needs_reflect {
842        let _ = writeln!(out, "\t\"reflect\"");
843    }
844    if needs_strings {
845        let _ = writeln!(out, "\t\"strings\"");
846    }
847    let _ = writeln!(out, "\t\"testing\"");
848    if needs_assert {
849        let _ = writeln!(out);
850        let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
851    }
852
853    if needs_pkg {
854        let _ = writeln!(out);
855        let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
856    }
857    let _ = writeln!(out, ")");
858    let _ = writeln!(out);
859
860    // Append the pre-rendered body (visitor structs + test functions).
861    out.push_str(&body);
862
863    // Clean up trailing newlines.
864    while out.ends_with("\n\n") {
865        out.pop();
866    }
867    if !out.ends_with('\n') {
868        out.push('\n');
869    }
870    out
871}
872
873/// Return `true` when a non-HTTP fixture can be exercised by calling the Go
874/// binding directly.
875///
876/// A fixture is Go-callable when the resolved call config provides a non-empty
877/// function name — either via a Go-specific override (`[e2e.call.overrides.go]
878/// function`) or via the base call `function` field.  The Go binding exposes all
879/// public functions from the Rust core as PascalCase exports, so any non-empty
880/// function name can be resolved to a valid Go symbol via `to_go_name`.
881fn fixture_has_go_callable(fixture: &Fixture, e2e_config: &crate::config::E2eConfig) -> bool {
882    // HTTP fixtures are handled by render_http_test_function — not our concern here.
883    if fixture.is_http_test() {
884        return false;
885    }
886    let call_config = e2e_config.resolve_call_for_fixture(
887        fixture.call.as_deref(),
888        &fixture.id,
889        &fixture.resolved_category(),
890        &fixture.tags,
891        &fixture.input,
892    );
893    // Honor per-call `skip_languages`: when the resolved call's `skip_languages`
894    // contains `"go"`, the Go binding doesn't expose this function.
895    if call_config.skip_languages.iter().any(|l| l == "go") {
896        return false;
897    }
898    let go_override = call_config
899        .overrides
900        .get("go")
901        .or_else(|| e2e_config.call.overrides.get("go"));
902    // When a client_factory is configured, the fixture is callable via the
903    // client-method pattern even when the base function name is empty.
904    if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
905        return true;
906    }
907    // Prefer a Go-specific override function name; fall back to the base function name.
908    // Any non-empty function name is callable: the Go binding exports all public
909    // Rust functions as PascalCase symbols (snake_case → PascalCase via to_go_name).
910    let fn_name = go_override
911        .and_then(|o| o.function.as_deref())
912        .filter(|s| !s.is_empty())
913        .unwrap_or(call_config.function.as_str());
914    !fn_name.is_empty()
915}
916
917fn render_test_function(
918    out: &mut String,
919    fixture: &Fixture,
920    import_alias: &str,
921    e2e_config: &crate::config::E2eConfig,
922    adapters: &[alef_core::config::AdapterConfig],
923    data_enum_names: &std::collections::HashSet<&str>,
924) {
925    let fn_name = fixture.id.to_upper_camel_case();
926    let description = &fixture.description;
927
928    // Delegate HTTP fixtures to the shared driver via GoTestClientRenderer.
929    if fixture.http.is_some() {
930        render_http_test_function(out, fixture);
931        return;
932    }
933
934    // Non-HTTP fixtures are tested directly when the call config provides a
935    // callable Go function.  Emit a t.Skip() stub when:
936    //   - No mock response and no callable (non-HTTP, non-mock, unreachable), or
937    //   - The call's skip_languages includes "go" (e.g. streaming not supported).
938    if !fixture_has_go_callable(fixture, e2e_config) {
939        let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
940        let _ = writeln!(out, "\t// {description}");
941        let _ = writeln!(
942            out,
943            "\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")"
944        );
945        let _ = writeln!(out, "}}");
946        return;
947    }
948
949    // Resolve call config per-fixture (supports named calls via fixture.call).
950    let call_config = e2e_config.resolve_call_for_fixture(
951        fixture.call.as_deref(),
952        &fixture.id,
953        &fixture.resolved_category(),
954        &fixture.tags,
955        &fixture.input,
956    );
957    // Per-call field resolver: uses effective fields/result_fields from the resolved call.
958    let call_field_resolver = FieldResolver::new(
959        e2e_config.effective_fields(call_config),
960        e2e_config.effective_fields_optional(call_config),
961        e2e_config.effective_result_fields(call_config),
962        e2e_config.effective_fields_array(call_config),
963        &std::collections::HashSet::new(),
964    );
965    let field_resolver = &call_field_resolver;
966    let lang = "go";
967    let overrides = call_config.overrides.get(lang);
968
969    // Select the function name: Go bindings now integrate visitor support into
970    // the main Convert() function via ConversionOptions.Visitor field.
971    // (In other languages, there may be separate visitor_function overrides, but Go uses a single function.)
972    let base_function_name = overrides
973        .and_then(|o| o.function.as_deref())
974        .unwrap_or(&call_config.function);
975    let function_name = to_go_name(base_function_name);
976    let result_var = &call_config.result_var;
977    let args = &call_config.args;
978
979    // Whether the function returns (value, error) or just (error) or just (value).
980    // Check Go override first, fall back to call-level returns_result.
981    let returns_result = overrides
982        .and_then(|o| o.returns_result)
983        .unwrap_or(call_config.returns_result);
984
985    // Whether the function returns only error (no value component), i.e. Result<(), E>.
986    // When returns_result=true and returns_void=true, Go emits `err :=` not `_, err :=`.
987    let returns_void = call_config.returns_void;
988
989    // result_is_simple: result is a scalar (*string, *bool, etc.) not a struct.
990    // Boolean OR with call-level — serde defaults `result_is_simple` to `false` on
991    // CallOverride, so a Go override that only sets e.g. `returns_result = false`
992    // would otherwise silently clobber a true call-level value. Falls back to the
993    // rust override only when neither the Go override nor the call-level value is set.
994    let result_is_simple = overrides.is_some_and(|o| o.result_is_simple)
995        || call_config.result_is_simple
996        || call_config
997            .overrides
998            .get("rust")
999            .map(|o| o.result_is_simple)
1000            .unwrap_or(false);
1001
1002    // result_is_array: the simple result is a slice/array type (e.g., []string).
1003    // Boolean OR with call-level — serde defaults `result_is_array` to `false` on
1004    // CallOverride, so a Go override that only sets `result_is_pointer` would
1005    // otherwise silently mask a true call-level value. Until the field becomes
1006    // Option<bool>, OR'ing is the only safe coalesce.
1007    let result_is_array = overrides.is_some_and(|o| o.result_is_array) || call_config.result_is_array;
1008
1009    // Per-call Go options_type, falling back to the default call's Go override.
1010    let call_options_type = overrides.and_then(|o| o.options_type.as_deref()).or_else(|| {
1011        e2e_config
1012            .call
1013            .overrides
1014            .get("go")
1015            .and_then(|o| o.options_type.as_deref())
1016    });
1017
1018    // Whether json_object options are passed as a pointer (*OptionsType).
1019    let call_options_ptr = overrides.map(|o| o.options_ptr).unwrap_or_else(|| {
1020        e2e_config
1021            .call
1022            .overrides
1023            .get("go")
1024            .map(|o| o.options_ptr)
1025            .unwrap_or(false)
1026    });
1027
1028    let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
1029    // Validation-category fixtures expect engine *creation* to fail. Other expects_error
1030    // fixtures (error_*) construct a valid engine and expect the *operation* to fail —
1031    // engine creation should not be wrapped in assert.Error there.
1032    let validation_creation_failure = expects_error && fixture.resolved_category() == "validation";
1033
1034    // Client factory: when set, the test creates a client via `pkg.Factory("test-key", baseURL)`
1035    // and calls methods on the instance rather than top-level package functions.
1036    let client_factory = overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
1037        e2e_config
1038            .call
1039            .overrides
1040            .get(lang)
1041            .and_then(|o| o.client_factory.as_deref())
1042    });
1043
1044    let (mut setup_lines, args_str) = build_args_and_setup(
1045        &fixture.input,
1046        args,
1047        import_alias,
1048        call_options_type,
1049        fixture,
1050        call_options_ptr,
1051        validation_creation_failure,
1052        data_enum_names,
1053    );
1054
1055    // Build visitor if present — integrate into options instead of separate parameter.
1056    // Go binding's Convert() checks options.Visitor and delegates to convertWithVisitorHelper when set.
1057    let mut visitor_opts_var: Option<String> = None;
1058    if fixture.visitor.is_some() {
1059        let struct_name = visitor_struct_name(&fixture.id);
1060        setup_lines.push(format!("visitor := &{struct_name}{{}}"));
1061        // Create a fresh opts variable with the visitor attached.
1062        let opts_type = call_options_type.unwrap_or("ConversionOptions");
1063        let opts_var = "opts".to_string();
1064        setup_lines.push(format!("opts := &{import_alias}.{opts_type}{{}}"));
1065        setup_lines.push("opts.Visitor = visitor".to_string());
1066        visitor_opts_var = Some(opts_var);
1067    }
1068
1069    let go_extra_args = overrides.map(|o| o.extra_args.as_slice()).unwrap_or(&[]).to_vec();
1070    let final_args = {
1071        let mut parts: Vec<String> = Vec::new();
1072        if !args_str.is_empty() {
1073            // When visitor is present, replace trailing ", nil" with ", opts"
1074            let processed_args = if let Some(ref opts_var) = visitor_opts_var {
1075                args_str.trim_end_matches(", nil").to_string() + ", " + opts_var
1076            } else {
1077                args_str
1078            };
1079            parts.push(processed_args);
1080        }
1081        parts.extend(go_extra_args);
1082        parts.join(", ")
1083    };
1084
1085    let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1086    let _ = writeln!(out, "\t// {description}");
1087
1088    // Live-API fixtures use `env.api_key_var` to mark the env var that
1089    // supplies the real API key. Skip the test when the env var is unset
1090    // (mirrors Python's pytest.skip and Node's early-return pattern).
1091    let has_mock = fixture.mock_response.is_some() || fixture.http.is_some();
1092    let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
1093    if let Some(var) = api_key_var {
1094        if has_mock {
1095            // Env-fallback branch: when the real API key is set use the live
1096            // provider; otherwise fall back to the mock server so the test
1097            // always runs in CI without credentials.
1098            let fixture_id = &fixture.id;
1099            let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
1100            let _ = writeln!(out, "\tvar baseURL *string");
1101            let _ = writeln!(out, "\tif apiKey != \"\" {{");
1102            let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using real API ({var} is set)\")");
1103            let _ = writeln!(out, "\t}} else {{");
1104            let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using mock server ({var} not set)\")");
1105            let _ = writeln!(
1106                out,
1107                "\t\tu := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1108            );
1109            let _ = writeln!(out, "\t\tbaseURL = &u");
1110            let _ = writeln!(out, "\t\tapiKey = \"test-key\"");
1111            let _ = writeln!(out, "\t}}");
1112        } else {
1113            let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
1114            let _ = writeln!(out, "\tif apiKey == \"\" {{");
1115            let _ = writeln!(out, "\t\tt.Skipf(\"{var} not set\")");
1116            let _ = writeln!(out, "\t}}");
1117        }
1118    }
1119
1120    for line in &setup_lines {
1121        let _ = writeln!(out, "\t{line}");
1122    }
1123
1124    // Client factory: emit client creation before the call.
1125    // Each test creates a fresh client pointed at MOCK_SERVER_URL/fixtures/<id>
1126    // so the mock server can serve the fixture response via prefix routing.
1127    let call_prefix = if let Some(factory) = client_factory {
1128        let factory_name = to_go_name(factory);
1129        let fixture_id = &fixture.id;
1130        // Determine how to express the API key and base URL for the client
1131        // constructor call, depending on which code path was emitted above.
1132        let (api_key_expr, base_url_expr) = if has_mock && api_key_var.is_some() {
1133            // Env-fallback: local vars emitted above carry the right values.
1134            ("apiKey".to_string(), "baseURL".to_string())
1135        } else if api_key_var.is_some() {
1136            // Skip-unless-set: live API only, no mock fallback.
1137            ("apiKey".to_string(), "nil".to_string())
1138        } else if fixture.has_host_root_route() {
1139            let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1140            let _ = writeln!(out, "\tmockURL := os.Getenv(\"{env_key}\")");
1141            let _ = writeln!(out, "\tif mockURL == \"\" {{");
1142            let _ = writeln!(
1143                out,
1144                "\t\tmockURL = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1145            );
1146            let _ = writeln!(out, "\t}}");
1147            ("\"test-key\"".to_string(), "&mockURL".to_string())
1148        } else {
1149            let _ = writeln!(
1150                out,
1151                "\tmockURL := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1152            );
1153            ("\"test-key\"".to_string(), "&mockURL".to_string())
1154        };
1155        let _ = writeln!(
1156            out,
1157            "\tclient, clientErr := {import_alias}.{factory_name}({api_key_expr}, {base_url_expr}, nil, nil, nil)"
1158        );
1159        let _ = writeln!(out, "\tif clientErr != nil {{");
1160        let _ = writeln!(out, "\t\tt.Fatalf(\"create client failed: %v\", clientErr)");
1161        let _ = writeln!(out, "\t}}");
1162        "client".to_string()
1163    } else {
1164        import_alias.to_string()
1165    };
1166
1167    // The Go binding generator wraps the FFI call in `(T, error)` whenever any
1168    // param requires JSON marshalling, even when the underlying Rust function
1169    // does not return Result. Detect that so error-expecting tests emit `_, err :=`
1170    // instead of `err :=` when the binding has a value component.
1171    let binding_returns_error_pre = args
1172        .iter()
1173        .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1174    let effective_returns_result_pre = returns_result || binding_returns_error_pre || client_factory.is_some();
1175
1176    if expects_error {
1177        if effective_returns_result_pre && !returns_void {
1178            let _ = writeln!(out, "\t_, err := {call_prefix}.{function_name}({final_args})");
1179        } else {
1180            let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1181        }
1182        let _ = writeln!(out, "\tif err == nil {{");
1183        let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
1184        let _ = writeln!(out, "\t}}");
1185        let _ = writeln!(out, "}}");
1186        return;
1187    }
1188
1189    // Detect streaming fixtures (call-level `streaming` opt-out is honored).
1190    let is_streaming = crate::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming);
1191
1192    // Determine the streaming item type for this call (used when draining the channel).
1193    // Find the first streaming adapter matching the function name (e.g., crawl_stream → CrawlEvent).
1194    // Match on snake_case adapter name vs. snake_case function name; e2e codegen sees
1195    // camelCase function names like `CrawlStream` but adapters declare `crawl_stream`.
1196    use heck::ToSnakeCase;
1197    let fn_snake = function_name.to_snake_case();
1198    let base_snake = base_function_name.to_snake_case();
1199    let streaming_item_type = if is_streaming {
1200        adapters
1201            .iter()
1202            .filter(|a| matches!(a.pattern, alef_core::config::extras::AdapterPattern::Streaming))
1203            .find(|a| a.name == fn_snake || a.name == base_snake)
1204            .and_then(|a| a.item_type.as_deref())
1205            .and_then(|t| t.rsplit("::").next())
1206            .unwrap_or("Item") // Fallback if no matching adapter is declared
1207    } else {
1208        "Item" // Unused, but needed for type consistency
1209    };
1210
1211    // Check if any assertion actually uses the result variable.
1212    // If all assertions are skipped (field not on result type), use `_` to avoid
1213    // Go's "declared and not used" compile error.
1214    // For streaming fixtures: streaming virtual fields count as usable.
1215    let has_usable_assertion = fixture.assertions.iter().any(|a| {
1216        if a.assertion_type == "not_error" || a.assertion_type == "error" {
1217            return false;
1218        }
1219        // method_result assertions always use the result variable.
1220        if a.assertion_type == "method_result" {
1221            return true;
1222        }
1223        match &a.field {
1224            Some(f) if !f.is_empty() => {
1225                if is_streaming && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
1226                    return true;
1227                }
1228                // When the call returns a plain scalar (result_is_simple), `render_assertion`
1229                // ignores the field entirely and operates on the result variable directly
1230                // (e.g. `fmt.Sprint(result)`). In that case the assertion always uses the
1231                // result variable regardless of whether `f` is a real field on the type, so
1232                // the call must bind to `result` and not `_`.
1233                if result_is_simple {
1234                    return true;
1235                }
1236                field_resolver.is_valid_for_result(f)
1237            }
1238            _ => true,
1239        }
1240    });
1241
1242    // The Go binding generator (alef-backend-go) wraps the FFI call in `(T, error)`
1243    // whenever any param requires JSON marshalling (Vec, Map, Named struct), even when
1244    // the underlying Rust function does not return Result. So a result_is_simple call
1245    // like `generate_cache_key(parts: &[(String, String)]) -> String` still surfaces in
1246    // Go as `func GenerateCacheKey(parts [][]string) (*string, error)`. Detect that
1247    // here so the test emits `_, err :=` / `result, err :=` instead of `result :=`.
1248    let binding_returns_error = args
1249        .iter()
1250        .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1251    // Client-factory methods always return (value, error) in the Go binding.
1252    let effective_returns_result = returns_result || binding_returns_error || client_factory.is_some();
1253
1254    // For result_is_simple functions, the result variable IS the value (e.g. *string, *bool).
1255    // We create a local `value` that dereferences it so assertions can use a plain type.
1256    // For functions that return (value, error): emit `result, err :=`
1257    // For functions that return only error: emit `err :=`
1258    // For functions that return only a value (result_is_simple, no error): emit `result :=`
1259    if !effective_returns_result && result_is_simple {
1260        // Function returns a single value, no error (e.g. *string, *bool).
1261        let result_binding = if has_usable_assertion {
1262            result_var.to_string()
1263        } else {
1264            "_".to_string()
1265        };
1266        // In Go, `_ :=` is invalid — must use `_ =` for the blank identifier.
1267        let assign_op = if result_binding == "_" { "=" } else { ":=" };
1268        let _ = writeln!(
1269            out,
1270            "\t{result_binding} {assign_op} {call_prefix}.{function_name}({final_args})"
1271        );
1272        if has_usable_assertion && result_binding != "_" {
1273            if result_is_array {
1274                // Array results are slices (not pointers); assign directly without dereference.
1275                let _ = writeln!(out, "\tvalue := {result_var}");
1276            } else {
1277                // Check if ALL simple-result assertions are is_empty/is_null with no field.
1278                // If so, skip dereference — we'll use the pointer directly.
1279                let only_nil_assertions = fixture
1280                    .assertions
1281                    .iter()
1282                    .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1283                    .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1284                    .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1285
1286                if !only_nil_assertions {
1287                    // Emit nil check and dereference for simple pointer results only if
1288                    // the result is actually a pointer (determined by result_is_pointer override).
1289                    let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
1290                    if result_is_ptr {
1291                        let _ = writeln!(out, "\tif {result_var} == nil {{");
1292                        let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1293                        let _ = writeln!(out, "\t}}");
1294                        let _ = writeln!(out, "\tvalue := *{result_var}");
1295                    } else {
1296                        // Result is a value type (not a pointer), use directly without dereference.
1297                        let _ = writeln!(out, "\tvalue := {result_var}");
1298                    }
1299                }
1300            }
1301        }
1302    } else if !effective_returns_result || returns_void {
1303        // Function returns only error (either returns_result=false, or returns_result=true
1304        // with returns_void=true meaning the Go function signature is `func(...) error`).
1305        let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1306        let _ = writeln!(out, "\tif err != nil {{");
1307        let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1308        let _ = writeln!(out, "\t}}");
1309        // No result variable to use in assertions.
1310        let _ = writeln!(out, "}}");
1311        return;
1312    } else {
1313        // returns_result = true, returns_void = false: function returns (value, error).
1314        // For streaming fixtures, always capture the channel as `stream`.
1315        let result_binding = if is_streaming {
1316            "stream".to_string()
1317        } else if has_usable_assertion {
1318            result_var.to_string()
1319        } else {
1320            "_".to_string()
1321        };
1322        let _ = writeln!(
1323            out,
1324            "\t{result_binding}, err := {call_prefix}.{function_name}({final_args})"
1325        );
1326        let _ = writeln!(out, "\tif err != nil {{");
1327        let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1328        let _ = writeln!(out, "\t}}");
1329        // For streaming fixtures: drain the channel into a []T slice.
1330        if is_streaming {
1331            let _ = writeln!(out, "\tvar chunks []{import_alias}.{streaming_item_type}");
1332            let _ = writeln!(out, "\tfor chunk := range stream {{");
1333            let _ = writeln!(out, "\t\tchunks = append(chunks, chunk)");
1334            let _ = writeln!(out, "\t}}");
1335        }
1336        if result_is_simple && has_usable_assertion && result_binding != "_" {
1337            if result_is_array {
1338                // Array results are slices (not pointers); assign directly without dereference.
1339                let _ = writeln!(out, "\tvalue := {}", result_var);
1340            } else {
1341                // Check if ALL simple-result assertions are is_empty/is_null with no field.
1342                // If so, skip dereference — we'll use the pointer directly.
1343                let only_nil_assertions = fixture
1344                    .assertions
1345                    .iter()
1346                    .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1347                    .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1348                    .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1349
1350                if !only_nil_assertions {
1351                    // Emit nil check and dereference for simple pointer results only if
1352                    // the result is actually a pointer (determined by result_is_pointer override).
1353                    let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
1354                    if result_is_ptr {
1355                        let _ = writeln!(out, "\tif {} == nil {{", result_var);
1356                        let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1357                        let _ = writeln!(out, "\t}}");
1358                        let _ = writeln!(out, "\tvalue := *{}", result_var);
1359                    } else {
1360                        // Result is a value type (not a pointer), use directly without dereference.
1361                        let _ = writeln!(out, "\tvalue := {}", result_var);
1362                    }
1363                }
1364            }
1365        }
1366    }
1367
1368    // For result_is_simple functions, determine if we created a dereferenced `value` variable.
1369    // We skip dereferencing if all simple-result assertions are is_empty/is_null with no field,
1370    // or if the result is a value type (not a pointer).
1371    let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
1372    let has_deref_value = if result_is_simple && has_usable_assertion && !result_is_array && result_is_ptr {
1373        let only_nil_assertions = fixture
1374            .assertions
1375            .iter()
1376            .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1377            .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1378            .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1379        !only_nil_assertions
1380    } else if result_is_simple && has_usable_assertion && result_is_ptr {
1381        true
1382    } else {
1383        result_is_simple && has_usable_assertion
1384    };
1385
1386    let effective_result_var = if has_deref_value {
1387        "value".to_string()
1388    } else {
1389        result_var.to_string()
1390    };
1391
1392    // Collect optional fields referenced by assertions and emit nil-safe
1393    // dereference blocks so that assertions can use plain string locals.
1394    // Only dereference fields whose assertion values are strings (or that are
1395    // used in string-oriented assertions like equals/contains with string values).
1396    let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1397    for assertion in &fixture.assertions {
1398        if let Some(f) = &assertion.field {
1399            if !f.is_empty() {
1400                // Only emit local vars for fields that are actually valid for the result type.
1401                // Skip fields that will be marked "skipped" later in render_assertion.
1402                if !result_is_simple && !field_resolver.is_valid_for_result(f) {
1403                    continue;
1404                }
1405                let resolved = field_resolver.resolve(f);
1406                if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
1407                    // Only create deref locals for string-valued fields that are NOT arrays.
1408                    // Array fields (e.g., *[]string) must keep their pointer form so
1409                    // render_assertion can emit strings.Join(*field, " ") rather than
1410                    // treating them as plain strings.
1411                    let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
1412                    let is_array_field = field_resolver.is_array(resolved);
1413                    if !is_string_field || is_array_field {
1414                        // Non-string optional fields (e.g., *uint64) and array optional
1415                        // fields (e.g., *[]string) are handled by nil guards in render_assertion.
1416                        continue;
1417                    }
1418                    let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
1419                    let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
1420                    if field_resolver.has_map_access(f) {
1421                        // Go map access returns a value type (string), not a pointer.
1422                        // Use the value directly — empty string means not present.
1423                        let _ = writeln!(out, "\t{local_var} := {field_expr}");
1424                    } else {
1425                        let _ = writeln!(out, "\tvar {local_var} string");
1426                        let _ = writeln!(out, "\tif {field_expr} != nil {{");
1427                        // Use fmt.Sprintf to safely convert any type (named string types, structs, etc.)
1428                        // to string. This handles both *FinishReason (named type) and *FormatMetadata
1429                        // (struct types) without compile errors.
1430                        let _ = writeln!(out, "\t\t{local_var} = fmt.Sprintf(\"%v\", *{field_expr})");
1431                        let _ = writeln!(out, "\t}}");
1432                    }
1433                    optional_locals.insert(f.clone(), local_var);
1434                }
1435            }
1436        }
1437    }
1438
1439    // Emit assertions, wrapping in nil guards when an intermediate path segment is optional.
1440    for assertion in &fixture.assertions {
1441        if let Some(f) = &assertion.field {
1442            if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
1443                // Check if any prefix of the dotted path is optional (pointer in Go).
1444                // e.g., "document.nodes" — if "document" is optional, guard the whole block.
1445                let parts: Vec<&str> = f.split('.').collect();
1446                let mut guard_expr: Option<String> = None;
1447                for i in 1..parts.len() {
1448                    let prefix = parts[..i].join(".");
1449                    let resolved_prefix = field_resolver.resolve(&prefix);
1450                    if field_resolver.is_optional(resolved_prefix) {
1451                        // If the prefix ends with a numeric index (e.g. "segments[0]"),
1452                        // the element itself is a value type in Go — it cannot be nil.
1453                        // Use the array field without the index (e.g. "segments") as the
1454                        // nil guard instead, so we emit `result.Segments != nil` rather
1455                        // than the invalid `result.Segments[0] != nil`.
1456                        let guard_prefix = if let Some(bracket_pos) = resolved_prefix.rfind('[') {
1457                            let suffix = &resolved_prefix[bracket_pos + 1..];
1458                            let is_numeric_index = suffix.trim_end_matches(']').chars().all(|c| c.is_ascii_digit());
1459                            if is_numeric_index {
1460                                &resolved_prefix[..bracket_pos]
1461                            } else {
1462                                resolved_prefix
1463                            }
1464                        } else {
1465                            resolved_prefix
1466                        };
1467                        let accessor = field_resolver.accessor(guard_prefix, "go", &effective_result_var);
1468                        guard_expr = Some(accessor);
1469                        break;
1470                    }
1471                }
1472                if let Some(guard) = guard_expr {
1473                    // Only emit nil guard if the assertion will actually produce code
1474                    // (not just a skip comment), to avoid empty branches (SA9003).
1475                    if field_resolver.is_valid_for_result(f) {
1476                        // For Go, avoid emitting nil checks on struct value types.
1477                        // In Go, struct types that are not wrapped in Option<T> in Rust
1478                        // remain value types in the Go binding, so they cannot be nil.
1479                        // Skip the guard if the expression is a direct struct field access
1480                        // (no array indexing, no map keys, no function calls).
1481                        let is_struct_value = !guard.contains('[') && !guard.contains('(') && !guard.contains("map");
1482                        if is_struct_value {
1483                            // Guard refers to a struct value type — skip the nil check
1484                            // and render the assertion directly.
1485                            render_assertion(
1486                                out,
1487                                assertion,
1488                                &effective_result_var,
1489                                import_alias,
1490                                field_resolver,
1491                                &optional_locals,
1492                                result_is_simple,
1493                                result_is_array,
1494                                is_streaming,
1495                            );
1496                            continue;
1497                        }
1498                        let _ = writeln!(out, "\tif {guard} != nil {{");
1499                        // Render into a temporary buffer so we can re-indent by one
1500                        // tab level to sit inside the nil-guard block.
1501                        let mut nil_buf = String::new();
1502                        render_assertion(
1503                            &mut nil_buf,
1504                            assertion,
1505                            &effective_result_var,
1506                            import_alias,
1507                            field_resolver,
1508                            &optional_locals,
1509                            result_is_simple,
1510                            result_is_array,
1511                            is_streaming,
1512                        );
1513                        for line in nil_buf.lines() {
1514                            let _ = writeln!(out, "\t{line}");
1515                        }
1516                        let _ = writeln!(out, "\t}}");
1517                    } else {
1518                        render_assertion(
1519                            out,
1520                            assertion,
1521                            &effective_result_var,
1522                            import_alias,
1523                            field_resolver,
1524                            &optional_locals,
1525                            result_is_simple,
1526                            result_is_array,
1527                            is_streaming,
1528                        );
1529                    }
1530                    continue;
1531                }
1532            }
1533        }
1534        render_assertion(
1535            out,
1536            assertion,
1537            &effective_result_var,
1538            import_alias,
1539            field_resolver,
1540            &optional_locals,
1541            result_is_simple,
1542            result_is_array,
1543            is_streaming,
1544        );
1545    }
1546
1547    let _ = writeln!(out, "}}");
1548}
1549
1550/// Render an HTTP server test function using net/http against MOCK_SERVER_URL.
1551///
1552/// Delegates to the shared driver [`client::http_call::render_http_test`] via
1553/// [`GoTestClientRenderer`]. The emitted test shape is unchanged: `func Test_<Name>(t *testing.T)`
1554/// with a `net/http` client that hits `$MOCK_SERVER_URL/fixtures/<id>`.
1555fn render_http_test_function(out: &mut String, fixture: &Fixture) {
1556    client::http_call::render_http_test(out, &GoTestClientRenderer, fixture);
1557}
1558
1559// ---------------------------------------------------------------------------
1560// HTTP test rendering — GoTestClientRenderer
1561// ---------------------------------------------------------------------------
1562
1563/// Go `net/http` test renderer.
1564///
1565/// Go HTTP e2e tests send a request to `$MOCK_SERVER_URL/fixtures/<id>` using
1566/// the standard library `net/http` client. The trait primitives emit the
1567/// request-build, response-capture, and assertion code that the previous
1568/// monolithic renderer produced, so generated output is unchanged after the
1569/// migration.
1570struct GoTestClientRenderer;
1571
1572impl client::TestClientRenderer for GoTestClientRenderer {
1573    fn language_name(&self) -> &'static str {
1574        "go"
1575    }
1576
1577    /// Go test names use `UpperCamelCase` so they form valid exported identifiers
1578    /// (e.g. `Test_MyFixtureId`). Override the default `sanitize_ident` which
1579    /// produces `lower_snake_case`.
1580    fn sanitize_test_name(&self, id: &str) -> String {
1581        id.to_upper_camel_case()
1582    }
1583
1584    /// Emit `func Test_<fn_name>(t *testing.T) {`, a description comment, and the
1585    /// `baseURL` / request scaffolding. Skipped fixtures get `t.Skip(...)` inline.
1586    fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
1587        let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1588        let _ = writeln!(out, "\t// {description}");
1589        if let Some(reason) = skip_reason {
1590            let escaped = go_string_literal(reason);
1591            let _ = writeln!(out, "\tt.Skip({escaped})");
1592        }
1593    }
1594
1595    fn render_test_close(&self, out: &mut String) {
1596        let _ = writeln!(out, "}}");
1597    }
1598
1599    /// Emit the full `net/http` request scaffolding: URL construction, body,
1600    /// headers, cookies, a no-redirect client, and `io.ReadAll` for the body.
1601    ///
1602    /// `bodyBytes` is always declared (with `_ = bodyBytes` to avoid the Go
1603    /// "declared and not used" compile error on tests with no body assertion).
1604    fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
1605        let method = ctx.method.to_uppercase();
1606        let path = ctx.path;
1607
1608        let _ = writeln!(out, "\tbaseURL := os.Getenv(\"MOCK_SERVER_URL\")");
1609        let _ = writeln!(out, "\tif baseURL == \"\" {{");
1610        let _ = writeln!(out, "\t\tbaseURL = \"http://localhost:8080\"");
1611        let _ = writeln!(out, "\t}}");
1612
1613        // Build request body expression.
1614        let body_expr = if let Some(body) = ctx.body {
1615            let json = serde_json::to_string(body).unwrap_or_default();
1616            let escaped = go_string_literal(&json);
1617            format!("strings.NewReader({})", escaped)
1618        } else {
1619            "strings.NewReader(\"\")".to_string()
1620        };
1621
1622        let _ = writeln!(out, "\tbody := {body_expr}");
1623        let _ = writeln!(
1624            out,
1625            "\treq, err := http.NewRequest(\"{method}\", baseURL+\"{path}\", body)"
1626        );
1627        let _ = writeln!(out, "\tif err != nil {{");
1628        let _ = writeln!(out, "\t\tt.Fatalf(\"new request failed: %v\", err)");
1629        let _ = writeln!(out, "\t}}");
1630
1631        // Content-Type header (only when a body is present).
1632        if ctx.body.is_some() {
1633            let content_type = ctx.content_type.unwrap_or("application/json");
1634            let _ = writeln!(out, "\treq.Header.Set(\"Content-Type\", \"{content_type}\")");
1635        }
1636
1637        // Explicit request headers (sorted for deterministic output).
1638        let mut header_names: Vec<&String> = ctx.headers.keys().collect();
1639        header_names.sort();
1640        for name in header_names {
1641            let value = &ctx.headers[name];
1642            let escaped_name = go_string_literal(name);
1643            let escaped_value = go_string_literal(value);
1644            let _ = writeln!(out, "\treq.Header.Set({escaped_name}, {escaped_value})");
1645        }
1646
1647        // Cookies.
1648        if !ctx.cookies.is_empty() {
1649            let mut cookie_names: Vec<&String> = ctx.cookies.keys().collect();
1650            cookie_names.sort();
1651            for name in cookie_names {
1652                let value = &ctx.cookies[name];
1653                let escaped_name = go_string_literal(name);
1654                let escaped_value = go_string_literal(value);
1655                let _ = writeln!(
1656                    out,
1657                    "\treq.AddCookie(&http.Cookie{{Name: {escaped_name}, Value: {escaped_value}}})"
1658                );
1659            }
1660        }
1661
1662        // No-redirect client so 3xx fixtures assert the redirect response itself.
1663        let _ = writeln!(out, "\tnoRedirectClient := &http.Client{{");
1664        let _ = writeln!(
1665            out,
1666            "\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {{"
1667        );
1668        let _ = writeln!(out, "\t\t\treturn http.ErrUseLastResponse");
1669        let _ = writeln!(out, "\t\t}},");
1670        let _ = writeln!(out, "\t}}");
1671        let _ = writeln!(out, "\tresp, err := noRedirectClient.Do(req)");
1672        let _ = writeln!(out, "\tif err != nil {{");
1673        let _ = writeln!(out, "\t\tt.Fatalf(\"request failed: %v\", err)");
1674        let _ = writeln!(out, "\t}}");
1675        let _ = writeln!(out, "\tdefer resp.Body.Close()");
1676
1677        // Always read the response body so body-assertion methods can reference
1678        // `bodyBytes`. Suppress the "declared and not used" compile error with
1679        // `_ = bodyBytes` for tests that have no body assertion.
1680        let _ = writeln!(out, "\tbodyBytes, err := io.ReadAll(resp.Body)");
1681        let _ = writeln!(out, "\tif err != nil {{");
1682        let _ = writeln!(out, "\t\tt.Fatalf(\"read body failed: %v\", err)");
1683        let _ = writeln!(out, "\t}}");
1684        let _ = writeln!(out, "\t_ = bodyBytes");
1685    }
1686
1687    fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
1688        let _ = writeln!(out, "\tif resp.StatusCode != {status} {{");
1689        let _ = writeln!(out, "\t\tt.Fatalf(\"status: got %d want {status}\", resp.StatusCode)");
1690        let _ = writeln!(out, "\t}}");
1691    }
1692
1693    /// Emit a header assertion, skipping special tokens (`<<present>>`, `<<absent>>`,
1694    /// `<<uuid>>`) and hop-by-hop headers (`Connection`) that `net/http` strips.
1695    fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
1696        // Skip special-token assertions.
1697        if matches!(expected, "<<absent>>" | "<<present>>" | "<<uuid>>") {
1698            return;
1699        }
1700        // Connection is a hop-by-hop header that Go's net/http strips.
1701        if name.eq_ignore_ascii_case("connection") {
1702            return;
1703        }
1704        let escaped_name = go_string_literal(name);
1705        let escaped_value = go_string_literal(expected);
1706        let _ = writeln!(
1707            out,
1708            "\tif !strings.Contains(resp.Header.Get({escaped_name}), {escaped_value}) {{"
1709        );
1710        let _ = writeln!(
1711            out,
1712            "\t\tt.Fatalf(\"header %s mismatch: got %q want to contain %q\", {escaped_name}, resp.Header.Get({escaped_name}), {escaped_value})"
1713        );
1714        let _ = writeln!(out, "\t}}");
1715    }
1716
1717    /// Emit an exact-equality body assertion.
1718    ///
1719    /// JSON objects and arrays are round-tripped via `json.Unmarshal` + `reflect.DeepEqual`.
1720    /// Scalar values are compared as trimmed strings.
1721    fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1722        match expected {
1723            serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
1724                let json_str = serde_json::to_string(expected).unwrap_or_default();
1725                let escaped = go_string_literal(&json_str);
1726                let _ = writeln!(out, "\tvar got any");
1727                let _ = writeln!(out, "\tvar want any");
1728                let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &got); err != nil {{");
1729                let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal got: %v\", err)");
1730                let _ = writeln!(out, "\t}}");
1731                let _ = writeln!(
1732                    out,
1733                    "\tif err := json.Unmarshal([]byte({escaped}), &want); err != nil {{"
1734                );
1735                let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal want: %v\", err)");
1736                let _ = writeln!(out, "\t}}");
1737                let _ = writeln!(out, "\tif !reflect.DeepEqual(got, want) {{");
1738                let _ = writeln!(out, "\t\tt.Fatalf(\"body mismatch: got %v want %v\", got, want)");
1739                let _ = writeln!(out, "\t}}");
1740            }
1741            serde_json::Value::String(s) => {
1742                let escaped = go_string_literal(s);
1743                let _ = writeln!(out, "\twant := {escaped}");
1744                let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1745                let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1746                let _ = writeln!(out, "\t}}");
1747            }
1748            other => {
1749                let escaped = go_string_literal(&other.to_string());
1750                let _ = writeln!(out, "\twant := {escaped}");
1751                let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1752                let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1753                let _ = writeln!(out, "\t}}");
1754            }
1755        }
1756    }
1757
1758    /// Emit partial-body assertions: every key in `expected` must appear in the
1759    /// parsed JSON response with the matching value.
1760    fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1761        if let Some(obj) = expected.as_object() {
1762            let _ = writeln!(out, "\tvar _partialGot map[string]any");
1763            let _ = writeln!(
1764                out,
1765                "\tif err := json.Unmarshal(bodyBytes, &_partialGot); err != nil {{"
1766            );
1767            let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal partial: %v\", err)");
1768            let _ = writeln!(out, "\t}}");
1769            for (key, val) in obj {
1770                let escaped_key = go_string_literal(key);
1771                let json_val = serde_json::to_string(val).unwrap_or_default();
1772                let escaped_val = go_string_literal(&json_val);
1773                let _ = writeln!(out, "\t{{");
1774                let _ = writeln!(out, "\t\tvar _wantVal any");
1775                let _ = writeln!(
1776                    out,
1777                    "\t\tif err := json.Unmarshal([]byte({escaped_val}), &_wantVal); err != nil {{"
1778                );
1779                let _ = writeln!(out, "\t\t\tt.Fatalf(\"json unmarshal partial want: %v\", err)");
1780                let _ = writeln!(out, "\t\t}}");
1781                let _ = writeln!(
1782                    out,
1783                    "\t\tif !reflect.DeepEqual(_partialGot[{escaped_key}], _wantVal) {{"
1784                );
1785                let _ = writeln!(
1786                    out,
1787                    "\t\t\tt.Fatalf(\"partial body field {key}: got %v want %v\", _partialGot[{escaped_key}], _wantVal)"
1788                );
1789                let _ = writeln!(out, "\t\t}}");
1790                let _ = writeln!(out, "\t}}");
1791            }
1792        }
1793    }
1794
1795    /// Emit validation-error assertions for 422 responses.
1796    ///
1797    /// Checks that each expected `msg` appears in at least one element of the
1798    /// parsed body's `"errors"` array.
1799    fn render_assert_validation_errors(
1800        &self,
1801        out: &mut String,
1802        _response_var: &str,
1803        errors: &[ValidationErrorExpectation],
1804    ) {
1805        let _ = writeln!(out, "\tvar _veBody map[string]any");
1806        let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &_veBody); err != nil {{");
1807        let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal validation errors: %v\", err)");
1808        let _ = writeln!(out, "\t}}");
1809        let _ = writeln!(out, "\t_veErrors, _ := _veBody[\"errors\"].([]any)");
1810        for ve in errors {
1811            let escaped_msg = go_string_literal(&ve.msg);
1812            let _ = writeln!(out, "\t{{");
1813            let _ = writeln!(out, "\t\t_found := false");
1814            let _ = writeln!(out, "\t\tfor _, _e := range _veErrors {{");
1815            let _ = writeln!(out, "\t\t\tif _em, ok := _e.(map[string]any); ok {{");
1816            let _ = writeln!(
1817                out,
1818                "\t\t\t\tif _msg, ok := _em[\"msg\"].(string); ok && strings.Contains(_msg, {escaped_msg}) {{"
1819            );
1820            let _ = writeln!(out, "\t\t\t\t\t_found = true");
1821            let _ = writeln!(out, "\t\t\t\t\tbreak");
1822            let _ = writeln!(out, "\t\t\t\t}}");
1823            let _ = writeln!(out, "\t\t\t}}");
1824            let _ = writeln!(out, "\t\t}}");
1825            let _ = writeln!(out, "\t\tif !_found {{");
1826            let _ = writeln!(
1827                out,
1828                "\t\t\tt.Fatalf(\"validation error with msg containing %q not found in errors\", {escaped_msg})"
1829            );
1830            let _ = writeln!(out, "\t\t}}");
1831            let _ = writeln!(out, "\t}}");
1832        }
1833    }
1834}
1835
1836/// Build setup lines (e.g. handle creation) and the argument list for the function call.
1837///
1838/// Returns `(setup_lines, args_string)`.
1839///
1840/// `options_ptr` — when `true`, `json_object` args with an `options_type` are
1841/// passed as a Go pointer (`*OptionsType`): absent/empty → `nil`, present →
1842/// `&varName` after JSON unmarshal.
1843#[allow(clippy::too_many_arguments)]
1844fn build_args_and_setup(
1845    input: &serde_json::Value,
1846    args: &[crate::config::ArgMapping],
1847    import_alias: &str,
1848    options_type: Option<&str>,
1849    fixture: &crate::fixture::Fixture,
1850    options_ptr: bool,
1851    expects_error: bool,
1852    data_enum_names: &std::collections::HashSet<&str>,
1853) -> (Vec<String>, String) {
1854    let fixture_id = &fixture.id;
1855    use heck::ToUpperCamelCase;
1856
1857    if args.is_empty() {
1858        return (Vec::new(), String::new());
1859    }
1860
1861    let mut setup_lines: Vec<String> = Vec::new();
1862    let mut parts: Vec<String> = Vec::new();
1863
1864    for arg in args {
1865        if arg.arg_type == "mock_url" {
1866            if fixture.has_host_root_route() {
1867                let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1868                setup_lines.push(format!("{} := os.Getenv(\"{env_key}\")", arg.name));
1869                setup_lines.push(format!(
1870                    "if {} == \"\" {{ {} = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\" }}",
1871                    arg.name, arg.name
1872                ));
1873            } else {
1874                setup_lines.push(format!(
1875                    "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
1876                    arg.name,
1877                ));
1878            }
1879            parts.push(arg.name.clone());
1880            continue;
1881        }
1882
1883        if arg.arg_type == "mock_url_list" {
1884            // []string of URLs: each element is either a bare path (`/seed1`) — prefixed
1885            // with the per-fixture mock-server URL at runtime — or an absolute URL kept as-is.
1886            // Mirrors the `mock_url` resolution: `MOCK_SERVER_<FIXTURE_ID>` first, then
1887            // `MOCK_SERVER_URL/fixtures/<id>`.
1888            let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1889            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1890            let val = input.get(field).unwrap_or(&serde_json::Value::Null);
1891
1892            let paths: Vec<String> = if let Some(arr) = val.as_array() {
1893                arr.iter().filter_map(|v| v.as_str().map(go_string_literal)).collect()
1894            } else {
1895                Vec::new()
1896            };
1897
1898            let paths_literal = paths.join(", ");
1899            let var_name = &arg.name;
1900
1901            setup_lines.push(format!(
1902                "{var_name}Base := os.Getenv(\"{env_key}\")\n\tif {var_name}Base == \"\" {{\n\t\t{var_name}Base = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"\n\t}}"
1903            ));
1904            setup_lines.push(format!(
1905                "var {var_name} []string\n\tfor _, p := range []string{{{paths_literal}}} {{\n\t\tif strings.HasPrefix(p, \"http\") {{\n\t\t\t{var_name} = append({var_name}, p)\n\t\t}} else {{\n\t\t\t{var_name} = append({var_name}, {var_name}Base + p)\n\t\t}}\n\t}}"
1906            ));
1907            parts.push(var_name.to_string());
1908            continue;
1909        }
1910
1911        if arg.arg_type == "handle" {
1912            // Generate a CreateEngine (or equivalent) call and pass the variable.
1913            let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
1914            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1915            let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
1916            // When the fixture expects an error (validation test), engine creation
1917            // is the error source. Assert the error and return so the test passes
1918            // without proceeding to the (unreachable) function call.
1919            let create_err_handler = if expects_error {
1920                "assert.Error(t, createErr)\n\t\treturn".to_string()
1921            } else {
1922                "t.Fatalf(\"create handle failed: %v\", createErr)".to_string()
1923            };
1924            if config_value.is_null()
1925                || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
1926            {
1927                setup_lines.push(format!(
1928                    "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}",
1929                    name = arg.name,
1930                ));
1931            } else {
1932                let json_str = serde_json::to_string(config_value).unwrap_or_default();
1933                let go_literal = go_string_literal(&json_str);
1934                let name = &arg.name;
1935                setup_lines.push(format!(
1936                    "var {name}Config {import_alias}.CrawlConfig\n\tif err := json.Unmarshal([]byte({go_literal}), &{name}Config); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
1937                ));
1938                setup_lines.push(format!(
1939                    "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}"
1940                ));
1941            }
1942            parts.push(arg.name.clone());
1943            continue;
1944        }
1945
1946        let val: Option<&serde_json::Value> = if arg.field == "input" {
1947            Some(input)
1948        } else {
1949            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1950            input.get(field)
1951        };
1952
1953        // file_path args are fixture-relative paths under `test_documents/`. The Go test
1954        // runner's TestMain (in main_test.go) already does `os.Chdir(test_documents)` so
1955        // tests can pass these relative strings directly; no additional resolution needed.
1956
1957        // Handle bytes type: fixture stores base64-encoded bytes.
1958        // Emit a Go base64.StdEncoding.DecodeString call to decode at runtime.
1959        if arg.arg_type == "bytes" {
1960            let var_name = format!("{}Bytes", arg.name);
1961            match val {
1962                None | Some(serde_json::Value::Null) => {
1963                    if arg.optional {
1964                        parts.push("nil".to_string());
1965                    } else {
1966                        parts.push("[]byte{}".to_string());
1967                    }
1968                }
1969                Some(serde_json::Value::String(s)) => {
1970                    // Bytes args whose fixture value is a string are fixture-relative paths into
1971                    // the repo-root `test_documents/` directory (matching the rust e2e codegen
1972                    // convention). The Go test runner's TestMain chdirs into test_documents/, so
1973                    // a bare relative path resolves correctly via os.ReadFile.
1974                    let go_path = go_string_literal(s);
1975                    setup_lines.push(format!(
1976                        "{var_name}, {var_name}Err := os.ReadFile({go_path})\n\tif {var_name}Err != nil {{\n\t\tt.Fatalf(\"read fixture {s}: %v\", {var_name}Err)\n\t}}"
1977                    ));
1978                    parts.push(var_name);
1979                }
1980                Some(other) => {
1981                    parts.push(format!("[]byte({})", json_to_go(other)));
1982                }
1983            }
1984            continue;
1985        }
1986
1987        match val {
1988            None | Some(serde_json::Value::Null) if arg.optional => {
1989                // Optional arg absent: emit Go zero/nil for the type.
1990                match arg.arg_type.as_str() {
1991                    "string" => {
1992                        // Optional string in Go bindings is *string → nil.
1993                        parts.push("nil".to_string());
1994                    }
1995                    "json_object" => {
1996                        if options_ptr {
1997                            // Pointer options type (*OptionsType): absent → nil.
1998                            parts.push("nil".to_string());
1999                        } else if let Some(opts_type) = options_type {
2000                            // Value options type: zero-value struct.
2001                            parts.push(format!("{import_alias}.{opts_type}{{}}"));
2002                        } else {
2003                            parts.push("nil".to_string());
2004                        }
2005                    }
2006                    _ => {
2007                        parts.push("nil".to_string());
2008                    }
2009                }
2010            }
2011            None | Some(serde_json::Value::Null) => {
2012                // Required arg with no fixture value: pass a language-appropriate default.
2013                let default_val = match arg.arg_type.as_str() {
2014                    "string" => "\"\"".to_string(),
2015                    "int" | "integer" | "i64" => "0".to_string(),
2016                    "float" | "number" => "0.0".to_string(),
2017                    "bool" | "boolean" => "false".to_string(),
2018                    "json_object" => {
2019                        if options_ptr {
2020                            // Pointer options type (*OptionsType): absent → nil.
2021                            "nil".to_string()
2022                        } else if let Some(opts_type) = options_type {
2023                            format!("{import_alias}.{opts_type}{{}}")
2024                        } else {
2025                            "nil".to_string()
2026                        }
2027                    }
2028                    _ => "nil".to_string(),
2029                };
2030                parts.push(default_val);
2031            }
2032            Some(v) => {
2033                match arg.arg_type.as_str() {
2034                    "json_object" => {
2035                        // JSON arrays unmarshal into []string (Go slices).
2036                        // JSON objects with a known options_type unmarshal into that type.
2037                        let is_array = v.is_array();
2038                        let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
2039                        if is_empty_obj {
2040                            if options_ptr {
2041                                // Pointer options type: empty object → nil.
2042                                parts.push("nil".to_string());
2043                            } else if let Some(opts_type) = options_type {
2044                                parts.push(format!("{import_alias}.{opts_type}{{}}"));
2045                            } else {
2046                                parts.push("nil".to_string());
2047                            }
2048                        } else if is_array {
2049                            // Array type — unmarshal into a Go slice. Honor `go_type` for a
2050                            // fully explicit Go type (e.g. `"kreuzberg.BatchBytesItem"`), fall
2051                            // back to deriving the slice type from `element_type`, defaulting
2052                            // to `[]string` for unknown types.
2053                            let go_slice_type = if let Some(go_t) = arg.go_type.as_deref() {
2054                                // go_type is the slice element type — wrap it in [].
2055                                // If it already starts with '[' the user specified the full
2056                                // slice type; use it verbatim.
2057                                if go_t.starts_with('[') {
2058                                    go_t.to_string()
2059                                } else {
2060                                    // Qualify unqualified types (e.g., "BatchBytesItem" → "kreuzberg.BatchBytesItem")
2061                                    let qualified = if go_t.contains('.') {
2062                                        go_t.to_string()
2063                                    } else {
2064                                        format!("{import_alias}.{go_t}")
2065                                    };
2066                                    format!("[]{qualified}")
2067                                }
2068                            } else {
2069                                element_type_to_go_slice(arg.element_type.as_deref(), import_alias)
2070                            };
2071
2072                            // Extract the element type (unqualified) from go_type or element_type to check if it's a sum-type.
2073                            let element_type_name = if let Some(go_t) = arg.go_type.as_deref() {
2074                                // go_type is the element type — extract the unqualified name.
2075                                if go_t.starts_with('[') {
2076                                    None // Full slice type specified; can't extract element
2077                                } else if let Some(idx) = go_t.rfind('.') {
2078                                    Some(&go_t[idx + 1..]) // Strip package prefix
2079                                } else {
2080                                    Some(go_t)
2081                                }
2082                            } else {
2083                                arg.element_type.as_deref()
2084                            };
2085
2086                            let is_sum_type = element_type_name.is_some_and(|et| data_enum_names.contains(et));
2087
2088                            // Convert JSON for Go compatibility (e.g., byte arrays → base64 strings)
2089                            let converted_v = convert_json_for_go(v.clone());
2090                            let var_name = &arg.name;
2091
2092                            if is_sum_type {
2093                                // For sum-types, unmarshal each element via Unmarshal<Type> discriminator.
2094                                let element_type = element_type_name.unwrap();
2095                                let json_str = serde_json::to_string(&converted_v).unwrap_or_default();
2096                                let go_literal = go_string_literal(&json_str);
2097                                setup_lines.push(format!(
2098                                    "var {var_name}Raw []json.RawMessage\n\tif err := json.Unmarshal([]byte({go_literal}), &{var_name}Raw); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
2099                                ));
2100                                setup_lines.push(format!(
2101                                    "var {var_name} {go_slice_type}\n\tfor _, raw := range {var_name}Raw {{\n\t\telem, err := {import_alias}.Unmarshal{element_type}(raw)\n\t\tif err != nil {{\n\t\t\tt.Fatalf(\"unmarshal {element_type} failed: %v\", err)\n\t\t}}\n\t\t{var_name} = append({var_name}, elem)\n\t}}"
2102                                ));
2103                            } else {
2104                                // For non-sum-type arrays, use standard json.Unmarshal.
2105                                let json_str = serde_json::to_string(&converted_v).unwrap_or_default();
2106                                let go_literal = go_string_literal(&json_str);
2107                                setup_lines.push(format!(
2108                                    "var {var_name} {go_slice_type}\n\tif err := json.Unmarshal([]byte({go_literal}), &{var_name}); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
2109                                ));
2110                            }
2111                            parts.push(var_name.to_string());
2112                        } else if let Some(opts_type) = options_type {
2113                            // Object with known type — unmarshal into typed struct.
2114                            // When options_ptr is set, the Go struct uses snake_case JSON
2115                            // field tags and lowercase/snake_case enum values.  Remap the
2116                            // fixture's camelCase keys and PascalCase enum string values.
2117                            let remapped_v = if options_ptr {
2118                                convert_json_for_go(v.clone())
2119                            } else {
2120                                v.clone()
2121                            };
2122                            let json_str = serde_json::to_string(&remapped_v).unwrap_or_default();
2123                            let go_literal = go_string_literal(&json_str);
2124                            let var_name = &arg.name;
2125                            setup_lines.push(format!(
2126                                "var {var_name} {import_alias}.{opts_type}\n\tif err := json.Unmarshal([]byte({go_literal}), &{var_name}); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
2127                            ));
2128                            // Pass as pointer when options_ptr is set.
2129                            let arg_expr = if options_ptr {
2130                                format!("&{var_name}")
2131                            } else {
2132                                var_name.to_string()
2133                            };
2134                            parts.push(arg_expr);
2135                        } else {
2136                            parts.push(json_to_go(v));
2137                        }
2138                    }
2139                    "string" if arg.optional => {
2140                        // Optional string in Go is *string — take address of a local.
2141                        let var_name = format!("{}Val", arg.name);
2142                        let go_val = json_to_go(v);
2143                        setup_lines.push(format!("{var_name} := {go_val}"));
2144                        parts.push(format!("&{var_name}"));
2145                    }
2146                    _ => {
2147                        parts.push(json_to_go(v));
2148                    }
2149                }
2150            }
2151        }
2152    }
2153
2154    (setup_lines, parts.join(", "))
2155}
2156
2157#[allow(clippy::too_many_arguments)]
2158fn render_assertion(
2159    out: &mut String,
2160    assertion: &Assertion,
2161    result_var: &str,
2162    import_alias: &str,
2163    field_resolver: &FieldResolver,
2164    optional_locals: &std::collections::HashMap<String, String>,
2165    result_is_simple: bool,
2166    result_is_array: bool,
2167    is_streaming: bool,
2168) {
2169    // Handle synthetic / derived fields before the is_valid_for_result check
2170    // so they are never treated as struct field accesses on the result.
2171    if !result_is_simple {
2172        if let Some(f) = &assertion.field {
2173            // embed_texts returns *[][]float32; the embedding matrix is *result_var.
2174            // We emit inline func() expressions so we don't need additional variables.
2175            let embed_deref = format!("(*{result_var})");
2176            match f.as_str() {
2177                "chunks_have_content" => {
2178                    let pred = format!(
2179                        "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range chunks {{ if c.Content == \"\" {{ return false }} }}; return true }}()"
2180                    );
2181                    match assertion.assertion_type.as_str() {
2182                        "is_true" => {
2183                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2184                        }
2185                        "is_false" => {
2186                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2187                        }
2188                        _ => {
2189                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2190                        }
2191                    }
2192                    return;
2193                }
2194                "chunks_have_embeddings" => {
2195                    let pred = format!(
2196                        "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range chunks {{ if c.Embedding == nil || len(*c.Embedding) == 0 {{ return false }} }}; return true }}()"
2197                    );
2198                    match assertion.assertion_type.as_str() {
2199                        "is_true" => {
2200                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2201                        }
2202                        "is_false" => {
2203                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2204                        }
2205                        _ => {
2206                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2207                        }
2208                    }
2209                    return;
2210                }
2211                "chunks_have_heading_context" => {
2212                    let pred = format!(
2213                        "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range chunks {{ if c.Metadata.HeadingContext == nil {{ return false }} }}; return true }}()"
2214                    );
2215                    match assertion.assertion_type.as_str() {
2216                        "is_true" => {
2217                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2218                        }
2219                        "is_false" => {
2220                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2221                        }
2222                        _ => {
2223                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2224                        }
2225                    }
2226                    return;
2227                }
2228                "first_chunk_starts_with_heading" => {
2229                    let pred = format!(
2230                        "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil || len(chunks) == 0 {{ return false }}; return chunks[0].Metadata.HeadingContext != nil }}()"
2231                    );
2232                    match assertion.assertion_type.as_str() {
2233                        "is_true" => {
2234                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2235                        }
2236                        "is_false" => {
2237                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2238                        }
2239                        _ => {
2240                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2241                        }
2242                    }
2243                    return;
2244                }
2245                "embeddings" => {
2246                    match assertion.assertion_type.as_str() {
2247                        "count_equals" => {
2248                            if let Some(val) = &assertion.value {
2249                                if let Some(n) = val.as_u64() {
2250                                    let _ = writeln!(
2251                                        out,
2252                                        "\tassert.Equal(t, {n}, len({embed_deref}), \"expected exactly {n} elements\")"
2253                                    );
2254                                }
2255                            }
2256                        }
2257                        "count_min" => {
2258                            if let Some(val) = &assertion.value {
2259                                if let Some(n) = val.as_u64() {
2260                                    let _ = writeln!(
2261                                        out,
2262                                        "\tassert.GreaterOrEqual(t, len({embed_deref}), {n}, \"expected at least {n} elements\")"
2263                                    );
2264                                }
2265                            }
2266                        }
2267                        "not_empty" => {
2268                            let _ = writeln!(
2269                                out,
2270                                "\tassert.NotEmpty(t, {embed_deref}, \"expected non-empty embeddings\")"
2271                            );
2272                        }
2273                        "is_empty" => {
2274                            let _ = writeln!(out, "\tassert.Empty(t, {embed_deref}, \"expected empty embeddings\")");
2275                        }
2276                        _ => {
2277                            let _ = writeln!(
2278                                out,
2279                                "\t// skipped: unsupported assertion type on synthetic field 'embeddings'"
2280                            );
2281                        }
2282                    }
2283                    return;
2284                }
2285                "embedding_dimensions" => {
2286                    let expr = format!(
2287                        "func() int {{ if len({embed_deref}) == 0 {{ return 0 }}; return len({embed_deref}[0]) }}()"
2288                    );
2289                    match assertion.assertion_type.as_str() {
2290                        "equals" => {
2291                            if let Some(val) = &assertion.value {
2292                                if let Some(n) = val.as_u64() {
2293                                    let _ = writeln!(
2294                                        out,
2295                                        "\tif {expr} != {n} {{\n\t\tt.Errorf(\"equals mismatch: got %v\", {expr})\n\t}}"
2296                                    );
2297                                }
2298                            }
2299                        }
2300                        "greater_than" => {
2301                            if let Some(val) = &assertion.value {
2302                                if let Some(n) = val.as_u64() {
2303                                    let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2304                                }
2305                            }
2306                        }
2307                        _ => {
2308                            let _ = writeln!(
2309                                out,
2310                                "\t// skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
2311                            );
2312                        }
2313                    }
2314                    return;
2315                }
2316                "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
2317                    let pred = match f.as_str() {
2318                        "embeddings_valid" => {
2319                            format!(
2320                                "func() bool {{ for _, e := range {embed_deref} {{ if len(e) == 0 {{ return false }} }}; return true }}()"
2321                            )
2322                        }
2323                        "embeddings_finite" => {
2324                            format!(
2325                                "func() bool {{ for _, e := range {embed_deref} {{ for _, v := range e {{ if v != v || v == float32(1.0/0.0) || v == float32(-1.0/0.0) {{ return false }} }} }}; return true }}()"
2326                            )
2327                        }
2328                        "embeddings_non_zero" => {
2329                            format!(
2330                                "func() bool {{ for _, e := range {embed_deref} {{ hasNonZero := false; for _, v := range e {{ if v != 0 {{ hasNonZero = true; break }} }}; if !hasNonZero {{ return false }} }}; return true }}()"
2331                            )
2332                        }
2333                        "embeddings_normalized" => {
2334                            format!(
2335                                "func() bool {{ for _, e := range {embed_deref} {{ var n float64; for _, v := range e {{ n += float64(v) * float64(v) }}; if n < 0.999 || n > 1.001 {{ return false }} }}; return true }}()"
2336                            )
2337                        }
2338                        _ => unreachable!(),
2339                    };
2340                    match assertion.assertion_type.as_str() {
2341                        "is_true" => {
2342                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2343                        }
2344                        "is_false" => {
2345                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2346                        }
2347                        _ => {
2348                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2349                        }
2350                    }
2351                    return;
2352                }
2353                // ---- keywords / keywords_count ----
2354                // Go ExtractionResult does not expose extracted_keywords; skip.
2355                "keywords" | "keywords_count" => {
2356                    let _ = writeln!(out, "\t// skipped: field '{f}' not available on Go ExtractionResult");
2357                    return;
2358                }
2359                _ => {}
2360            }
2361        }
2362    }
2363
2364    // Streaming virtual fields: intercept before is_valid_for_result so they are
2365    // never skipped.  These fields resolve against the `chunks` collected-list variable.
2366    // Skip the streaming interception entirely when the call has opted out
2367    // (`[e2e.calls.<name>] streaming = false`) — `chunks` then names a plain
2368    // field on the synchronous result struct and must flow through normal
2369    // accessor resolution (e.g. `result.Chunks`).
2370    if !result_is_simple && is_streaming {
2371        if let Some(f) = &assertion.field {
2372            if !f.is_empty() && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
2373                if let Some(expr) =
2374                    crate::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, "go", "chunks")
2375                {
2376                    match assertion.assertion_type.as_str() {
2377                        "count_min" => {
2378                            if let Some(val) = &assertion.value {
2379                                if let Some(n) = val.as_u64() {
2380                                    let _ = writeln!(
2381                                        out,
2382                                        "\tassert.GreaterOrEqual(t, len({expr}), {n}, \"expected >= {n} chunks\")"
2383                                    );
2384                                }
2385                            }
2386                        }
2387                        "count_equals" => {
2388                            if let Some(val) = &assertion.value {
2389                                if let Some(n) = val.as_u64() {
2390                                    let _ = writeln!(
2391                                        out,
2392                                        "\tassert.Equal(t, {n}, len({expr}), \"expected exactly {n} chunks\")"
2393                                    );
2394                                }
2395                            }
2396                        }
2397                        "equals" => {
2398                            if let Some(serde_json::Value::String(s)) = &assertion.value {
2399                                let escaped = crate::escape::go_string_literal(s);
2400                                // Deep-path streaming-virtual fields like `tool_calls[0].function.name`
2401                                // resolve to pointer-typed Go fields (`*string`). The flat virtual
2402                                // accessors `stream_content` / `finish_reason` already return `string`.
2403                                // Wrap only the deep-path case in a safe-deref IIFE.
2404                                let is_deep_path = f.contains('.') || f.contains('[');
2405                                let safe_expr = if is_deep_path {
2406                                    format!(
2407                                        "func() string {{ v := {expr}; if v == nil {{ return \"\" }}; return *v }}()"
2408                                    )
2409                                } else {
2410                                    expr.clone()
2411                                };
2412                                let _ = writeln!(out, "\tassert.Equal(t, {escaped}, {safe_expr})");
2413                            } else if let Some(val) = &assertion.value {
2414                                if let Some(n) = val.as_u64() {
2415                                    let _ = writeln!(out, "\tassert.Equal(t, {n}, {expr})");
2416                                }
2417                            }
2418                        }
2419                        "not_empty" => {
2420                            let _ = writeln!(out, "\tassert.NotEmpty(t, {expr}, \"expected non-empty\")");
2421                        }
2422                        "is_empty" => {
2423                            let _ = writeln!(out, "\tassert.Empty(t, {expr}, \"expected empty\")");
2424                        }
2425                        "is_true" => {
2426                            let _ = writeln!(out, "\tassert.True(t, {expr}, \"expected true\")");
2427                        }
2428                        "is_false" => {
2429                            let _ = writeln!(out, "\tassert.False(t, {expr}, \"expected false\")");
2430                        }
2431                        "greater_than" => {
2432                            if let Some(val) = &assertion.value {
2433                                if let Some(n) = val.as_u64() {
2434                                    let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2435                                }
2436                            }
2437                        }
2438                        "greater_than_or_equal" => {
2439                            if let Some(val) = &assertion.value {
2440                                if let Some(n) = val.as_u64() {
2441                                    let _ =
2442                                        writeln!(out, "\tassert.GreaterOrEqual(t, {expr}, {n}, \"expected >= {n}\")");
2443                                }
2444                            }
2445                        }
2446                        "contains" => {
2447                            if let Some(serde_json::Value::String(s)) = &assertion.value {
2448                                let escaped = crate::escape::go_string_literal(s);
2449                                let _ =
2450                                    writeln!(out, "\tassert.Contains(t, {expr}, {escaped}, \"expected to contain\")");
2451                            }
2452                        }
2453                        _ => {
2454                            let _ = writeln!(
2455                                out,
2456                                "\t// streaming field '{f}': assertion type '{}' not rendered",
2457                                assertion.assertion_type
2458                            );
2459                        }
2460                    }
2461                }
2462                return;
2463            }
2464        }
2465    }
2466
2467    // Skip assertions on fields that don't exist on the result type.
2468    // When result_is_simple, all field assertions operate on the scalar result directly.
2469    if !result_is_simple {
2470        if let Some(f) = &assertion.field {
2471            if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
2472                let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
2473                return;
2474            }
2475        }
2476    }
2477
2478    let field_expr = if result_is_simple {
2479        // The result IS the value — field access is irrelevant.
2480        result_var.to_string()
2481    } else {
2482        match &assertion.field {
2483            Some(f) if !f.is_empty() => {
2484                // Use the local variable if the field was dereferenced above.
2485                if let Some(local_var) = optional_locals.get(f.as_str()) {
2486                    local_var.clone()
2487                } else {
2488                    field_resolver.accessor(f, "go", result_var)
2489                }
2490            }
2491            _ => result_var.to_string(),
2492        }
2493    };
2494
2495    // Check if the field (after resolution) is optional, which means it's a pointer in Go.
2496    // Also check if a `.length` suffix's parent is optional (e.g., metadata.headings.length
2497    // where metadata.headings is optional → len() needs dereference).
2498    let is_optional = assertion
2499        .field
2500        .as_ref()
2501        .map(|f| {
2502            let resolved = field_resolver.resolve(f);
2503            let check_path = resolved
2504                .strip_suffix(".length")
2505                .or_else(|| resolved.strip_suffix(".count"))
2506                .or_else(|| resolved.strip_suffix(".size"))
2507                .unwrap_or(resolved);
2508            field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
2509        })
2510        .unwrap_or(false);
2511
2512    // When field_expr is `len(X)` and X is an optional (pointer) field, rewrite to `len(*X)`
2513    // and we'll wrap with a nil guard in the assertion handlers.
2514    // However, slices are already nil-able and should not be dereferenced.
2515    let field_is_array_for_len = assertion
2516        .field
2517        .as_ref()
2518        .map(|f| {
2519            let resolved = field_resolver.resolve(f);
2520            let check_path = resolved
2521                .strip_suffix(".length")
2522                .or_else(|| resolved.strip_suffix(".count"))
2523                .or_else(|| resolved.strip_suffix(".size"))
2524                .unwrap_or(resolved);
2525            field_resolver.is_array(check_path)
2526        })
2527        .unwrap_or(false);
2528    let field_expr =
2529        if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') && !field_is_array_for_len {
2530            let inner = &field_expr[4..field_expr.len() - 1];
2531            format!("len(*{inner})")
2532        } else {
2533            field_expr
2534        };
2535    // Build the nil-guard expression for the inner pointer (without len wrapper).
2536    let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
2537        Some(field_expr[5..field_expr.len() - 1].to_string())
2538    } else {
2539        None
2540    };
2541
2542    // For optional non-string fields that weren't dereferenced into locals,
2543    // we need to dereference the pointer in comparisons.
2544    // However, slices are already nil-able and should not be dereferenced.
2545    let field_is_slice = assertion
2546        .field
2547        .as_ref()
2548        .map(|f| field_resolver.is_array(field_resolver.resolve(f)))
2549        .unwrap_or(false);
2550    let deref_field_expr = if is_optional && !field_expr.starts_with("len(") && !field_is_slice {
2551        format!("*{field_expr}")
2552    } else {
2553        field_expr.clone()
2554    };
2555
2556    // Detect array element access (e.g., `result.Assets[0].ContentHash`).
2557    // When the field_expr contains `[0]`, we must guard against an out-of-bounds
2558    // panic by checking that the array is non-empty first.
2559    // Extract the array slice expression (everything before `[0]`).
2560    let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
2561        let mut array_expr = field_expr[..idx].to_string();
2562        if let Some(stripped) = array_expr.strip_prefix("len(") {
2563            array_expr = stripped.to_string();
2564        }
2565        Some(array_expr)
2566    } else {
2567        None
2568    };
2569
2570    // Render the assertion into a temporary buffer first, then wrap with the array
2571    // bounds guard (if needed) by adding one extra level of indentation.
2572    let mut assertion_buf = String::new();
2573    let out_ref = &mut assertion_buf;
2574
2575    match assertion.assertion_type.as_str() {
2576        "equals" => {
2577            if let Some(expected) = &assertion.value {
2578                let go_val = json_to_go(expected);
2579                // For string equality, trim whitespace to handle trailing newlines from the converter.
2580                if expected.is_string() {
2581                    // Wrap field expression with strings.TrimSpace() for string comparisons.
2582                    // Use string() cast to handle named string types (e.g. BatchStatus, FinishReason).
2583                    // For complex types like FormatMetadata, use fmt.Sprintf to convert.
2584                    let resolved_name = assertion
2585                        .field
2586                        .as_ref()
2587                        .map(|f| field_resolver.resolve(f))
2588                        .unwrap_or_default();
2589                    let is_struct = resolved_name.contains("FormatMetadata");
2590                    let trimmed_field = if is_struct {
2591                        // Use fmt.Sprintf for struct types instead of string cast
2592                        if is_optional && !field_expr.starts_with("len(") {
2593                            format!("strings.TrimSpace(fmt.Sprintf(\"%v\", *{field_expr}))")
2594                        } else {
2595                            format!("strings.TrimSpace(fmt.Sprintf(\"%v\", {field_expr}))")
2596                        }
2597                    } else if is_optional && !field_expr.starts_with("len(") {
2598                        format!("strings.TrimSpace(string(*{field_expr}))")
2599                    } else {
2600                        format!("strings.TrimSpace(string({field_expr}))")
2601                    };
2602                    if is_optional && !field_expr.starts_with("len(") {
2603                        let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
2604                    } else {
2605                        let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
2606                    }
2607                } else if is_optional && !field_expr.starts_with("len(") {
2608                    let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
2609                } else {
2610                    let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
2611                }
2612                let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
2613                let _ = writeln!(out_ref, "\t}}");
2614            }
2615        }
2616        "contains" => {
2617            if let Some(expected) = &assertion.value {
2618                let go_val = json_to_go(expected);
2619                // Determine the "string view" of the field expression.
2620                // - []string (optional) → jsonString(field_expr) — Go slices are nil-able, no `*` needed
2621                // - *string → string(*field_expr)
2622                // - string → string(field_expr) (or just field_expr for plain strings)
2623                // - result_is_array (result_is_simple + array result) → jsonString(field_expr)
2624                let resolved_field = assertion.field.as_deref().unwrap_or("");
2625                let resolved_name = field_resolver.resolve(resolved_field);
2626                let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2627                let is_opt =
2628                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2629                let field_for_contains = if is_opt && field_is_array {
2630                    // Go slices are nil-able directly — no pointer dereference needed.
2631                    format!("jsonString({field_expr})")
2632                } else if is_opt {
2633                    format!("fmt.Sprint(*{field_expr})")
2634                } else if field_is_array {
2635                    format!("jsonString({field_expr})")
2636                } else {
2637                    format!("fmt.Sprint({field_expr})")
2638                };
2639                if is_opt {
2640                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2641                    let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2642                    let _ = writeln!(
2643                        out_ref,
2644                        "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2645                    );
2646                    let _ = writeln!(out_ref, "\t}}");
2647                    let _ = writeln!(out_ref, "\t}}");
2648                } else {
2649                    let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2650                    let _ = writeln!(
2651                        out_ref,
2652                        "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2653                    );
2654                    let _ = writeln!(out_ref, "\t}}");
2655                }
2656            }
2657        }
2658        "contains_all" => {
2659            if let Some(values) = &assertion.values {
2660                let resolved_field = assertion.field.as_deref().unwrap_or("");
2661                let resolved_name = field_resolver.resolve(resolved_field);
2662                let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2663                let is_opt =
2664                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2665                for val in values {
2666                    let go_val = json_to_go(val);
2667                    let field_for_contains = if is_opt && field_is_array {
2668                        // Go slices are nil-able directly — no pointer dereference needed.
2669                        format!("jsonString({field_expr})")
2670                    } else if is_opt {
2671                        format!("fmt.Sprint(*{field_expr})")
2672                    } else if field_is_array {
2673                        format!("jsonString({field_expr})")
2674                    } else {
2675                        format!("fmt.Sprint({field_expr})")
2676                    };
2677                    if is_opt {
2678                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2679                        let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2680                        let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2681                        let _ = writeln!(out_ref, "\t}}");
2682                        let _ = writeln!(out_ref, "\t}}");
2683                    } else {
2684                        let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2685                        let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2686                        let _ = writeln!(out_ref, "\t}}");
2687                    }
2688                }
2689            }
2690        }
2691        "not_contains" => {
2692            if let Some(expected) = &assertion.value {
2693                let go_val = json_to_go(expected);
2694                let resolved_field = assertion.field.as_deref().unwrap_or("");
2695                let resolved_name = field_resolver.resolve(resolved_field);
2696                let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2697                let is_opt =
2698                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2699                let field_for_contains = if is_opt && field_is_array {
2700                    // Go slices are nil-able directly — no pointer dereference needed.
2701                    format!("jsonString({field_expr})")
2702                } else if is_opt {
2703                    format!("fmt.Sprint(*{field_expr})")
2704                } else if field_is_array {
2705                    format!("jsonString({field_expr})")
2706                } else {
2707                    format!("fmt.Sprint({field_expr})")
2708                };
2709                let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
2710                let _ = writeln!(
2711                    out_ref,
2712                    "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
2713                );
2714                let _ = writeln!(out_ref, "\t}}");
2715            }
2716        }
2717        "not_empty" => {
2718            // For optional struct pointers (not arrays), just check != nil.
2719            // For optional slice/string pointers, check nil and len.
2720            let field_is_array = {
2721                let rf = assertion.field.as_deref().unwrap_or("");
2722                let rn = field_resolver.resolve(rf);
2723                field_resolver.is_array(rn)
2724            };
2725            if is_optional && !field_is_array {
2726                // Struct pointer: non-empty means not nil.
2727                let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
2728            } else if is_optional && field_is_slice {
2729                // Slice optional: Go slices are already nil-able — no dereference needed.
2730                let _ = writeln!(out_ref, "\tif {field_expr} == nil || len({field_expr}) == 0 {{");
2731            } else if is_optional {
2732                // Pointer-to-slice (*[]T): dereference then len.
2733                let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
2734            } else if result_is_simple && result_is_array {
2735                // Simple array result ([]T) — direct slice, not a pointer; check length only.
2736                let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2737            } else {
2738                let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2739            }
2740            let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
2741            let _ = writeln!(out_ref, "\t}}");
2742        }
2743        "is_empty" => {
2744            let field_is_array = {
2745                let rf = assertion.field.as_deref().unwrap_or("");
2746                let rn = field_resolver.resolve(rf);
2747                field_resolver.is_array(rn)
2748            };
2749            // Special case: result_is_simple && !result_is_array && no field means the result is a pointer.
2750            // Empty means nil.
2751            if result_is_simple && !result_is_array && assertion.field.as_ref().is_none_or(|f| f.is_empty()) {
2752                // Pointer result (not dereferenced): empty means nil.
2753                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2754            } else if is_optional && !field_is_array {
2755                // Struct pointer: empty means nil.
2756                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2757            } else if is_optional && field_is_slice {
2758                // Slice optional: Go slices are already nil-able — no dereference needed.
2759                let _ = writeln!(out_ref, "\tif {field_expr} != nil && len({field_expr}) != 0 {{");
2760            } else if is_optional {
2761                // Pointer-to-slice (*[]T): dereference then len.
2762                let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
2763            } else {
2764                let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
2765            }
2766            let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
2767            let _ = writeln!(out_ref, "\t}}");
2768        }
2769        "contains_any" => {
2770            if let Some(values) = &assertion.values {
2771                let resolved_field = assertion.field.as_deref().unwrap_or("");
2772                let resolved_name = field_resolver.resolve(resolved_field);
2773                let field_is_array = field_resolver.is_array(resolved_name);
2774                let is_opt =
2775                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2776                let field_for_contains = if is_opt && field_is_array {
2777                    // Go slices are nil-able directly — no pointer dereference needed.
2778                    format!("jsonString({field_expr})")
2779                } else if is_opt {
2780                    format!("fmt.Sprint(*{field_expr})")
2781                } else if field_is_array {
2782                    format!("jsonString({field_expr})")
2783                } else {
2784                    format!("fmt.Sprint({field_expr})")
2785                };
2786                let _ = writeln!(out_ref, "\t{{");
2787                let _ = writeln!(out_ref, "\t\tfound := false");
2788                for val in values {
2789                    let go_val = json_to_go(val);
2790                    let _ = writeln!(
2791                        out_ref,
2792                        "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
2793                    );
2794                }
2795                let _ = writeln!(out_ref, "\t\tif !found {{");
2796                let _ = writeln!(
2797                    out_ref,
2798                    "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
2799                );
2800                let _ = writeln!(out_ref, "\t\t}}");
2801                let _ = writeln!(out_ref, "\t}}");
2802            }
2803        }
2804        "greater_than" => {
2805            if let Some(val) = &assertion.value {
2806                let go_val = json_to_go(val);
2807                // Use `< N+1` instead of `<= N` to avoid golangci-lint sloppyLen
2808                // warning when N is 0 (len(x) <= 0 → len(x) < 1).
2809                // For optional (pointer) fields, dereference and guard with nil check.
2810                if is_optional {
2811                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2812                    if let Some(n) = val.as_u64() {
2813                        let next = n + 1;
2814                        let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
2815                    } else {
2816                        let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
2817                    }
2818                    let _ = writeln!(
2819                        out_ref,
2820                        "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
2821                    );
2822                    let _ = writeln!(out_ref, "\t\t}}");
2823                    let _ = writeln!(out_ref, "\t}}");
2824                } else if let Some(n) = val.as_u64() {
2825                    let next = n + 1;
2826                    let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
2827                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2828                    let _ = writeln!(out_ref, "\t}}");
2829                } else {
2830                    let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
2831                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2832                    let _ = writeln!(out_ref, "\t}}");
2833                }
2834            }
2835        }
2836        "less_than" => {
2837            if let Some(val) = &assertion.value {
2838                let go_val = json_to_go(val);
2839                if let Some(ref guard) = nil_guard_expr {
2840                    let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2841                    let _ = writeln!(out_ref, "\t\tif {field_expr} >= {go_val} {{");
2842                    let _ = writeln!(out_ref, "\t\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2843                    let _ = writeln!(out_ref, "\t\t}}");
2844                    let _ = writeln!(out_ref, "\t}}");
2845                } else if is_optional && !field_expr.starts_with("len(") {
2846                    // Optional pointer field: nil-guard and dereference before comparison.
2847                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2848                    let _ = writeln!(out_ref, "\t\tif {deref_field_expr} >= {go_val} {{");
2849                    let _ = writeln!(
2850                        out_ref,
2851                        "\t\t\tt.Errorf(\"expected < {go_val}, got %v\", {deref_field_expr})"
2852                    );
2853                    let _ = writeln!(out_ref, "\t\t}}");
2854                    let _ = writeln!(out_ref, "\t}}");
2855                } else {
2856                    let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
2857                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2858                    let _ = writeln!(out_ref, "\t}}");
2859                }
2860            }
2861        }
2862        "greater_than_or_equal" => {
2863            if let Some(val) = &assertion.value {
2864                let go_val = json_to_go(val);
2865                if let Some(ref guard) = nil_guard_expr {
2866                    let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2867                    let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
2868                    let _ = writeln!(
2869                        out_ref,
2870                        "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
2871                    );
2872                    let _ = writeln!(out_ref, "\t\t}}");
2873                    let _ = writeln!(out_ref, "\t}}");
2874                } else if is_optional && !field_expr.starts_with("len(") {
2875                    // Optional pointer field: nil-guard and dereference before comparison.
2876                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2877                    let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
2878                    let _ = writeln!(
2879                        out_ref,
2880                        "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
2881                    );
2882                    let _ = writeln!(out_ref, "\t\t}}");
2883                    let _ = writeln!(out_ref, "\t}}");
2884                } else {
2885                    let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
2886                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
2887                    let _ = writeln!(out_ref, "\t}}");
2888                }
2889            }
2890        }
2891        "less_than_or_equal" => {
2892            if let Some(val) = &assertion.value {
2893                let go_val = json_to_go(val);
2894                if is_optional && !field_expr.starts_with("len(") {
2895                    // Optional pointer field: nil-guard and dereference before comparison.
2896                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2897                    let _ = writeln!(out_ref, "\t\tif {deref_field_expr} > {go_val} {{");
2898                    let _ = writeln!(
2899                        out_ref,
2900                        "\t\t\tt.Errorf(\"expected <= {go_val}, got %v\", {deref_field_expr})"
2901                    );
2902                    let _ = writeln!(out_ref, "\t\t}}");
2903                    let _ = writeln!(out_ref, "\t}}");
2904                } else {
2905                    let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
2906                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
2907                    let _ = writeln!(out_ref, "\t}}");
2908                }
2909            }
2910        }
2911        "starts_with" => {
2912            if let Some(expected) = &assertion.value {
2913                let go_val = json_to_go(expected);
2914                let field_for_prefix = if is_optional
2915                    && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2916                {
2917                    format!("string(*{field_expr})")
2918                } else {
2919                    format!("string({field_expr})")
2920                };
2921                let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
2922                let _ = writeln!(
2923                    out_ref,
2924                    "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
2925                );
2926                let _ = writeln!(out_ref, "\t}}");
2927            }
2928        }
2929        "count_min" => {
2930            if let Some(val) = &assertion.value {
2931                if let Some(n) = val.as_u64() {
2932                    if is_optional {
2933                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2934                        // Slices are value types in Go — use len(slice) not len(*slice).
2935                        let len_expr = if field_is_slice {
2936                            format!("len({field_expr})")
2937                        } else {
2938                            format!("len(*{field_expr})")
2939                        };
2940                        let _ = writeln!(
2941                            out_ref,
2942                            "\t\tassert.GreaterOrEqual(t, {len_expr}, {n}, \"expected at least {n} elements\")"
2943                        );
2944                        let _ = writeln!(out_ref, "\t}}");
2945                    } else {
2946                        let _ = writeln!(
2947                            out_ref,
2948                            "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
2949                        );
2950                    }
2951                }
2952            }
2953        }
2954        "count_equals" => {
2955            if let Some(val) = &assertion.value {
2956                if let Some(n) = val.as_u64() {
2957                    if is_optional {
2958                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2959                        // Slices are value types in Go — use len(slice) not len(*slice).
2960                        let len_expr = if field_is_slice {
2961                            format!("len({field_expr})")
2962                        } else {
2963                            format!("len(*{field_expr})")
2964                        };
2965                        let _ = writeln!(
2966                            out_ref,
2967                            "\t\tassert.Equal(t, {len_expr}, {n}, \"expected exactly {n} elements\")"
2968                        );
2969                        let _ = writeln!(out_ref, "\t}}");
2970                    } else {
2971                        let _ = writeln!(
2972                            out_ref,
2973                            "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
2974                        );
2975                    }
2976                }
2977            }
2978        }
2979        "is_true" => {
2980            if is_optional {
2981                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2982                let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
2983                let _ = writeln!(out_ref, "\t}}");
2984            } else {
2985                let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
2986            }
2987        }
2988        "is_false" => {
2989            if is_optional {
2990                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2991                let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
2992                let _ = writeln!(out_ref, "\t}}");
2993            } else {
2994                let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
2995            }
2996        }
2997        "method_result" => {
2998            if let Some(method_name) = &assertion.method {
2999                let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
3000                let check = assertion.check.as_deref().unwrap_or("is_true");
3001                // For pointer-returning functions, dereference with `*`. Value-returning
3002                // functions (e.g., NodeInfo field access) are used directly.
3003                let deref_expr = if info.is_pointer {
3004                    format!("*{}", info.call_expr)
3005                } else {
3006                    info.call_expr.clone()
3007                };
3008                match check {
3009                    "equals" => {
3010                        if let Some(val) = &assertion.value {
3011                            if val.is_boolean() {
3012                                if val.as_bool() == Some(true) {
3013                                    let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
3014                                } else {
3015                                    let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
3016                                }
3017                            } else {
3018                                // Apply type cast to numeric literals when the method returns
3019                                // a typed uint (e.g., *uint) to avoid reflect.DeepEqual
3020                                // mismatches between int and uint in testify's assert.Equal.
3021                                let go_val = if let Some(cast) = info.value_cast {
3022                                    if val.is_number() {
3023                                        format!("{cast}({})", json_to_go(val))
3024                                    } else {
3025                                        json_to_go(val)
3026                                    }
3027                                } else {
3028                                    json_to_go(val)
3029                                };
3030                                let _ = writeln!(
3031                                    out_ref,
3032                                    "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
3033                                );
3034                            }
3035                        }
3036                    }
3037                    "is_true" => {
3038                        let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
3039                    }
3040                    "is_false" => {
3041                        let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
3042                    }
3043                    "greater_than_or_equal" => {
3044                        if let Some(val) = &assertion.value {
3045                            let n = val.as_u64().unwrap_or(0);
3046                            // Use the value_cast type if available (e.g., uint for named_children_count).
3047                            let cast = info.value_cast.unwrap_or("uint");
3048                            let _ = writeln!(
3049                                out_ref,
3050                                "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
3051                            );
3052                        }
3053                    }
3054                    "count_min" => {
3055                        if let Some(val) = &assertion.value {
3056                            let n = val.as_u64().unwrap_or(0);
3057                            let _ = writeln!(
3058                                out_ref,
3059                                "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
3060                            );
3061                        }
3062                    }
3063                    "contains" => {
3064                        if let Some(val) = &assertion.value {
3065                            let go_val = json_to_go(val);
3066                            let _ = writeln!(
3067                                out_ref,
3068                                "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
3069                            );
3070                        }
3071                    }
3072                    "is_error" => {
3073                        let _ = writeln!(out_ref, "\t{{");
3074                        let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
3075                        let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
3076                        let _ = writeln!(out_ref, "\t}}");
3077                    }
3078                    other_check => {
3079                        panic!("Go e2e generator: unsupported method_result check type: {other_check}");
3080                    }
3081                }
3082            } else {
3083                panic!("Go e2e generator: method_result assertion missing 'method' field");
3084            }
3085        }
3086        "min_length" => {
3087            if let Some(val) = &assertion.value {
3088                if let Some(n) = val.as_u64() {
3089                    if is_optional {
3090                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
3091                        let _ = writeln!(
3092                            out_ref,
3093                            "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
3094                        );
3095                        let _ = writeln!(out_ref, "\t}}");
3096                    } else if field_expr.starts_with("len(") {
3097                        let _ = writeln!(
3098                            out_ref,
3099                            "\tassert.GreaterOrEqual(t, {field_expr}, {n}, \"expected length >= {n}\")"
3100                        );
3101                    } else {
3102                        let _ = writeln!(
3103                            out_ref,
3104                            "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
3105                        );
3106                    }
3107                }
3108            }
3109        }
3110        "max_length" => {
3111            if let Some(val) = &assertion.value {
3112                if let Some(n) = val.as_u64() {
3113                    if is_optional {
3114                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
3115                        let _ = writeln!(
3116                            out_ref,
3117                            "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
3118                        );
3119                        let _ = writeln!(out_ref, "\t}}");
3120                    } else if field_expr.starts_with("len(") {
3121                        let _ = writeln!(
3122                            out_ref,
3123                            "\tassert.LessOrEqual(t, {field_expr}, {n}, \"expected length <= {n}\")"
3124                        );
3125                    } else {
3126                        let _ = writeln!(
3127                            out_ref,
3128                            "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
3129                        );
3130                    }
3131                }
3132            }
3133        }
3134        "ends_with" => {
3135            if let Some(expected) = &assertion.value {
3136                let go_val = json_to_go(expected);
3137                let field_for_suffix = if is_optional
3138                    && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
3139                {
3140                    format!("string(*{field_expr})")
3141                } else {
3142                    format!("string({field_expr})")
3143                };
3144                let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
3145                let _ = writeln!(
3146                    out_ref,
3147                    "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
3148                );
3149                let _ = writeln!(out_ref, "\t}}");
3150            }
3151        }
3152        "matches_regex" => {
3153            if let Some(expected) = &assertion.value {
3154                let go_val = json_to_go(expected);
3155                let field_for_regex = if is_optional
3156                    && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
3157                {
3158                    format!("*{field_expr}")
3159                } else {
3160                    field_expr.clone()
3161                };
3162                let _ = writeln!(
3163                    out_ref,
3164                    "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
3165                );
3166            }
3167        }
3168        "not_error" => {
3169            // Already handled by the `if err != nil` check above.
3170        }
3171        "error" => {
3172            // Handled at the test function level.
3173        }
3174        other => {
3175            panic!("Go e2e generator: unsupported assertion type: {other}");
3176        }
3177    }
3178
3179    // If the assertion accesses an array element via [0], wrap the generated code in a
3180    // bounds check to prevent an index-out-of-range panic when the array is empty.
3181    if let Some(ref arr) = array_guard {
3182        if !assertion_buf.is_empty() {
3183            let _ = writeln!(out, "\tif len({arr}) > 0 {{");
3184            // Re-indent each line by one additional tab level.
3185            for line in assertion_buf.lines() {
3186                let _ = writeln!(out, "\t{line}");
3187            }
3188            let _ = writeln!(out, "\t}}");
3189        }
3190    } else {
3191        out.push_str(&assertion_buf);
3192    }
3193}
3194
3195/// Metadata about the return type of a Go method call for `method_result` assertions.
3196struct GoMethodCallInfo {
3197    /// The call expression string.
3198    call_expr: String,
3199    /// Whether the return type is a pointer (needs `*` dereference for value comparison).
3200    is_pointer: bool,
3201    /// Optional Go type cast to apply to numeric literal values in `equals` assertions
3202    /// (e.g., `"uint"` so that `0` becomes `uint(0)` to match `*uint` deref type).
3203    value_cast: Option<&'static str>,
3204}
3205
3206/// Build a Go call expression for a `method_result` assertion on a tree-sitter Tree.
3207///
3208/// Maps method names to the appropriate Go function calls, matching the Go binding API
3209/// in `packages/go/binding.go`. Returns a [`GoMethodCallInfo`] describing the call and
3210/// its return type characteristics.
3211///
3212/// Return types by method:
3213/// - `has_error_nodes`, `contains_node_type` → `*bool` (pointer)
3214/// - `error_count` → `*uint` (pointer, value_cast = "uint")
3215/// - `tree_to_sexp` → `*string` (pointer)
3216/// - `root_node_type` → `string` via `RootNodeInfo(tree).Kind` (value)
3217/// - `named_children_count` → `uint` via `RootNodeInfo(tree).NamedChildCount` (value, value_cast = "uint")
3218/// - `find_nodes_by_type` → `*[]NodeInfo` (pointer to slice)
3219/// - `run_query` → `(*[]QueryMatch, error)` (pointer + error; use `is_error` check type)
3220fn build_go_method_call(
3221    result_var: &str,
3222    method_name: &str,
3223    args: Option<&serde_json::Value>,
3224    import_alias: &str,
3225) -> GoMethodCallInfo {
3226    match method_name {
3227        "root_node_type" => GoMethodCallInfo {
3228            call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
3229            is_pointer: false,
3230            value_cast: None,
3231        },
3232        "named_children_count" => GoMethodCallInfo {
3233            call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
3234            is_pointer: false,
3235            value_cast: Some("uint"),
3236        },
3237        "has_error_nodes" => GoMethodCallInfo {
3238            call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
3239            is_pointer: true,
3240            value_cast: None,
3241        },
3242        "error_count" | "tree_error_count" => GoMethodCallInfo {
3243            call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
3244            is_pointer: true,
3245            value_cast: Some("uint"),
3246        },
3247        "tree_to_sexp" => GoMethodCallInfo {
3248            call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
3249            is_pointer: true,
3250            value_cast: None,
3251        },
3252        "contains_node_type" => {
3253            let node_type = args
3254                .and_then(|a| a.get("node_type"))
3255                .and_then(|v| v.as_str())
3256                .unwrap_or("");
3257            GoMethodCallInfo {
3258                call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
3259                is_pointer: true,
3260                value_cast: None,
3261            }
3262        }
3263        "find_nodes_by_type" => {
3264            let node_type = args
3265                .and_then(|a| a.get("node_type"))
3266                .and_then(|v| v.as_str())
3267                .unwrap_or("");
3268            GoMethodCallInfo {
3269                call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
3270                is_pointer: true,
3271                value_cast: None,
3272            }
3273        }
3274        "run_query" => {
3275            let query_source = args
3276                .and_then(|a| a.get("query_source"))
3277                .and_then(|v| v.as_str())
3278                .unwrap_or("");
3279            let language = args
3280                .and_then(|a| a.get("language"))
3281                .and_then(|v| v.as_str())
3282                .unwrap_or("");
3283            let query_lit = go_string_literal(query_source);
3284            let lang_lit = go_string_literal(language);
3285            // RunQuery returns (*[]QueryMatch, error) — use is_error check type.
3286            GoMethodCallInfo {
3287                call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
3288                is_pointer: false,
3289                value_cast: None,
3290            }
3291        }
3292        other => {
3293            let method_pascal = other.to_upper_camel_case();
3294            GoMethodCallInfo {
3295                call_expr: format!("{result_var}.{method_pascal}()"),
3296                is_pointer: false,
3297                value_cast: None,
3298            }
3299        }
3300    }
3301}
3302
3303/// Convert a `serde_json::Value` to a Go literal string.
3304/// Recursively convert a JSON value for Go struct unmarshalling.
3305///
3306/// The Go binding's `ConversionOptions` struct uses:
3307/// - `snake_case` JSON field tags (e.g. `"code_block_style"` not `"codeBlockStyle"`)
3308/// - lowercase/snake_case string values for enums (e.g. `"indented"`, `"atx_closed"`)
3309///
3310/// Fixture JSON uses camelCase keys and PascalCase enum values (Python/TS conventions).
3311/// This function remaps both so the generated Go tests can unmarshal correctly.
3312fn convert_json_for_go(value: serde_json::Value) -> serde_json::Value {
3313    match value {
3314        serde_json::Value::Object(map) => {
3315            let new_map: serde_json::Map<String, serde_json::Value> = map
3316                .into_iter()
3317                .map(|(k, v)| (camel_to_snake_case(&k), convert_json_for_go(v)))
3318                .collect();
3319            serde_json::Value::Object(new_map)
3320        }
3321        serde_json::Value::Array(arr) => {
3322            // Check if this is a byte array (array of integers 0-255).
3323            // If so, encode as base64 string for Go json.Unmarshal compatibility.
3324            if is_byte_array(&arr) {
3325                let bytes: Vec<u8> = arr
3326                    .iter()
3327                    .filter_map(|v| v.as_u64().and_then(|n| if n <= 255 { Some(n as u8) } else { None }))
3328                    .collect();
3329                // Encode bytes as base64 for Go json.Unmarshal (Go expects []byte as base64 strings)
3330                let encoded = base64_encode(&bytes);
3331                serde_json::Value::String(encoded)
3332            } else {
3333                serde_json::Value::Array(arr.into_iter().map(convert_json_for_go).collect())
3334            }
3335        }
3336        serde_json::Value::String(s) => {
3337            // Convert PascalCase enum values to snake_case.
3338            // Only convert values that look like PascalCase (start with uppercase, no spaces).
3339            serde_json::Value::String(pascal_to_snake_case(&s))
3340        }
3341        other => other,
3342    }
3343}
3344
3345/// Check if an array looks like a byte array (all elements are integers 0-255).
3346fn is_byte_array(arr: &[serde_json::Value]) -> bool {
3347    if arr.is_empty() {
3348        return false;
3349    }
3350    arr.iter().all(|v| {
3351        if let serde_json::Value::Number(n) = v {
3352            n.is_u64() && n.as_u64().is_some_and(|u| u <= 255)
3353        } else {
3354            false
3355        }
3356    })
3357}
3358
3359/// Encode bytes as base64 string (standard alphabet without padding in this output,
3360/// though Go's json.Unmarshal handles both).
3361fn base64_encode(bytes: &[u8]) -> String {
3362    const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3363    let mut result = String::new();
3364    let mut i = 0;
3365
3366    while i + 2 < bytes.len() {
3367        let b1 = bytes[i];
3368        let b2 = bytes[i + 1];
3369        let b3 = bytes[i + 2];
3370
3371        result.push(TABLE[(b1 >> 2) as usize] as char);
3372        result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3373        result.push(TABLE[(((b2 & 0x0f) << 2) | (b3 >> 6)) as usize] as char);
3374        result.push(TABLE[(b3 & 0x3f) as usize] as char);
3375
3376        i += 3;
3377    }
3378
3379    // Handle remaining bytes
3380    if i < bytes.len() {
3381        let b1 = bytes[i];
3382        result.push(TABLE[(b1 >> 2) as usize] as char);
3383
3384        if i + 1 < bytes.len() {
3385            let b2 = bytes[i + 1];
3386            result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3387            result.push(TABLE[((b2 & 0x0f) << 2) as usize] as char);
3388            result.push('=');
3389        } else {
3390            result.push(TABLE[((b1 & 0x03) << 4) as usize] as char);
3391            result.push_str("==");
3392        }
3393    }
3394
3395    result
3396}
3397
3398/// Convert a camelCase or PascalCase string to snake_case.
3399fn camel_to_snake_case(s: &str) -> String {
3400    let mut result = String::new();
3401    let mut prev_upper = false;
3402    for (i, c) in s.char_indices() {
3403        if c.is_uppercase() {
3404            if i > 0 && !prev_upper {
3405                result.push('_');
3406            }
3407            result.push(c.to_lowercase().next().unwrap_or(c));
3408            prev_upper = true;
3409        } else {
3410            if prev_upper && i > 1 {
3411                // Handles sequences like "URLPath" → "url_path": insert _ before last uppercase
3412                // when transitioning from a run of uppercase back to lowercase.
3413                // This is tricky — use simple approach: detect Aa pattern.
3414            }
3415            result.push(c);
3416            prev_upper = false;
3417        }
3418    }
3419    result
3420}
3421
3422/// Convert a PascalCase string to snake_case (for enum values).
3423///
3424/// Only converts if the string looks like PascalCase (starts uppercase, no spaces/underscores).
3425/// Values that are already lowercase/snake_case are returned unchanged.
3426fn pascal_to_snake_case(s: &str) -> String {
3427    // Skip conversion for strings that already contain underscores, spaces, or start lowercase.
3428    let first_char = s.chars().next();
3429    if first_char.is_none() || !first_char.unwrap().is_uppercase() || s.contains('_') || s.contains(' ') {
3430        return s.to_string();
3431    }
3432    camel_to_snake_case(s)
3433}
3434
3435/// Map an `ArgMapping.element_type` to a Go slice type. Used for `json_object` args
3436/// whose fixture value is a JSON array. The element type is wrapped in `[]…` so an
3437/// element of `String` becomes `[]string` and `Vec<String>` becomes `[][]string`.
3438fn element_type_to_go_slice(element_type: Option<&str>, import_alias: &str) -> String {
3439    let elem = element_type.unwrap_or("String").trim();
3440    let go_elem = rust_type_to_go(elem, import_alias);
3441    format!("[]{go_elem}")
3442}
3443
3444/// Map a small subset of Rust scalar / `Vec<T>` types to their Go equivalents.
3445/// For unknown types, qualify with the import alias (e.g., "kreuzberg.BatchBytesItem").
3446fn rust_type_to_go(rust: &str, import_alias: &str) -> String {
3447    let trimmed = rust.trim();
3448    if let Some(inner) = trimmed.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
3449        return format!("[]{}", rust_type_to_go(inner, import_alias));
3450    }
3451    match trimmed {
3452        "String" | "&str" | "str" => "string".to_string(),
3453        "bool" => "bool".to_string(),
3454        "f32" => "float32".to_string(),
3455        "f64" => "float64".to_string(),
3456        "i8" => "int8".to_string(),
3457        "i16" => "int16".to_string(),
3458        "i32" => "int32".to_string(),
3459        "i64" | "isize" => "int64".to_string(),
3460        "u8" => "uint8".to_string(),
3461        "u16" => "uint16".to_string(),
3462        "u32" => "uint32".to_string(),
3463        "u64" | "usize" => "uint64".to_string(),
3464        _ => format!("{import_alias}.{trimmed}"),
3465    }
3466}
3467
3468fn json_to_go(value: &serde_json::Value) -> String {
3469    match value {
3470        serde_json::Value::String(s) => go_string_literal(s),
3471        serde_json::Value::Bool(b) => b.to_string(),
3472        serde_json::Value::Number(n) => n.to_string(),
3473        serde_json::Value::Null => "nil".to_string(),
3474        // For complex types, serialize to JSON string and pass as literal.
3475        other => go_string_literal(&other.to_string()),
3476    }
3477}
3478
3479// ---------------------------------------------------------------------------
3480// Visitor generation
3481// ---------------------------------------------------------------------------
3482
3483/// Derive a unique, exported Go struct name for a visitor from a fixture ID.
3484///
3485/// E.g. `visitor_continue_default` → `visitorContinueDefault` (unexported, avoids
3486/// polluting the exported API of the test package while still being package-level).
3487fn visitor_struct_name(fixture_id: &str) -> String {
3488    use heck::ToUpperCamelCase;
3489    // Use UpperCamelCase so Go treats it as exported — required for method sets.
3490    format!("testVisitor{}", fixture_id.to_upper_camel_case())
3491}
3492
3493/// Emit a package-level Go struct declaration and all its visitor methods.
3494///
3495/// The struct embeds `BaseVisitor` to satisfy all interface methods not
3496/// explicitly overridden by the fixture callbacks.
3497fn emit_go_visitor_struct(
3498    out: &mut String,
3499    struct_name: &str,
3500    visitor_spec: &crate::fixture::VisitorSpec,
3501    import_alias: &str,
3502) {
3503    let _ = writeln!(out, "type {struct_name} struct{{");
3504    let _ = writeln!(out, "\t{import_alias}.BaseVisitor");
3505    let _ = writeln!(out, "}}");
3506    for (method_name, action) in &visitor_spec.callbacks {
3507        emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
3508    }
3509}
3510
3511/// Emit a Go visitor method for a callback action on the named struct.
3512fn emit_go_visitor_method(
3513    out: &mut String,
3514    struct_name: &str,
3515    method_name: &str,
3516    action: &CallbackAction,
3517    import_alias: &str,
3518) {
3519    let camel_method = method_to_camel(method_name);
3520    // Parameter signatures must exactly match the htmltomarkdown.Visitor interface.
3521    // Optional fields use pointer types (*string, *uint32, etc.) to indicate nil-ability.
3522    let params = match method_name {
3523        "visit_link" => format!("_ {import_alias}.NodeContext, href string, text string, title *string"),
3524        "visit_image" => format!("_ {import_alias}.NodeContext, src string, alt string, title *string"),
3525        "visit_heading" => format!("_ {import_alias}.NodeContext, level uint32, text string, id *string"),
3526        "visit_code_block" => format!("_ {import_alias}.NodeContext, lang *string, code string"),
3527        "visit_code_inline"
3528        | "visit_strong"
3529        | "visit_emphasis"
3530        | "visit_strikethrough"
3531        | "visit_underline"
3532        | "visit_subscript"
3533        | "visit_superscript"
3534        | "visit_mark"
3535        | "visit_button"
3536        | "visit_summary"
3537        | "visit_figcaption"
3538        | "visit_definition_term"
3539        | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
3540        "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
3541        "visit_list_item" => {
3542            format!("_ {import_alias}.NodeContext, ordered bool, marker string, text string")
3543        }
3544        "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth uint"),
3545        "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
3546        "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName string, html string"),
3547        "visit_form" => format!("_ {import_alias}.NodeContext, action *string, method *string"),
3548        "visit_input" => {
3549            format!("_ {import_alias}.NodeContext, inputType string, name *string, value *string")
3550        }
3551        "visit_audio" | "visit_video" | "visit_iframe" => {
3552            format!("_ {import_alias}.NodeContext, src *string")
3553        }
3554        "visit_details" => format!("_ {import_alias}.NodeContext, open bool"),
3555        "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
3556            format!("_ {import_alias}.NodeContext, output string")
3557        }
3558        "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
3559        "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
3560        _ => format!("_ {import_alias}.NodeContext"),
3561    };
3562
3563    let _ = writeln!(
3564        out,
3565        "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
3566    );
3567    match action {
3568        CallbackAction::Skip => {
3569            let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip()");
3570        }
3571        CallbackAction::Continue => {
3572            let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue()");
3573        }
3574        CallbackAction::PreserveHtml => {
3575            let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHTML()");
3576        }
3577        CallbackAction::Custom { output } => {
3578            let escaped = go_string_literal(output);
3579            let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
3580        }
3581        CallbackAction::CustomTemplate { template, .. } => {
3582            // Convert {var} placeholders to %s format verbs and collect arg names.
3583            // E.g. `QUOTE: "{text}"` → fmt.Sprintf("QUOTE: \"%s\"", text)
3584            //
3585            // For pointer-typed params (e.g. `src *string`), dereference with `*`
3586            // — the test fixtures always supply a non-nil value for methods that
3587            // fire a custom template, so this is safe in practice.
3588            let ptr_params = go_visitor_ptr_params(method_name);
3589            let (fmt_str, fmt_args) = template_to_sprintf(template, &ptr_params);
3590            let escaped_fmt = go_string_literal(&fmt_str);
3591            if fmt_args.is_empty() {
3592                let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
3593            } else {
3594                let args_str = fmt_args.join(", ");
3595                let _ = writeln!(
3596                    out,
3597                    "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
3598                );
3599            }
3600        }
3601    }
3602    let _ = writeln!(out, "}}");
3603}
3604
3605/// Return the set of camelCase parameter names that are pointer types (`*string`) for a
3606/// given visitor method name.  Used to dereference pointers in template `fmt.Sprintf` calls.
3607fn go_visitor_ptr_params(method_name: &str) -> std::collections::HashSet<&'static str> {
3608    match method_name {
3609        "visit_link" => ["title"].into(),
3610        "visit_image" => ["title"].into(),
3611        "visit_heading" => ["id"].into(),
3612        "visit_code_block" => ["lang"].into(),
3613        "visit_form" => ["action", "method"].into(),
3614        "visit_input" => ["name", "value"].into(),
3615        "visit_audio" | "visit_video" | "visit_iframe" => ["src"].into(),
3616        _ => std::collections::HashSet::new(),
3617    }
3618}
3619
3620/// Convert a `{var}` template string into a `fmt.Sprintf` format string and argument list.
3621///
3622/// For example, `QUOTE: "{text}"` becomes `("QUOTE: \"%s\"", vec!["text"])`.
3623///
3624/// Placeholder names in the template use snake_case (matching fixture field names); they
3625/// are converted to Go camelCase parameter names using `go_param_name` so they match the
3626/// generated visitor method signatures (e.g. `{input_type}` → `inputType`).
3627///
3628/// `ptr_params` — camelCase names of parameters that are `*string`; these are
3629/// dereferenced with `*` when used as `fmt.Sprintf` arguments.  The fixtures that
3630/// use `custom_template` on pointer-param methods always supply a non-nil value.
3631fn template_to_sprintf(template: &str, ptr_params: &std::collections::HashSet<&str>) -> (String, Vec<String>) {
3632    let mut fmt_str = String::new();
3633    let mut args: Vec<String> = Vec::new();
3634    let mut chars = template.chars().peekable();
3635    while let Some(c) = chars.next() {
3636        if c == '{' {
3637            // Collect placeholder name until '}'.
3638            let mut name = String::new();
3639            for inner in chars.by_ref() {
3640                if inner == '}' {
3641                    break;
3642                }
3643                name.push(inner);
3644            }
3645            fmt_str.push_str("%s");
3646            // Convert snake_case placeholder to Go camelCase to match method param names.
3647            let go_name = go_param_name(&name);
3648            // Dereference pointer params so fmt.Sprintf receives a string value.
3649            let arg_expr = if ptr_params.contains(go_name.as_str()) {
3650                format!("*{go_name}")
3651            } else {
3652                go_name
3653            };
3654            args.push(arg_expr);
3655        } else {
3656            fmt_str.push(c);
3657        }
3658    }
3659    (fmt_str, args)
3660}
3661
3662/// Convert snake_case method names to Go camelCase.
3663fn method_to_camel(snake: &str) -> String {
3664    use heck::ToUpperCamelCase;
3665    snake.to_upper_camel_case()
3666}
3667
3668#[cfg(test)]
3669mod tests {
3670    use super::*;
3671    use crate::config::{CallConfig, E2eConfig};
3672    use crate::fixture::{Assertion, Fixture};
3673
3674    fn make_fixture(id: &str) -> Fixture {
3675        Fixture {
3676            id: id.to_string(),
3677            category: None,
3678            description: "test fixture".to_string(),
3679            tags: vec![],
3680            skip: None,
3681            env: None,
3682            call: None,
3683            input: serde_json::Value::Null,
3684            mock_response: Some(crate::fixture::MockResponse {
3685                status: 200,
3686                body: Some(serde_json::Value::Null),
3687                stream_chunks: None,
3688                headers: std::collections::HashMap::new(),
3689            }),
3690            source: String::new(),
3691            http: None,
3692            assertions: vec![Assertion {
3693                assertion_type: "not_error".to_string(),
3694                ..Default::default()
3695            }],
3696            visitor: None,
3697        }
3698    }
3699
3700    /// snake_case function names in `[e2e.call]` must be routed through `to_go_name`
3701    /// so the emitted Go call uses the idiomatic CamelCase (e.g. `CleanExtractedText`
3702    /// instead of `clean_extracted_text`).
3703    #[test]
3704    fn test_go_method_name_uses_go_casing() {
3705        let e2e_config = E2eConfig {
3706            call: CallConfig {
3707                function: "clean_extracted_text".to_string(),
3708                module: "github.com/example/mylib".to_string(),
3709                result_var: "result".to_string(),
3710                returns_result: true,
3711                ..CallConfig::default()
3712            },
3713            ..E2eConfig::default()
3714        };
3715
3716        let fixture = make_fixture("basic_text");
3717        let mut out = String::new();
3718        render_test_function(
3719            &mut out,
3720            &fixture,
3721            "kreuzberg",
3722            &e2e_config,
3723            &[],
3724            &std::collections::HashSet::new(),
3725        );
3726
3727        assert!(
3728            out.contains("kreuzberg.CleanExtractedText("),
3729            "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
3730        );
3731        assert!(
3732            !out.contains("kreuzberg.clean_extracted_text("),
3733            "must not emit raw snake_case method name, got:\n{out}"
3734        );
3735    }
3736
3737    #[test]
3738    fn test_streaming_fixture_emits_collect_snippet() {
3739        // A streaming fixture should emit `stream, err :=` and the collect loop.
3740        let streaming_fixture_json = r#"{
3741            "id": "basic_stream",
3742            "description": "basic streaming test",
3743            "call": "chat_stream",
3744            "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3745            "mock_response": {
3746                "status": 200,
3747                "stream_chunks": [{"delta": "hello"}]
3748            },
3749            "assertions": [
3750                {"type": "count_min", "field": "chunks", "value": 1}
3751            ]
3752        }"#;
3753        let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3754        assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3755
3756        let e2e_config = E2eConfig {
3757            call: CallConfig {
3758                function: "chat_stream".to_string(),
3759                module: "github.com/example/mylib".to_string(),
3760                result_var: "result".to_string(),
3761                returns_result: true,
3762                r#async: true,
3763                ..CallConfig::default()
3764            },
3765            ..E2eConfig::default()
3766        };
3767
3768        let mut out = String::new();
3769        render_test_function(
3770            &mut out,
3771            &fixture,
3772            "pkg",
3773            &e2e_config,
3774            &[],
3775            &std::collections::HashSet::new(),
3776        );
3777
3778        assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3779        assert!(
3780            out.contains("for chunk := range stream"),
3781            "should emit collect loop, got:\n{out}"
3782        );
3783    }
3784
3785    #[test]
3786    fn test_streaming_with_client_factory_and_json_arg() {
3787        // Mimics the real liter-llm setup: no returns_result on the call,
3788        // json_object arg (binding_returns_error=true), and client_factory from
3789        // the default Go call override.
3790        use alef_core::config::e2e::{ArgMapping, CallOverride};
3791        let streaming_fixture_json = r#"{
3792            "id": "basic_stream_client",
3793            "description": "basic streaming test with client",
3794            "call": "chat_stream",
3795            "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3796            "mock_response": {
3797                "status": 200,
3798                "stream_chunks": [{"delta": "hello"}]
3799            },
3800            "assertions": [
3801                {"type": "count_min", "field": "chunks", "value": 1}
3802            ]
3803        }"#;
3804        let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3805        assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3806
3807        let go_override = CallOverride {
3808            client_factory: Some("CreateClient".to_string()),
3809            ..Default::default()
3810        };
3811
3812        let mut call_overrides = std::collections::HashMap::new();
3813        call_overrides.insert("go".to_string(), go_override);
3814
3815        let e2e_config = E2eConfig {
3816            call: CallConfig {
3817                function: "chat_stream".to_string(),
3818                module: "github.com/example/mylib".to_string(),
3819                result_var: "result".to_string(),
3820                returns_result: false, // NOT true — like real liter-llm
3821                r#async: true,
3822                args: vec![ArgMapping {
3823                    name: "request".to_string(),
3824                    field: "input".to_string(),
3825                    arg_type: "json_object".to_string(),
3826                    optional: false,
3827                    owned: true,
3828                    element_type: None,
3829                    go_type: None,
3830                }],
3831                overrides: call_overrides,
3832                ..CallConfig::default()
3833            },
3834            ..E2eConfig::default()
3835        };
3836
3837        let mut out = String::new();
3838        render_test_function(
3839            &mut out,
3840            &fixture,
3841            "pkg",
3842            &e2e_config,
3843            &[],
3844            &std::collections::HashSet::new(),
3845        );
3846
3847        eprintln!("generated:\n{out}");
3848        assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3849        assert!(
3850            out.contains("for chunk := range stream"),
3851            "should emit collect loop, got:\n{out}"
3852        );
3853    }
3854
3855    /// When `segments` is an optional field (Option<Vec<T>>) and a fixture asserts on
3856    /// `segments[0].id`, the prefix guard must be `result.Segments != nil` — NOT
3857    /// `result.Segments[0] != nil`, which is a compile error for a value-typed element.
3858    #[test]
3859    fn test_indexed_element_prefix_guard_uses_array_not_element() {
3860        let mut optional_fields = std::collections::HashSet::new();
3861        optional_fields.insert("segments".to_string());
3862        let mut array_fields = std::collections::HashSet::new();
3863        array_fields.insert("segments".to_string());
3864
3865        let e2e_config = E2eConfig {
3866            call: CallConfig {
3867                function: "transcribe".to_string(),
3868                module: "github.com/example/mylib".to_string(),
3869                result_var: "result".to_string(),
3870                returns_result: true,
3871                ..CallConfig::default()
3872            },
3873            fields_optional: optional_fields,
3874            fields_array: array_fields,
3875            ..E2eConfig::default()
3876        };
3877
3878        let fixture = Fixture {
3879            id: "edge_transcribe_with_timestamps".to_string(),
3880            category: None,
3881            description: "Transcription with timestamp segments".to_string(),
3882            tags: vec![],
3883            skip: None,
3884            env: None,
3885            call: None,
3886            input: serde_json::Value::Null,
3887            mock_response: Some(crate::fixture::MockResponse {
3888                status: 200,
3889                body: Some(serde_json::Value::Null),
3890                stream_chunks: None,
3891                headers: std::collections::HashMap::new(),
3892            }),
3893            source: String::new(),
3894            http: None,
3895            assertions: vec![
3896                Assertion {
3897                    assertion_type: "not_error".to_string(),
3898                    ..Default::default()
3899                },
3900                Assertion {
3901                    assertion_type: "equals".to_string(),
3902                    field: Some("segments[0].id".to_string()),
3903                    value: Some(serde_json::Value::Number(serde_json::Number::from(0u64))),
3904                    ..Default::default()
3905                },
3906            ],
3907            visitor: None,
3908        };
3909
3910        let mut out = String::new();
3911        render_test_function(
3912            &mut out,
3913            &fixture,
3914            "pkg",
3915            &e2e_config,
3916            &[],
3917            &std::collections::HashSet::new(),
3918        );
3919
3920        eprintln!("generated:\n{out}");
3921
3922        // Must guard on the slice itself — not on the element.
3923        // Accepts either `Segments != nil` or `len(Segments) > 0`; both are
3924        // valid Go guards for the slice and avoid the invalid element nil
3925        // check.
3926        assert!(
3927            out.contains("result.Segments != nil") || out.contains("len(result.Segments) > 0"),
3928            "guard must be on Segments (the slice), not an element; got:\n{out}"
3929        );
3930        // Must NOT emit the invalid element nil check.
3931        assert!(
3932            !out.contains("result.Segments[0] != nil"),
3933            "must not emit Segments[0] != nil for a value-type element; got:\n{out}"
3934        );
3935    }
3936
3937    /// Regression test: a `result_is_simple` call with a `contains` assertion whose
3938    /// `field` ("result") is not a struct field must still bind the call to the result
3939    /// variable AND emit the `fmt`/`strings` imports.  The assertion renderer ignores
3940    /// the field for `result_is_simple` calls and emits `strings.Contains(fmt.Sprint(result), …)`,
3941    /// so binding to `_` (or omitting the imports) produces uncompilable Go.
3942    #[test]
3943    fn test_result_is_simple_contains_binds_result_and_emits_imports() {
3944        use alef_core::config::e2e::ArgMapping;
3945
3946        let e2e_config = E2eConfig {
3947            call: CallConfig {
3948                function: "detect_mime_type_from_bytes".to_string(),
3949                module: "github.com/example/mylib".to_string(),
3950                result_var: "result".to_string(),
3951                returns_result: true,
3952                result_is_simple: true,
3953                args: vec![ArgMapping {
3954                    name: "content".to_string(),
3955                    field: "input.data".to_string(),
3956                    arg_type: "bytes".to_string(),
3957                    optional: false,
3958                    owned: false,
3959                    element_type: None,
3960                    go_type: None,
3961                }],
3962                ..CallConfig::default()
3963            },
3964            ..E2eConfig::default()
3965        };
3966
3967        let fixture = Fixture {
3968            id: "mime_detect_bytes".to_string(),
3969            category: None,
3970            description: "Detect MIME type from file bytes".to_string(),
3971            tags: vec![],
3972            skip: None,
3973            env: None,
3974            call: None,
3975            input: serde_json::json!({"data": "pdf/fake_memo.pdf"}),
3976            mock_response: None,
3977            source: String::new(),
3978            http: None,
3979            assertions: vec![Assertion {
3980                assertion_type: "contains".to_string(),
3981                field: Some("result".to_string()),
3982                value: Some(serde_json::Value::String("pdf".to_string())),
3983                ..Default::default()
3984            }],
3985            visitor: None,
3986        };
3987
3988        let out = render_test_file(
3989            "mime_utilities",
3990            &[&fixture],
3991            "github.com/example/mylib",
3992            "kreuzberg",
3993            &e2e_config,
3994            &[],
3995            &std::collections::HashSet::new(),
3996        );
3997
3998        assert!(
3999            out.contains("result, err := kreuzberg.DetectMimeTypeFromBytes("),
4000            "expected the call to bind to `result`, not `_`; got:\n{out}"
4001        );
4002        assert!(
4003            out.contains("strings.Contains(") && out.contains("fmt.Sprint("),
4004            "expected `strings.Contains(fmt.Sprint(...))` rendering; got:\n{out}"
4005        );
4006        assert!(
4007            out.contains("\t\"fmt\""),
4008            "expected the `fmt` import to be emitted; got:\n{out}"
4009        );
4010        assert!(
4011            out.contains("\t\"strings\""),
4012            "expected the `strings` import to be emitted; got:\n{out}"
4013        );
4014    }
4015}