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