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                // Only emit local vars for fields that are actually valid for the result type.
1385                // Skip fields that will be marked "skipped" later in render_assertion.
1386                if !result_is_simple && !field_resolver.is_valid_for_result(f) {
1387                    continue;
1388                }
1389                let resolved = field_resolver.resolve(f);
1390                if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
1391                    // Only create deref locals for string-valued fields that are NOT arrays.
1392                    // Array fields (e.g., *[]string) must keep their pointer form so
1393                    // render_assertion can emit strings.Join(*field, " ") rather than
1394                    // treating them as plain strings.
1395                    let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
1396                    let is_array_field = field_resolver.is_array(resolved);
1397                    if !is_string_field || is_array_field {
1398                        // Non-string optional fields (e.g., *uint64) and array optional
1399                        // fields (e.g., *[]string) are handled by nil guards in render_assertion.
1400                        continue;
1401                    }
1402                    let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
1403                    let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
1404                    if field_resolver.has_map_access(f) {
1405                        // Go map access returns a value type (string), not a pointer.
1406                        // Use the value directly — empty string means not present.
1407                        let _ = writeln!(out, "\t{local_var} := {field_expr}");
1408                    } else {
1409                        let _ = writeln!(out, "\tvar {local_var} string");
1410                        let _ = writeln!(out, "\tif {field_expr} != nil {{");
1411                        // Use fmt.Sprintf to safely convert any type (named string types, structs, etc.)
1412                        // to string. This handles both *FinishReason (named type) and *FormatMetadata
1413                        // (struct types) without compile errors.
1414                        let _ = writeln!(out, "\t\t{local_var} = fmt.Sprintf(\"%v\", *{field_expr})");
1415                        let _ = writeln!(out, "\t}}");
1416                    }
1417                    optional_locals.insert(f.clone(), local_var);
1418                }
1419            }
1420        }
1421    }
1422
1423    // Emit assertions, wrapping in nil guards when an intermediate path segment is optional.
1424    for assertion in &fixture.assertions {
1425        if let Some(f) = &assertion.field {
1426            if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
1427                // Check if any prefix of the dotted path is optional (pointer in Go).
1428                // e.g., "document.nodes" — if "document" is optional, guard the whole block.
1429                let parts: Vec<&str> = f.split('.').collect();
1430                let mut guard_expr: Option<String> = None;
1431                for i in 1..parts.len() {
1432                    let prefix = parts[..i].join(".");
1433                    let resolved_prefix = field_resolver.resolve(&prefix);
1434                    if field_resolver.is_optional(resolved_prefix) {
1435                        // If the prefix ends with a numeric index (e.g. "segments[0]"),
1436                        // the element itself is a value type in Go — it cannot be nil.
1437                        // Use the array field without the index (e.g. "segments") as the
1438                        // nil guard instead, so we emit `result.Segments != nil` rather
1439                        // than the invalid `result.Segments[0] != nil`.
1440                        let guard_prefix = if let Some(bracket_pos) = resolved_prefix.rfind('[') {
1441                            let suffix = &resolved_prefix[bracket_pos + 1..];
1442                            let is_numeric_index = suffix.trim_end_matches(']').chars().all(|c| c.is_ascii_digit());
1443                            if is_numeric_index {
1444                                &resolved_prefix[..bracket_pos]
1445                            } else {
1446                                resolved_prefix
1447                            }
1448                        } else {
1449                            resolved_prefix
1450                        };
1451                        let accessor = field_resolver.accessor(guard_prefix, "go", &effective_result_var);
1452                        guard_expr = Some(accessor);
1453                        break;
1454                    }
1455                }
1456                if let Some(guard) = guard_expr {
1457                    // Only emit nil guard if the assertion will actually produce code
1458                    // (not just a skip comment), to avoid empty branches (SA9003).
1459                    if field_resolver.is_valid_for_result(f) {
1460                        // For Go, avoid emitting nil checks on struct value types.
1461                        // In Go, struct types that are not wrapped in Option<T> in Rust
1462                        // remain value types in the Go binding, so they cannot be nil.
1463                        // Skip the guard if the expression is a direct struct field access
1464                        // (no array indexing, no map keys, no function calls).
1465                        let is_struct_value = !guard.contains('[') && !guard.contains('(') && !guard.contains("map");
1466                        if is_struct_value {
1467                            // Guard refers to a struct value type — skip the nil check
1468                            // and render the assertion directly.
1469                            render_assertion(
1470                                out,
1471                                assertion,
1472                                &effective_result_var,
1473                                import_alias,
1474                                field_resolver,
1475                                &optional_locals,
1476                                result_is_simple,
1477                                result_is_array,
1478                                is_streaming,
1479                            );
1480                            continue;
1481                        }
1482                        let _ = writeln!(out, "\tif {guard} != nil {{");
1483                        // Render into a temporary buffer so we can re-indent by one
1484                        // tab level to sit inside the nil-guard block.
1485                        let mut nil_buf = String::new();
1486                        render_assertion(
1487                            &mut nil_buf,
1488                            assertion,
1489                            &effective_result_var,
1490                            import_alias,
1491                            field_resolver,
1492                            &optional_locals,
1493                            result_is_simple,
1494                            result_is_array,
1495                            is_streaming,
1496                        );
1497                        for line in nil_buf.lines() {
1498                            let _ = writeln!(out, "\t{line}");
1499                        }
1500                        let _ = writeln!(out, "\t}}");
1501                    } else {
1502                        render_assertion(
1503                            out,
1504                            assertion,
1505                            &effective_result_var,
1506                            import_alias,
1507                            field_resolver,
1508                            &optional_locals,
1509                            result_is_simple,
1510                            result_is_array,
1511                            is_streaming,
1512                        );
1513                    }
1514                    continue;
1515                }
1516            }
1517        }
1518        render_assertion(
1519            out,
1520            assertion,
1521            &effective_result_var,
1522            import_alias,
1523            field_resolver,
1524            &optional_locals,
1525            result_is_simple,
1526            result_is_array,
1527            is_streaming,
1528        );
1529    }
1530
1531    let _ = writeln!(out, "}}");
1532}
1533
1534/// Render an HTTP server test function using net/http against MOCK_SERVER_URL.
1535///
1536/// Delegates to the shared driver [`client::http_call::render_http_test`] via
1537/// [`GoTestClientRenderer`]. The emitted test shape is unchanged: `func Test_<Name>(t *testing.T)`
1538/// with a `net/http` client that hits `$MOCK_SERVER_URL/fixtures/<id>`.
1539fn render_http_test_function(out: &mut String, fixture: &Fixture) {
1540    client::http_call::render_http_test(out, &GoTestClientRenderer, fixture);
1541}
1542
1543// ---------------------------------------------------------------------------
1544// HTTP test rendering — GoTestClientRenderer
1545// ---------------------------------------------------------------------------
1546
1547/// Go `net/http` test renderer.
1548///
1549/// Go HTTP e2e tests send a request to `$MOCK_SERVER_URL/fixtures/<id>` using
1550/// the standard library `net/http` client. The trait primitives emit the
1551/// request-build, response-capture, and assertion code that the previous
1552/// monolithic renderer produced, so generated output is unchanged after the
1553/// migration.
1554struct GoTestClientRenderer;
1555
1556impl client::TestClientRenderer for GoTestClientRenderer {
1557    fn language_name(&self) -> &'static str {
1558        "go"
1559    }
1560
1561    /// Go test names use `UpperCamelCase` so they form valid exported identifiers
1562    /// (e.g. `Test_MyFixtureId`). Override the default `sanitize_ident` which
1563    /// produces `lower_snake_case`.
1564    fn sanitize_test_name(&self, id: &str) -> String {
1565        id.to_upper_camel_case()
1566    }
1567
1568    /// Emit `func Test_<fn_name>(t *testing.T) {`, a description comment, and the
1569    /// `baseURL` / request scaffolding. Skipped fixtures get `t.Skip(...)` inline.
1570    fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
1571        let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1572        let _ = writeln!(out, "\t// {description}");
1573        if let Some(reason) = skip_reason {
1574            let escaped = go_string_literal(reason);
1575            let _ = writeln!(out, "\tt.Skip({escaped})");
1576        }
1577    }
1578
1579    fn render_test_close(&self, out: &mut String) {
1580        let _ = writeln!(out, "}}");
1581    }
1582
1583    /// Emit the full `net/http` request scaffolding: URL construction, body,
1584    /// headers, cookies, a no-redirect client, and `io.ReadAll` for the body.
1585    ///
1586    /// `bodyBytes` is always declared (with `_ = bodyBytes` to avoid the Go
1587    /// "declared and not used" compile error on tests with no body assertion).
1588    fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
1589        let method = ctx.method.to_uppercase();
1590        let path = ctx.path;
1591
1592        let _ = writeln!(out, "\tbaseURL := os.Getenv(\"MOCK_SERVER_URL\")");
1593        let _ = writeln!(out, "\tif baseURL == \"\" {{");
1594        let _ = writeln!(out, "\t\tbaseURL = \"http://localhost:8080\"");
1595        let _ = writeln!(out, "\t}}");
1596
1597        // Build request body expression.
1598        let body_expr = if let Some(body) = ctx.body {
1599            let json = serde_json::to_string(body).unwrap_or_default();
1600            let escaped = go_string_literal(&json);
1601            format!("strings.NewReader({})", escaped)
1602        } else {
1603            "strings.NewReader(\"\")".to_string()
1604        };
1605
1606        let _ = writeln!(out, "\tbody := {body_expr}");
1607        let _ = writeln!(
1608            out,
1609            "\treq, err := http.NewRequest(\"{method}\", baseURL+\"{path}\", body)"
1610        );
1611        let _ = writeln!(out, "\tif err != nil {{");
1612        let _ = writeln!(out, "\t\tt.Fatalf(\"new request failed: %v\", err)");
1613        let _ = writeln!(out, "\t}}");
1614
1615        // Content-Type header (only when a body is present).
1616        if ctx.body.is_some() {
1617            let content_type = ctx.content_type.unwrap_or("application/json");
1618            let _ = writeln!(out, "\treq.Header.Set(\"Content-Type\", \"{content_type}\")");
1619        }
1620
1621        // Explicit request headers (sorted for deterministic output).
1622        let mut header_names: Vec<&String> = ctx.headers.keys().collect();
1623        header_names.sort();
1624        for name in header_names {
1625            let value = &ctx.headers[name];
1626            let escaped_name = go_string_literal(name);
1627            let escaped_value = go_string_literal(value);
1628            let _ = writeln!(out, "\treq.Header.Set({escaped_name}, {escaped_value})");
1629        }
1630
1631        // Cookies.
1632        if !ctx.cookies.is_empty() {
1633            let mut cookie_names: Vec<&String> = ctx.cookies.keys().collect();
1634            cookie_names.sort();
1635            for name in cookie_names {
1636                let value = &ctx.cookies[name];
1637                let escaped_name = go_string_literal(name);
1638                let escaped_value = go_string_literal(value);
1639                let _ = writeln!(
1640                    out,
1641                    "\treq.AddCookie(&http.Cookie{{Name: {escaped_name}, Value: {escaped_value}}})"
1642                );
1643            }
1644        }
1645
1646        // No-redirect client so 3xx fixtures assert the redirect response itself.
1647        let _ = writeln!(out, "\tnoRedirectClient := &http.Client{{");
1648        let _ = writeln!(
1649            out,
1650            "\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {{"
1651        );
1652        let _ = writeln!(out, "\t\t\treturn http.ErrUseLastResponse");
1653        let _ = writeln!(out, "\t\t}},");
1654        let _ = writeln!(out, "\t}}");
1655        let _ = writeln!(out, "\tresp, err := noRedirectClient.Do(req)");
1656        let _ = writeln!(out, "\tif err != nil {{");
1657        let _ = writeln!(out, "\t\tt.Fatalf(\"request failed: %v\", err)");
1658        let _ = writeln!(out, "\t}}");
1659        let _ = writeln!(out, "\tdefer resp.Body.Close()");
1660
1661        // Always read the response body so body-assertion methods can reference
1662        // `bodyBytes`. Suppress the "declared and not used" compile error with
1663        // `_ = bodyBytes` for tests that have no body assertion.
1664        let _ = writeln!(out, "\tbodyBytes, err := io.ReadAll(resp.Body)");
1665        let _ = writeln!(out, "\tif err != nil {{");
1666        let _ = writeln!(out, "\t\tt.Fatalf(\"read body failed: %v\", err)");
1667        let _ = writeln!(out, "\t}}");
1668        let _ = writeln!(out, "\t_ = bodyBytes");
1669    }
1670
1671    fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
1672        let _ = writeln!(out, "\tif resp.StatusCode != {status} {{");
1673        let _ = writeln!(out, "\t\tt.Fatalf(\"status: got %d want {status}\", resp.StatusCode)");
1674        let _ = writeln!(out, "\t}}");
1675    }
1676
1677    /// Emit a header assertion, skipping special tokens (`<<present>>`, `<<absent>>`,
1678    /// `<<uuid>>`) and hop-by-hop headers (`Connection`) that `net/http` strips.
1679    fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
1680        // Skip special-token assertions.
1681        if matches!(expected, "<<absent>>" | "<<present>>" | "<<uuid>>") {
1682            return;
1683        }
1684        // Connection is a hop-by-hop header that Go's net/http strips.
1685        if name.eq_ignore_ascii_case("connection") {
1686            return;
1687        }
1688        let escaped_name = go_string_literal(name);
1689        let escaped_value = go_string_literal(expected);
1690        let _ = writeln!(
1691            out,
1692            "\tif !strings.Contains(resp.Header.Get({escaped_name}), {escaped_value}) {{"
1693        );
1694        let _ = writeln!(
1695            out,
1696            "\t\tt.Fatalf(\"header %s mismatch: got %q want to contain %q\", {escaped_name}, resp.Header.Get({escaped_name}), {escaped_value})"
1697        );
1698        let _ = writeln!(out, "\t}}");
1699    }
1700
1701    /// Emit an exact-equality body assertion.
1702    ///
1703    /// JSON objects and arrays are round-tripped via `json.Unmarshal` + `reflect.DeepEqual`.
1704    /// Scalar values are compared as trimmed strings.
1705    fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1706        match expected {
1707            serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
1708                let json_str = serde_json::to_string(expected).unwrap_or_default();
1709                let escaped = go_string_literal(&json_str);
1710                let _ = writeln!(out, "\tvar got any");
1711                let _ = writeln!(out, "\tvar want any");
1712                let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &got); err != nil {{");
1713                let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal got: %v\", err)");
1714                let _ = writeln!(out, "\t}}");
1715                let _ = writeln!(
1716                    out,
1717                    "\tif err := json.Unmarshal([]byte({escaped}), &want); err != nil {{"
1718                );
1719                let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal want: %v\", err)");
1720                let _ = writeln!(out, "\t}}");
1721                let _ = writeln!(out, "\tif !reflect.DeepEqual(got, want) {{");
1722                let _ = writeln!(out, "\t\tt.Fatalf(\"body mismatch: got %v want %v\", got, want)");
1723                let _ = writeln!(out, "\t}}");
1724            }
1725            serde_json::Value::String(s) => {
1726                let escaped = go_string_literal(s);
1727                let _ = writeln!(out, "\twant := {escaped}");
1728                let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1729                let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1730                let _ = writeln!(out, "\t}}");
1731            }
1732            other => {
1733                let escaped = go_string_literal(&other.to_string());
1734                let _ = writeln!(out, "\twant := {escaped}");
1735                let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1736                let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1737                let _ = writeln!(out, "\t}}");
1738            }
1739        }
1740    }
1741
1742    /// Emit partial-body assertions: every key in `expected` must appear in the
1743    /// parsed JSON response with the matching value.
1744    fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1745        if let Some(obj) = expected.as_object() {
1746            let _ = writeln!(out, "\tvar _partialGot map[string]any");
1747            let _ = writeln!(
1748                out,
1749                "\tif err := json.Unmarshal(bodyBytes, &_partialGot); err != nil {{"
1750            );
1751            let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal partial: %v\", err)");
1752            let _ = writeln!(out, "\t}}");
1753            for (key, val) in obj {
1754                let escaped_key = go_string_literal(key);
1755                let json_val = serde_json::to_string(val).unwrap_or_default();
1756                let escaped_val = go_string_literal(&json_val);
1757                let _ = writeln!(out, "\t{{");
1758                let _ = writeln!(out, "\t\tvar _wantVal any");
1759                let _ = writeln!(
1760                    out,
1761                    "\t\tif err := json.Unmarshal([]byte({escaped_val}), &_wantVal); err != nil {{"
1762                );
1763                let _ = writeln!(out, "\t\t\tt.Fatalf(\"json unmarshal partial want: %v\", err)");
1764                let _ = writeln!(out, "\t\t}}");
1765                let _ = writeln!(
1766                    out,
1767                    "\t\tif !reflect.DeepEqual(_partialGot[{escaped_key}], _wantVal) {{"
1768                );
1769                let _ = writeln!(
1770                    out,
1771                    "\t\t\tt.Fatalf(\"partial body field {key}: got %v want %v\", _partialGot[{escaped_key}], _wantVal)"
1772                );
1773                let _ = writeln!(out, "\t\t}}");
1774                let _ = writeln!(out, "\t}}");
1775            }
1776        }
1777    }
1778
1779    /// Emit validation-error assertions for 422 responses.
1780    ///
1781    /// Checks that each expected `msg` appears in at least one element of the
1782    /// parsed body's `"errors"` array.
1783    fn render_assert_validation_errors(
1784        &self,
1785        out: &mut String,
1786        _response_var: &str,
1787        errors: &[ValidationErrorExpectation],
1788    ) {
1789        let _ = writeln!(out, "\tvar _veBody map[string]any");
1790        let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &_veBody); err != nil {{");
1791        let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal validation errors: %v\", err)");
1792        let _ = writeln!(out, "\t}}");
1793        let _ = writeln!(out, "\t_veErrors, _ := _veBody[\"errors\"].([]any)");
1794        for ve in errors {
1795            let escaped_msg = go_string_literal(&ve.msg);
1796            let _ = writeln!(out, "\t{{");
1797            let _ = writeln!(out, "\t\t_found := false");
1798            let _ = writeln!(out, "\t\tfor _, _e := range _veErrors {{");
1799            let _ = writeln!(out, "\t\t\tif _em, ok := _e.(map[string]any); ok {{");
1800            let _ = writeln!(
1801                out,
1802                "\t\t\t\tif _msg, ok := _em[\"msg\"].(string); ok && strings.Contains(_msg, {escaped_msg}) {{"
1803            );
1804            let _ = writeln!(out, "\t\t\t\t\t_found = true");
1805            let _ = writeln!(out, "\t\t\t\t\tbreak");
1806            let _ = writeln!(out, "\t\t\t\t}}");
1807            let _ = writeln!(out, "\t\t\t}}");
1808            let _ = writeln!(out, "\t\t}}");
1809            let _ = writeln!(out, "\t\tif !_found {{");
1810            let _ = writeln!(
1811                out,
1812                "\t\t\tt.Fatalf(\"validation error with msg containing %q not found in errors\", {escaped_msg})"
1813            );
1814            let _ = writeln!(out, "\t\t}}");
1815            let _ = writeln!(out, "\t}}");
1816        }
1817    }
1818}
1819
1820/// Build setup lines (e.g. handle creation) and the argument list for the function call.
1821///
1822/// Returns `(setup_lines, args_string)`.
1823///
1824/// `options_ptr` — when `true`, `json_object` args with an `options_type` are
1825/// passed as a Go pointer (`*OptionsType`): absent/empty → `nil`, present →
1826/// `&varName` after JSON unmarshal.
1827fn build_args_and_setup(
1828    input: &serde_json::Value,
1829    args: &[crate::config::ArgMapping],
1830    import_alias: &str,
1831    options_type: Option<&str>,
1832    fixture: &crate::fixture::Fixture,
1833    options_ptr: bool,
1834    expects_error: bool,
1835) -> (Vec<String>, String) {
1836    let fixture_id = &fixture.id;
1837    use heck::ToUpperCamelCase;
1838
1839    if args.is_empty() {
1840        return (Vec::new(), String::new());
1841    }
1842
1843    let mut setup_lines: Vec<String> = Vec::new();
1844    let mut parts: Vec<String> = Vec::new();
1845
1846    for arg in args {
1847        if arg.arg_type == "mock_url" {
1848            if fixture.has_host_root_route() {
1849                let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1850                setup_lines.push(format!("{} := os.Getenv(\"{env_key}\")", arg.name));
1851                setup_lines.push(format!(
1852                    "if {} == \"\" {{ {} = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\" }}",
1853                    arg.name, arg.name
1854                ));
1855            } else {
1856                setup_lines.push(format!(
1857                    "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
1858                    arg.name,
1859                ));
1860            }
1861            parts.push(arg.name.clone());
1862            continue;
1863        }
1864
1865        if arg.arg_type == "mock_url_list" {
1866            // []string of URLs: each element is either a bare path (`/seed1`) — prefixed
1867            // with the per-fixture mock-server URL at runtime — or an absolute URL kept as-is.
1868            // Mirrors the `mock_url` resolution: `MOCK_SERVER_<FIXTURE_ID>` first, then
1869            // `MOCK_SERVER_URL/fixtures/<id>`.
1870            let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1871            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1872            let val = input.get(field).unwrap_or(&serde_json::Value::Null);
1873
1874            let paths: Vec<String> = if let Some(arr) = val.as_array() {
1875                arr.iter().filter_map(|v| v.as_str().map(go_string_literal)).collect()
1876            } else {
1877                Vec::new()
1878            };
1879
1880            let paths_literal = paths.join(", ");
1881            let var_name = &arg.name;
1882
1883            setup_lines.push(format!(
1884                "{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}}"
1885            ));
1886            setup_lines.push(format!(
1887                "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}}"
1888            ));
1889            parts.push(var_name.to_string());
1890            continue;
1891        }
1892
1893        if arg.arg_type == "handle" {
1894            // Generate a CreateEngine (or equivalent) call and pass the variable.
1895            let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
1896            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1897            let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
1898            // When the fixture expects an error (validation test), engine creation
1899            // is the error source. Assert the error and return so the test passes
1900            // without proceeding to the (unreachable) function call.
1901            let create_err_handler = if expects_error {
1902                "assert.Error(t, createErr)\n\t\treturn".to_string()
1903            } else {
1904                "t.Fatalf(\"create handle failed: %v\", createErr)".to_string()
1905            };
1906            if config_value.is_null()
1907                || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
1908            {
1909                setup_lines.push(format!(
1910                    "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}",
1911                    name = arg.name,
1912                ));
1913            } else {
1914                let json_str = serde_json::to_string(config_value).unwrap_or_default();
1915                let go_literal = go_string_literal(&json_str);
1916                let name = &arg.name;
1917                setup_lines.push(format!(
1918                    "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}}"
1919                ));
1920                setup_lines.push(format!(
1921                    "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}"
1922                ));
1923            }
1924            parts.push(arg.name.clone());
1925            continue;
1926        }
1927
1928        let val: Option<&serde_json::Value> = if arg.field == "input" {
1929            Some(input)
1930        } else {
1931            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1932            input.get(field)
1933        };
1934
1935        // file_path args are fixture-relative paths under `test_documents/`. The Go test
1936        // runner's TestMain (in main_test.go) already does `os.Chdir(test_documents)` so
1937        // tests can pass these relative strings directly; no additional resolution needed.
1938
1939        // Handle bytes type: fixture stores base64-encoded bytes.
1940        // Emit a Go base64.StdEncoding.DecodeString call to decode at runtime.
1941        if arg.arg_type == "bytes" {
1942            let var_name = format!("{}Bytes", arg.name);
1943            match val {
1944                None | Some(serde_json::Value::Null) => {
1945                    if arg.optional {
1946                        parts.push("nil".to_string());
1947                    } else {
1948                        parts.push("[]byte{}".to_string());
1949                    }
1950                }
1951                Some(serde_json::Value::String(s)) => {
1952                    // Bytes args whose fixture value is a string are fixture-relative paths into
1953                    // the repo-root `test_documents/` directory (matching the rust e2e codegen
1954                    // convention). The Go test runner's TestMain chdirs into test_documents/, so
1955                    // a bare relative path resolves correctly via os.ReadFile.
1956                    let go_path = go_string_literal(s);
1957                    setup_lines.push(format!(
1958                        "{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}}"
1959                    ));
1960                    parts.push(var_name);
1961                }
1962                Some(other) => {
1963                    parts.push(format!("[]byte({})", json_to_go(other)));
1964                }
1965            }
1966            continue;
1967        }
1968
1969        match val {
1970            None | Some(serde_json::Value::Null) if arg.optional => {
1971                // Optional arg absent: emit Go zero/nil for the type.
1972                match arg.arg_type.as_str() {
1973                    "string" => {
1974                        // Optional string in Go bindings is *string → nil.
1975                        parts.push("nil".to_string());
1976                    }
1977                    "json_object" => {
1978                        if options_ptr {
1979                            // Pointer options type (*OptionsType): absent → nil.
1980                            parts.push("nil".to_string());
1981                        } else if let Some(opts_type) = options_type {
1982                            // Value options type: zero-value struct.
1983                            parts.push(format!("{import_alias}.{opts_type}{{}}"));
1984                        } else {
1985                            parts.push("nil".to_string());
1986                        }
1987                    }
1988                    _ => {
1989                        parts.push("nil".to_string());
1990                    }
1991                }
1992            }
1993            None | Some(serde_json::Value::Null) => {
1994                // Required arg with no fixture value: pass a language-appropriate default.
1995                let default_val = match arg.arg_type.as_str() {
1996                    "string" => "\"\"".to_string(),
1997                    "int" | "integer" | "i64" => "0".to_string(),
1998                    "float" | "number" => "0.0".to_string(),
1999                    "bool" | "boolean" => "false".to_string(),
2000                    "json_object" => {
2001                        if options_ptr {
2002                            // Pointer options type (*OptionsType): absent → nil.
2003                            "nil".to_string()
2004                        } else if let Some(opts_type) = options_type {
2005                            format!("{import_alias}.{opts_type}{{}}")
2006                        } else {
2007                            "nil".to_string()
2008                        }
2009                    }
2010                    _ => "nil".to_string(),
2011                };
2012                parts.push(default_val);
2013            }
2014            Some(v) => {
2015                match arg.arg_type.as_str() {
2016                    "json_object" => {
2017                        // JSON arrays unmarshal into []string (Go slices).
2018                        // JSON objects with a known options_type unmarshal into that type.
2019                        let is_array = v.is_array();
2020                        let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
2021                        if is_empty_obj {
2022                            if options_ptr {
2023                                // Pointer options type: empty object → nil.
2024                                parts.push("nil".to_string());
2025                            } else if let Some(opts_type) = options_type {
2026                                parts.push(format!("{import_alias}.{opts_type}{{}}"));
2027                            } else {
2028                                parts.push("nil".to_string());
2029                            }
2030                        } else if is_array {
2031                            // Array type — unmarshal into a Go slice. Honor `go_type` for a
2032                            // fully explicit Go type (e.g. `"kreuzberg.BatchBytesItem"`), fall
2033                            // back to deriving the slice type from `element_type`, defaulting
2034                            // to `[]string` for unknown types.
2035                            let go_slice_type = if let Some(go_t) = arg.go_type.as_deref() {
2036                                // go_type is the slice element type — wrap it in [].
2037                                // If it already starts with '[' the user specified the full
2038                                // slice type; use it verbatim.
2039                                if go_t.starts_with('[') {
2040                                    go_t.to_string()
2041                                } else {
2042                                    // Qualify unqualified types (e.g., "BatchBytesItem" → "kreuzberg.BatchBytesItem")
2043                                    let qualified = if go_t.contains('.') {
2044                                        go_t.to_string()
2045                                    } else {
2046                                        format!("{import_alias}.{go_t}")
2047                                    };
2048                                    format!("[]{qualified}")
2049                                }
2050                            } else {
2051                                element_type_to_go_slice(arg.element_type.as_deref(), import_alias)
2052                            };
2053                            // Convert JSON for Go compatibility (e.g., byte arrays → base64 strings)
2054                            let converted_v = convert_json_for_go(v.clone());
2055                            let json_str = serde_json::to_string(&converted_v).unwrap_or_default();
2056                            let go_literal = go_string_literal(&json_str);
2057                            let var_name = &arg.name;
2058                            setup_lines.push(format!(
2059                                "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}}"
2060                            ));
2061                            parts.push(var_name.to_string());
2062                        } else if let Some(opts_type) = options_type {
2063                            // Object with known type — unmarshal into typed struct.
2064                            // When options_ptr is set, the Go struct uses snake_case JSON
2065                            // field tags and lowercase/snake_case enum values.  Remap the
2066                            // fixture's camelCase keys and PascalCase enum string values.
2067                            let remapped_v = if options_ptr {
2068                                convert_json_for_go(v.clone())
2069                            } else {
2070                                v.clone()
2071                            };
2072                            let json_str = serde_json::to_string(&remapped_v).unwrap_or_default();
2073                            let go_literal = go_string_literal(&json_str);
2074                            let var_name = &arg.name;
2075                            setup_lines.push(format!(
2076                                "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}}"
2077                            ));
2078                            // Pass as pointer when options_ptr is set.
2079                            let arg_expr = if options_ptr {
2080                                format!("&{var_name}")
2081                            } else {
2082                                var_name.to_string()
2083                            };
2084                            parts.push(arg_expr);
2085                        } else {
2086                            parts.push(json_to_go(v));
2087                        }
2088                    }
2089                    "string" if arg.optional => {
2090                        // Optional string in Go is *string — take address of a local.
2091                        let var_name = format!("{}Val", arg.name);
2092                        let go_val = json_to_go(v);
2093                        setup_lines.push(format!("{var_name} := {go_val}"));
2094                        parts.push(format!("&{var_name}"));
2095                    }
2096                    _ => {
2097                        parts.push(json_to_go(v));
2098                    }
2099                }
2100            }
2101        }
2102    }
2103
2104    (setup_lines, parts.join(", "))
2105}
2106
2107#[allow(clippy::too_many_arguments)]
2108fn render_assertion(
2109    out: &mut String,
2110    assertion: &Assertion,
2111    result_var: &str,
2112    import_alias: &str,
2113    field_resolver: &FieldResolver,
2114    optional_locals: &std::collections::HashMap<String, String>,
2115    result_is_simple: bool,
2116    result_is_array: bool,
2117    is_streaming: bool,
2118) {
2119    // Handle synthetic / derived fields before the is_valid_for_result check
2120    // so they are never treated as struct field accesses on the result.
2121    if !result_is_simple {
2122        if let Some(f) = &assertion.field {
2123            // embed_texts returns *[][]float32; the embedding matrix is *result_var.
2124            // We emit inline func() expressions so we don't need additional variables.
2125            let embed_deref = format!("(*{result_var})");
2126            match f.as_str() {
2127                "chunks_have_content" => {
2128                    let pred = format!(
2129                        "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range chunks {{ if c.Content == \"\" {{ return false }} }}; return true }}()"
2130                    );
2131                    match assertion.assertion_type.as_str() {
2132                        "is_true" => {
2133                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2134                        }
2135                        "is_false" => {
2136                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2137                        }
2138                        _ => {
2139                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2140                        }
2141                    }
2142                    return;
2143                }
2144                "chunks_have_embeddings" => {
2145                    let pred = format!(
2146                        "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 }}()"
2147                    );
2148                    match assertion.assertion_type.as_str() {
2149                        "is_true" => {
2150                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2151                        }
2152                        "is_false" => {
2153                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2154                        }
2155                        _ => {
2156                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2157                        }
2158                    }
2159                    return;
2160                }
2161                "chunks_have_heading_context" => {
2162                    let pred = format!(
2163                        "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range chunks {{ if c.Metadata.HeadingContext == nil {{ return false }} }}; return true }}()"
2164                    );
2165                    match assertion.assertion_type.as_str() {
2166                        "is_true" => {
2167                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2168                        }
2169                        "is_false" => {
2170                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2171                        }
2172                        _ => {
2173                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2174                        }
2175                    }
2176                    return;
2177                }
2178                "first_chunk_starts_with_heading" => {
2179                    let pred = format!(
2180                        "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil || len(chunks) == 0 {{ return false }}; return chunks[0].Metadata.HeadingContext != nil }}()"
2181                    );
2182                    match assertion.assertion_type.as_str() {
2183                        "is_true" => {
2184                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2185                        }
2186                        "is_false" => {
2187                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2188                        }
2189                        _ => {
2190                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2191                        }
2192                    }
2193                    return;
2194                }
2195                "embeddings" => {
2196                    match assertion.assertion_type.as_str() {
2197                        "count_equals" => {
2198                            if let Some(val) = &assertion.value {
2199                                if let Some(n) = val.as_u64() {
2200                                    let _ = writeln!(
2201                                        out,
2202                                        "\tassert.Equal(t, {n}, len({embed_deref}), \"expected exactly {n} elements\")"
2203                                    );
2204                                }
2205                            }
2206                        }
2207                        "count_min" => {
2208                            if let Some(val) = &assertion.value {
2209                                if let Some(n) = val.as_u64() {
2210                                    let _ = writeln!(
2211                                        out,
2212                                        "\tassert.GreaterOrEqual(t, len({embed_deref}), {n}, \"expected at least {n} elements\")"
2213                                    );
2214                                }
2215                            }
2216                        }
2217                        "not_empty" => {
2218                            let _ = writeln!(
2219                                out,
2220                                "\tassert.NotEmpty(t, {embed_deref}, \"expected non-empty embeddings\")"
2221                            );
2222                        }
2223                        "is_empty" => {
2224                            let _ = writeln!(out, "\tassert.Empty(t, {embed_deref}, \"expected empty embeddings\")");
2225                        }
2226                        _ => {
2227                            let _ = writeln!(
2228                                out,
2229                                "\t// skipped: unsupported assertion type on synthetic field 'embeddings'"
2230                            );
2231                        }
2232                    }
2233                    return;
2234                }
2235                "embedding_dimensions" => {
2236                    let expr = format!(
2237                        "func() int {{ if len({embed_deref}) == 0 {{ return 0 }}; return len({embed_deref}[0]) }}()"
2238                    );
2239                    match assertion.assertion_type.as_str() {
2240                        "equals" => {
2241                            if let Some(val) = &assertion.value {
2242                                if let Some(n) = val.as_u64() {
2243                                    let _ = writeln!(
2244                                        out,
2245                                        "\tif {expr} != {n} {{\n\t\tt.Errorf(\"equals mismatch: got %v\", {expr})\n\t}}"
2246                                    );
2247                                }
2248                            }
2249                        }
2250                        "greater_than" => {
2251                            if let Some(val) = &assertion.value {
2252                                if let Some(n) = val.as_u64() {
2253                                    let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2254                                }
2255                            }
2256                        }
2257                        _ => {
2258                            let _ = writeln!(
2259                                out,
2260                                "\t// skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
2261                            );
2262                        }
2263                    }
2264                    return;
2265                }
2266                "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
2267                    let pred = match f.as_str() {
2268                        "embeddings_valid" => {
2269                            format!(
2270                                "func() bool {{ for _, e := range {embed_deref} {{ if len(e) == 0 {{ return false }} }}; return true }}()"
2271                            )
2272                        }
2273                        "embeddings_finite" => {
2274                            format!(
2275                                "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 }}()"
2276                            )
2277                        }
2278                        "embeddings_non_zero" => {
2279                            format!(
2280                                "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 }}()"
2281                            )
2282                        }
2283                        "embeddings_normalized" => {
2284                            format!(
2285                                "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 }}()"
2286                            )
2287                        }
2288                        _ => unreachable!(),
2289                    };
2290                    match assertion.assertion_type.as_str() {
2291                        "is_true" => {
2292                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2293                        }
2294                        "is_false" => {
2295                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2296                        }
2297                        _ => {
2298                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2299                        }
2300                    }
2301                    return;
2302                }
2303                // ---- keywords / keywords_count ----
2304                // Go ExtractionResult does not expose extracted_keywords; skip.
2305                "keywords" | "keywords_count" => {
2306                    let _ = writeln!(out, "\t// skipped: field '{f}' not available on Go ExtractionResult");
2307                    return;
2308                }
2309                _ => {}
2310            }
2311        }
2312    }
2313
2314    // Streaming virtual fields: intercept before is_valid_for_result so they are
2315    // never skipped.  These fields resolve against the `chunks` collected-list variable.
2316    // Skip the streaming interception entirely when the call has opted out
2317    // (`[e2e.calls.<name>] streaming = false`) — `chunks` then names a plain
2318    // field on the synchronous result struct and must flow through normal
2319    // accessor resolution (e.g. `result.Chunks`).
2320    if !result_is_simple && is_streaming {
2321        if let Some(f) = &assertion.field {
2322            if !f.is_empty() && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
2323                if let Some(expr) =
2324                    crate::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, "go", "chunks")
2325                {
2326                    match assertion.assertion_type.as_str() {
2327                        "count_min" => {
2328                            if let Some(val) = &assertion.value {
2329                                if let Some(n) = val.as_u64() {
2330                                    let _ = writeln!(
2331                                        out,
2332                                        "\tassert.GreaterOrEqual(t, len({expr}), {n}, \"expected >= {n} chunks\")"
2333                                    );
2334                                }
2335                            }
2336                        }
2337                        "count_equals" => {
2338                            if let Some(val) = &assertion.value {
2339                                if let Some(n) = val.as_u64() {
2340                                    let _ = writeln!(
2341                                        out,
2342                                        "\tassert.Equal(t, {n}, len({expr}), \"expected exactly {n} chunks\")"
2343                                    );
2344                                }
2345                            }
2346                        }
2347                        "equals" => {
2348                            if let Some(serde_json::Value::String(s)) = &assertion.value {
2349                                let escaped = crate::escape::go_string_literal(s);
2350                                // Deep-path streaming-virtual fields like `tool_calls[0].function.name`
2351                                // resolve to pointer-typed Go fields (`*string`). The flat virtual
2352                                // accessors `stream_content` / `finish_reason` already return `string`.
2353                                // Wrap only the deep-path case in a safe-deref IIFE.
2354                                let is_deep_path = f.contains('.') || f.contains('[');
2355                                let safe_expr = if is_deep_path {
2356                                    format!(
2357                                        "func() string {{ v := {expr}; if v == nil {{ return \"\" }}; return *v }}()"
2358                                    )
2359                                } else {
2360                                    expr.clone()
2361                                };
2362                                let _ = writeln!(out, "\tassert.Equal(t, {escaped}, {safe_expr})");
2363                            } else if let Some(val) = &assertion.value {
2364                                if let Some(n) = val.as_u64() {
2365                                    let _ = writeln!(out, "\tassert.Equal(t, {n}, {expr})");
2366                                }
2367                            }
2368                        }
2369                        "not_empty" => {
2370                            let _ = writeln!(out, "\tassert.NotEmpty(t, {expr}, \"expected non-empty\")");
2371                        }
2372                        "is_empty" => {
2373                            let _ = writeln!(out, "\tassert.Empty(t, {expr}, \"expected empty\")");
2374                        }
2375                        "is_true" => {
2376                            let _ = writeln!(out, "\tassert.True(t, {expr}, \"expected true\")");
2377                        }
2378                        "is_false" => {
2379                            let _ = writeln!(out, "\tassert.False(t, {expr}, \"expected false\")");
2380                        }
2381                        "greater_than" => {
2382                            if let Some(val) = &assertion.value {
2383                                if let Some(n) = val.as_u64() {
2384                                    let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2385                                }
2386                            }
2387                        }
2388                        "greater_than_or_equal" => {
2389                            if let Some(val) = &assertion.value {
2390                                if let Some(n) = val.as_u64() {
2391                                    let _ =
2392                                        writeln!(out, "\tassert.GreaterOrEqual(t, {expr}, {n}, \"expected >= {n}\")");
2393                                }
2394                            }
2395                        }
2396                        "contains" => {
2397                            if let Some(serde_json::Value::String(s)) = &assertion.value {
2398                                let escaped = crate::escape::go_string_literal(s);
2399                                let _ =
2400                                    writeln!(out, "\tassert.Contains(t, {expr}, {escaped}, \"expected to contain\")");
2401                            }
2402                        }
2403                        _ => {
2404                            let _ = writeln!(
2405                                out,
2406                                "\t// streaming field '{f}': assertion type '{}' not rendered",
2407                                assertion.assertion_type
2408                            );
2409                        }
2410                    }
2411                }
2412                return;
2413            }
2414        }
2415    }
2416
2417    // Skip assertions on fields that don't exist on the result type.
2418    // When result_is_simple, all field assertions operate on the scalar result directly.
2419    if !result_is_simple {
2420        if let Some(f) = &assertion.field {
2421            if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
2422                let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
2423                return;
2424            }
2425        }
2426    }
2427
2428    let field_expr = if result_is_simple {
2429        // The result IS the value — field access is irrelevant.
2430        result_var.to_string()
2431    } else {
2432        match &assertion.field {
2433            Some(f) if !f.is_empty() => {
2434                // Use the local variable if the field was dereferenced above.
2435                if let Some(local_var) = optional_locals.get(f.as_str()) {
2436                    local_var.clone()
2437                } else {
2438                    field_resolver.accessor(f, "go", result_var)
2439                }
2440            }
2441            _ => result_var.to_string(),
2442        }
2443    };
2444
2445    // Check if the field (after resolution) is optional, which means it's a pointer in Go.
2446    // Also check if a `.length` suffix's parent is optional (e.g., metadata.headings.length
2447    // where metadata.headings is optional → len() needs dereference).
2448    let is_optional = assertion
2449        .field
2450        .as_ref()
2451        .map(|f| {
2452            let resolved = field_resolver.resolve(f);
2453            let check_path = resolved
2454                .strip_suffix(".length")
2455                .or_else(|| resolved.strip_suffix(".count"))
2456                .or_else(|| resolved.strip_suffix(".size"))
2457                .unwrap_or(resolved);
2458            field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
2459        })
2460        .unwrap_or(false);
2461
2462    // When field_expr is `len(X)` and X is an optional (pointer) field, rewrite to `len(*X)`
2463    // and we'll wrap with a nil guard in the assertion handlers.
2464    // However, slices are already nil-able and should not be dereferenced.
2465    let field_is_array_for_len = assertion
2466        .field
2467        .as_ref()
2468        .map(|f| {
2469            let resolved = field_resolver.resolve(f);
2470            let check_path = resolved
2471                .strip_suffix(".length")
2472                .or_else(|| resolved.strip_suffix(".count"))
2473                .or_else(|| resolved.strip_suffix(".size"))
2474                .unwrap_or(resolved);
2475            field_resolver.is_array(check_path)
2476        })
2477        .unwrap_or(false);
2478    let field_expr =
2479        if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') && !field_is_array_for_len {
2480            let inner = &field_expr[4..field_expr.len() - 1];
2481            format!("len(*{inner})")
2482        } else {
2483            field_expr
2484        };
2485    // Build the nil-guard expression for the inner pointer (without len wrapper).
2486    let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
2487        Some(field_expr[5..field_expr.len() - 1].to_string())
2488    } else {
2489        None
2490    };
2491
2492    // For optional non-string fields that weren't dereferenced into locals,
2493    // we need to dereference the pointer in comparisons.
2494    // However, slices are already nil-able and should not be dereferenced.
2495    let field_is_slice = assertion
2496        .field
2497        .as_ref()
2498        .map(|f| field_resolver.is_array(field_resolver.resolve(f)))
2499        .unwrap_or(false);
2500    let deref_field_expr = if is_optional && !field_expr.starts_with("len(") && !field_is_slice {
2501        format!("*{field_expr}")
2502    } else {
2503        field_expr.clone()
2504    };
2505
2506    // Detect array element access (e.g., `result.Assets[0].ContentHash`).
2507    // When the field_expr contains `[0]`, we must guard against an out-of-bounds
2508    // panic by checking that the array is non-empty first.
2509    // Extract the array slice expression (everything before `[0]`).
2510    let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
2511        let mut array_expr = field_expr[..idx].to_string();
2512        if let Some(stripped) = array_expr.strip_prefix("len(") {
2513            array_expr = stripped.to_string();
2514        }
2515        Some(array_expr)
2516    } else {
2517        None
2518    };
2519
2520    // Render the assertion into a temporary buffer first, then wrap with the array
2521    // bounds guard (if needed) by adding one extra level of indentation.
2522    let mut assertion_buf = String::new();
2523    let out_ref = &mut assertion_buf;
2524
2525    match assertion.assertion_type.as_str() {
2526        "equals" => {
2527            if let Some(expected) = &assertion.value {
2528                let go_val = json_to_go(expected);
2529                // For string equality, trim whitespace to handle trailing newlines from the converter.
2530                if expected.is_string() {
2531                    // Wrap field expression with strings.TrimSpace() for string comparisons.
2532                    // Use string() cast to handle named string types (e.g. BatchStatus, FinishReason).
2533                    // For complex types like FormatMetadata, use fmt.Sprintf to convert.
2534                    let resolved_name = assertion
2535                        .field
2536                        .as_ref()
2537                        .map(|f| field_resolver.resolve(f))
2538                        .unwrap_or_default();
2539                    let is_struct = resolved_name.contains("FormatMetadata");
2540                    let trimmed_field = if is_struct {
2541                        // Use fmt.Sprintf for struct types instead of string cast
2542                        if is_optional && !field_expr.starts_with("len(") {
2543                            format!("strings.TrimSpace(fmt.Sprintf(\"%v\", *{field_expr}))")
2544                        } else {
2545                            format!("strings.TrimSpace(fmt.Sprintf(\"%v\", {field_expr}))")
2546                        }
2547                    } else if is_optional && !field_expr.starts_with("len(") {
2548                        format!("strings.TrimSpace(string(*{field_expr}))")
2549                    } else {
2550                        format!("strings.TrimSpace(string({field_expr}))")
2551                    };
2552                    if is_optional && !field_expr.starts_with("len(") {
2553                        let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
2554                    } else {
2555                        let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
2556                    }
2557                } else if is_optional && !field_expr.starts_with("len(") {
2558                    let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
2559                } else {
2560                    let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
2561                }
2562                let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
2563                let _ = writeln!(out_ref, "\t}}");
2564            }
2565        }
2566        "contains" => {
2567            if let Some(expected) = &assertion.value {
2568                let go_val = json_to_go(expected);
2569                // Determine the "string view" of the field expression.
2570                // - []string (optional) → jsonString(field_expr) — Go slices are nil-able, no `*` needed
2571                // - *string → string(*field_expr)
2572                // - string → string(field_expr) (or just field_expr for plain strings)
2573                // - result_is_array (result_is_simple + array result) → jsonString(field_expr)
2574                let resolved_field = assertion.field.as_deref().unwrap_or("");
2575                let resolved_name = field_resolver.resolve(resolved_field);
2576                let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2577                let is_opt =
2578                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2579                let field_for_contains = if is_opt && field_is_array {
2580                    // Go slices are nil-able directly — no pointer dereference needed.
2581                    format!("jsonString({field_expr})")
2582                } else if is_opt {
2583                    format!("fmt.Sprint(*{field_expr})")
2584                } else if field_is_array {
2585                    format!("jsonString({field_expr})")
2586                } else {
2587                    format!("fmt.Sprint({field_expr})")
2588                };
2589                if is_opt {
2590                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2591                    let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2592                    let _ = writeln!(
2593                        out_ref,
2594                        "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2595                    );
2596                    let _ = writeln!(out_ref, "\t}}");
2597                    let _ = writeln!(out_ref, "\t}}");
2598                } else {
2599                    let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2600                    let _ = writeln!(
2601                        out_ref,
2602                        "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2603                    );
2604                    let _ = writeln!(out_ref, "\t}}");
2605                }
2606            }
2607        }
2608        "contains_all" => {
2609            if let Some(values) = &assertion.values {
2610                let resolved_field = assertion.field.as_deref().unwrap_or("");
2611                let resolved_name = field_resolver.resolve(resolved_field);
2612                let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2613                let is_opt =
2614                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2615                for val in values {
2616                    let go_val = json_to_go(val);
2617                    let field_for_contains = if is_opt && field_is_array {
2618                        // Go slices are nil-able directly — no pointer dereference needed.
2619                        format!("jsonString({field_expr})")
2620                    } else if is_opt {
2621                        format!("fmt.Sprint(*{field_expr})")
2622                    } else if field_is_array {
2623                        format!("jsonString({field_expr})")
2624                    } else {
2625                        format!("fmt.Sprint({field_expr})")
2626                    };
2627                    if is_opt {
2628                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
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                        let _ = writeln!(out_ref, "\t}}");
2633                    } else {
2634                        let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2635                        let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2636                        let _ = writeln!(out_ref, "\t}}");
2637                    }
2638                }
2639            }
2640        }
2641        "not_contains" => {
2642            if let Some(expected) = &assertion.value {
2643                let go_val = json_to_go(expected);
2644                let resolved_field = assertion.field.as_deref().unwrap_or("");
2645                let resolved_name = field_resolver.resolve(resolved_field);
2646                let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2647                let is_opt =
2648                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2649                let field_for_contains = if is_opt && field_is_array {
2650                    // Go slices are nil-able directly — no pointer dereference needed.
2651                    format!("jsonString({field_expr})")
2652                } else if is_opt {
2653                    format!("fmt.Sprint(*{field_expr})")
2654                } else if field_is_array {
2655                    format!("jsonString({field_expr})")
2656                } else {
2657                    format!("fmt.Sprint({field_expr})")
2658                };
2659                let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
2660                let _ = writeln!(
2661                    out_ref,
2662                    "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
2663                );
2664                let _ = writeln!(out_ref, "\t}}");
2665            }
2666        }
2667        "not_empty" => {
2668            // For optional struct pointers (not arrays), just check != nil.
2669            // For optional slice/string pointers, check nil and len.
2670            let field_is_array = {
2671                let rf = assertion.field.as_deref().unwrap_or("");
2672                let rn = field_resolver.resolve(rf);
2673                field_resolver.is_array(rn)
2674            };
2675            if is_optional && !field_is_array {
2676                // Struct pointer: non-empty means not nil.
2677                let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
2678            } else if is_optional && field_is_slice {
2679                // Slice optional: Go slices are already nil-able — no dereference needed.
2680                let _ = writeln!(out_ref, "\tif {field_expr} == nil || len({field_expr}) == 0 {{");
2681            } else if is_optional {
2682                // Pointer-to-slice (*[]T): dereference then len.
2683                let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
2684            } else if result_is_simple && result_is_array {
2685                // Simple array result ([]T) — direct slice, not a pointer; check length only.
2686                let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2687            } else {
2688                let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2689            }
2690            let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
2691            let _ = writeln!(out_ref, "\t}}");
2692        }
2693        "is_empty" => {
2694            let field_is_array = {
2695                let rf = assertion.field.as_deref().unwrap_or("");
2696                let rn = field_resolver.resolve(rf);
2697                field_resolver.is_array(rn)
2698            };
2699            // Special case: result_is_simple && !result_is_array && no field means the result is a pointer.
2700            // Empty means nil.
2701            if result_is_simple && !result_is_array && assertion.field.as_ref().is_none_or(|f| f.is_empty()) {
2702                // Pointer result (not dereferenced): empty means nil.
2703                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2704            } else if is_optional && !field_is_array {
2705                // Struct pointer: empty means nil.
2706                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2707            } else if is_optional && field_is_slice {
2708                // Slice optional: Go slices are already nil-able — no dereference needed.
2709                let _ = writeln!(out_ref, "\tif {field_expr} != nil && len({field_expr}) != 0 {{");
2710            } else if is_optional {
2711                // Pointer-to-slice (*[]T): dereference then len.
2712                let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
2713            } else {
2714                let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
2715            }
2716            let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
2717            let _ = writeln!(out_ref, "\t}}");
2718        }
2719        "contains_any" => {
2720            if let Some(values) = &assertion.values {
2721                let resolved_field = assertion.field.as_deref().unwrap_or("");
2722                let resolved_name = field_resolver.resolve(resolved_field);
2723                let field_is_array = field_resolver.is_array(resolved_name);
2724                let is_opt =
2725                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2726                let field_for_contains = if is_opt && field_is_array {
2727                    // Go slices are nil-able directly — no pointer dereference needed.
2728                    format!("jsonString({field_expr})")
2729                } else if is_opt {
2730                    format!("fmt.Sprint(*{field_expr})")
2731                } else if field_is_array {
2732                    format!("jsonString({field_expr})")
2733                } else {
2734                    format!("fmt.Sprint({field_expr})")
2735                };
2736                let _ = writeln!(out_ref, "\t{{");
2737                let _ = writeln!(out_ref, "\t\tfound := false");
2738                for val in values {
2739                    let go_val = json_to_go(val);
2740                    let _ = writeln!(
2741                        out_ref,
2742                        "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
2743                    );
2744                }
2745                let _ = writeln!(out_ref, "\t\tif !found {{");
2746                let _ = writeln!(
2747                    out_ref,
2748                    "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
2749                );
2750                let _ = writeln!(out_ref, "\t\t}}");
2751                let _ = writeln!(out_ref, "\t}}");
2752            }
2753        }
2754        "greater_than" => {
2755            if let Some(val) = &assertion.value {
2756                let go_val = json_to_go(val);
2757                // Use `< N+1` instead of `<= N` to avoid golangci-lint sloppyLen
2758                // warning when N is 0 (len(x) <= 0 → len(x) < 1).
2759                // For optional (pointer) fields, dereference and guard with nil check.
2760                if is_optional {
2761                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2762                    if let Some(n) = val.as_u64() {
2763                        let next = n + 1;
2764                        let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
2765                    } else {
2766                        let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
2767                    }
2768                    let _ = writeln!(
2769                        out_ref,
2770                        "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
2771                    );
2772                    let _ = writeln!(out_ref, "\t\t}}");
2773                    let _ = writeln!(out_ref, "\t}}");
2774                } else if let Some(n) = val.as_u64() {
2775                    let next = n + 1;
2776                    let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
2777                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2778                    let _ = writeln!(out_ref, "\t}}");
2779                } else {
2780                    let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
2781                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2782                    let _ = writeln!(out_ref, "\t}}");
2783                }
2784            }
2785        }
2786        "less_than" => {
2787            if let Some(val) = &assertion.value {
2788                let go_val = json_to_go(val);
2789                if let Some(ref guard) = nil_guard_expr {
2790                    let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2791                    let _ = writeln!(out_ref, "\t\tif {field_expr} >= {go_val} {{");
2792                    let _ = writeln!(out_ref, "\t\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2793                    let _ = writeln!(out_ref, "\t\t}}");
2794                    let _ = writeln!(out_ref, "\t}}");
2795                } else if is_optional && !field_expr.starts_with("len(") {
2796                    // Optional pointer field: nil-guard and dereference before comparison.
2797                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2798                    let _ = writeln!(out_ref, "\t\tif {deref_field_expr} >= {go_val} {{");
2799                    let _ = writeln!(
2800                        out_ref,
2801                        "\t\t\tt.Errorf(\"expected < {go_val}, got %v\", {deref_field_expr})"
2802                    );
2803                    let _ = writeln!(out_ref, "\t\t}}");
2804                    let _ = writeln!(out_ref, "\t}}");
2805                } else {
2806                    let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
2807                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2808                    let _ = writeln!(out_ref, "\t}}");
2809                }
2810            }
2811        }
2812        "greater_than_or_equal" => {
2813            if let Some(val) = &assertion.value {
2814                let go_val = json_to_go(val);
2815                if let Some(ref guard) = nil_guard_expr {
2816                    let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2817                    let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
2818                    let _ = writeln!(
2819                        out_ref,
2820                        "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
2821                    );
2822                    let _ = writeln!(out_ref, "\t\t}}");
2823                    let _ = writeln!(out_ref, "\t}}");
2824                } else if is_optional && !field_expr.starts_with("len(") {
2825                    // Optional pointer field: nil-guard and dereference before comparison.
2826                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2827                    let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
2828                    let _ = writeln!(
2829                        out_ref,
2830                        "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
2831                    );
2832                    let _ = writeln!(out_ref, "\t\t}}");
2833                    let _ = writeln!(out_ref, "\t}}");
2834                } else {
2835                    let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
2836                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
2837                    let _ = writeln!(out_ref, "\t}}");
2838                }
2839            }
2840        }
2841        "less_than_or_equal" => {
2842            if let Some(val) = &assertion.value {
2843                let go_val = json_to_go(val);
2844                if is_optional && !field_expr.starts_with("len(") {
2845                    // Optional pointer field: nil-guard and dereference before comparison.
2846                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2847                    let _ = writeln!(out_ref, "\t\tif {deref_field_expr} > {go_val} {{");
2848                    let _ = writeln!(
2849                        out_ref,
2850                        "\t\t\tt.Errorf(\"expected <= {go_val}, got %v\", {deref_field_expr})"
2851                    );
2852                    let _ = writeln!(out_ref, "\t\t}}");
2853                    let _ = writeln!(out_ref, "\t}}");
2854                } else {
2855                    let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
2856                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
2857                    let _ = writeln!(out_ref, "\t}}");
2858                }
2859            }
2860        }
2861        "starts_with" => {
2862            if let Some(expected) = &assertion.value {
2863                let go_val = json_to_go(expected);
2864                let field_for_prefix = if is_optional
2865                    && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2866                {
2867                    format!("string(*{field_expr})")
2868                } else {
2869                    format!("string({field_expr})")
2870                };
2871                let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
2872                let _ = writeln!(
2873                    out_ref,
2874                    "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
2875                );
2876                let _ = writeln!(out_ref, "\t}}");
2877            }
2878        }
2879        "count_min" => {
2880            if let Some(val) = &assertion.value {
2881                if let Some(n) = val.as_u64() {
2882                    if is_optional {
2883                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2884                        // Slices are value types in Go — use len(slice) not len(*slice).
2885                        let len_expr = if field_is_slice {
2886                            format!("len({field_expr})")
2887                        } else {
2888                            format!("len(*{field_expr})")
2889                        };
2890                        let _ = writeln!(
2891                            out_ref,
2892                            "\t\tassert.GreaterOrEqual(t, {len_expr}, {n}, \"expected at least {n} elements\")"
2893                        );
2894                        let _ = writeln!(out_ref, "\t}}");
2895                    } else {
2896                        let _ = writeln!(
2897                            out_ref,
2898                            "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
2899                        );
2900                    }
2901                }
2902            }
2903        }
2904        "count_equals" => {
2905            if let Some(val) = &assertion.value {
2906                if let Some(n) = val.as_u64() {
2907                    if is_optional {
2908                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2909                        // Slices are value types in Go — use len(slice) not len(*slice).
2910                        let len_expr = if field_is_slice {
2911                            format!("len({field_expr})")
2912                        } else {
2913                            format!("len(*{field_expr})")
2914                        };
2915                        let _ = writeln!(
2916                            out_ref,
2917                            "\t\tassert.Equal(t, {len_expr}, {n}, \"expected exactly {n} elements\")"
2918                        );
2919                        let _ = writeln!(out_ref, "\t}}");
2920                    } else {
2921                        let _ = writeln!(
2922                            out_ref,
2923                            "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
2924                        );
2925                    }
2926                }
2927            }
2928        }
2929        "is_true" => {
2930            if is_optional {
2931                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2932                let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
2933                let _ = writeln!(out_ref, "\t}}");
2934            } else {
2935                let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
2936            }
2937        }
2938        "is_false" => {
2939            if is_optional {
2940                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2941                let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
2942                let _ = writeln!(out_ref, "\t}}");
2943            } else {
2944                let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
2945            }
2946        }
2947        "method_result" => {
2948            if let Some(method_name) = &assertion.method {
2949                let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
2950                let check = assertion.check.as_deref().unwrap_or("is_true");
2951                // For pointer-returning functions, dereference with `*`. Value-returning
2952                // functions (e.g., NodeInfo field access) are used directly.
2953                let deref_expr = if info.is_pointer {
2954                    format!("*{}", info.call_expr)
2955                } else {
2956                    info.call_expr.clone()
2957                };
2958                match check {
2959                    "equals" => {
2960                        if let Some(val) = &assertion.value {
2961                            if val.is_boolean() {
2962                                if val.as_bool() == Some(true) {
2963                                    let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2964                                } else {
2965                                    let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2966                                }
2967                            } else {
2968                                // Apply type cast to numeric literals when the method returns
2969                                // a typed uint (e.g., *uint) to avoid reflect.DeepEqual
2970                                // mismatches between int and uint in testify's assert.Equal.
2971                                let go_val = if let Some(cast) = info.value_cast {
2972                                    if val.is_number() {
2973                                        format!("{cast}({})", json_to_go(val))
2974                                    } else {
2975                                        json_to_go(val)
2976                                    }
2977                                } else {
2978                                    json_to_go(val)
2979                                };
2980                                let _ = writeln!(
2981                                    out_ref,
2982                                    "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
2983                                );
2984                            }
2985                        }
2986                    }
2987                    "is_true" => {
2988                        let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2989                    }
2990                    "is_false" => {
2991                        let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2992                    }
2993                    "greater_than_or_equal" => {
2994                        if let Some(val) = &assertion.value {
2995                            let n = val.as_u64().unwrap_or(0);
2996                            // Use the value_cast type if available (e.g., uint for named_children_count).
2997                            let cast = info.value_cast.unwrap_or("uint");
2998                            let _ = writeln!(
2999                                out_ref,
3000                                "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
3001                            );
3002                        }
3003                    }
3004                    "count_min" => {
3005                        if let Some(val) = &assertion.value {
3006                            let n = val.as_u64().unwrap_or(0);
3007                            let _ = writeln!(
3008                                out_ref,
3009                                "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
3010                            );
3011                        }
3012                    }
3013                    "contains" => {
3014                        if let Some(val) = &assertion.value {
3015                            let go_val = json_to_go(val);
3016                            let _ = writeln!(
3017                                out_ref,
3018                                "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
3019                            );
3020                        }
3021                    }
3022                    "is_error" => {
3023                        let _ = writeln!(out_ref, "\t{{");
3024                        let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
3025                        let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
3026                        let _ = writeln!(out_ref, "\t}}");
3027                    }
3028                    other_check => {
3029                        panic!("Go e2e generator: unsupported method_result check type: {other_check}");
3030                    }
3031                }
3032            } else {
3033                panic!("Go e2e generator: method_result assertion missing 'method' field");
3034            }
3035        }
3036        "min_length" => {
3037            if let Some(val) = &assertion.value {
3038                if let Some(n) = val.as_u64() {
3039                    if is_optional {
3040                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
3041                        let _ = writeln!(
3042                            out_ref,
3043                            "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
3044                        );
3045                        let _ = writeln!(out_ref, "\t}}");
3046                    } else if field_expr.starts_with("len(") {
3047                        let _ = writeln!(
3048                            out_ref,
3049                            "\tassert.GreaterOrEqual(t, {field_expr}, {n}, \"expected length >= {n}\")"
3050                        );
3051                    } else {
3052                        let _ = writeln!(
3053                            out_ref,
3054                            "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
3055                        );
3056                    }
3057                }
3058            }
3059        }
3060        "max_length" => {
3061            if let Some(val) = &assertion.value {
3062                if let Some(n) = val.as_u64() {
3063                    if is_optional {
3064                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
3065                        let _ = writeln!(
3066                            out_ref,
3067                            "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
3068                        );
3069                        let _ = writeln!(out_ref, "\t}}");
3070                    } else if field_expr.starts_with("len(") {
3071                        let _ = writeln!(
3072                            out_ref,
3073                            "\tassert.LessOrEqual(t, {field_expr}, {n}, \"expected length <= {n}\")"
3074                        );
3075                    } else {
3076                        let _ = writeln!(
3077                            out_ref,
3078                            "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
3079                        );
3080                    }
3081                }
3082            }
3083        }
3084        "ends_with" => {
3085            if let Some(expected) = &assertion.value {
3086                let go_val = json_to_go(expected);
3087                let field_for_suffix = if is_optional
3088                    && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
3089                {
3090                    format!("string(*{field_expr})")
3091                } else {
3092                    format!("string({field_expr})")
3093                };
3094                let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
3095                let _ = writeln!(
3096                    out_ref,
3097                    "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
3098                );
3099                let _ = writeln!(out_ref, "\t}}");
3100            }
3101        }
3102        "matches_regex" => {
3103            if let Some(expected) = &assertion.value {
3104                let go_val = json_to_go(expected);
3105                let field_for_regex = if is_optional
3106                    && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
3107                {
3108                    format!("*{field_expr}")
3109                } else {
3110                    field_expr.clone()
3111                };
3112                let _ = writeln!(
3113                    out_ref,
3114                    "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
3115                );
3116            }
3117        }
3118        "not_error" => {
3119            // Already handled by the `if err != nil` check above.
3120        }
3121        "error" => {
3122            // Handled at the test function level.
3123        }
3124        other => {
3125            panic!("Go e2e generator: unsupported assertion type: {other}");
3126        }
3127    }
3128
3129    // If the assertion accesses an array element via [0], wrap the generated code in a
3130    // bounds check to prevent an index-out-of-range panic when the array is empty.
3131    if let Some(ref arr) = array_guard {
3132        if !assertion_buf.is_empty() {
3133            let _ = writeln!(out, "\tif len({arr}) > 0 {{");
3134            // Re-indent each line by one additional tab level.
3135            for line in assertion_buf.lines() {
3136                let _ = writeln!(out, "\t{line}");
3137            }
3138            let _ = writeln!(out, "\t}}");
3139        }
3140    } else {
3141        out.push_str(&assertion_buf);
3142    }
3143}
3144
3145/// Metadata about the return type of a Go method call for `method_result` assertions.
3146struct GoMethodCallInfo {
3147    /// The call expression string.
3148    call_expr: String,
3149    /// Whether the return type is a pointer (needs `*` dereference for value comparison).
3150    is_pointer: bool,
3151    /// Optional Go type cast to apply to numeric literal values in `equals` assertions
3152    /// (e.g., `"uint"` so that `0` becomes `uint(0)` to match `*uint` deref type).
3153    value_cast: Option<&'static str>,
3154}
3155
3156/// Build a Go call expression for a `method_result` assertion on a tree-sitter Tree.
3157///
3158/// Maps method names to the appropriate Go function calls, matching the Go binding API
3159/// in `packages/go/binding.go`. Returns a [`GoMethodCallInfo`] describing the call and
3160/// its return type characteristics.
3161///
3162/// Return types by method:
3163/// - `has_error_nodes`, `contains_node_type` → `*bool` (pointer)
3164/// - `error_count` → `*uint` (pointer, value_cast = "uint")
3165/// - `tree_to_sexp` → `*string` (pointer)
3166/// - `root_node_type` → `string` via `RootNodeInfo(tree).Kind` (value)
3167/// - `named_children_count` → `uint` via `RootNodeInfo(tree).NamedChildCount` (value, value_cast = "uint")
3168/// - `find_nodes_by_type` → `*[]NodeInfo` (pointer to slice)
3169/// - `run_query` → `(*[]QueryMatch, error)` (pointer + error; use `is_error` check type)
3170fn build_go_method_call(
3171    result_var: &str,
3172    method_name: &str,
3173    args: Option<&serde_json::Value>,
3174    import_alias: &str,
3175) -> GoMethodCallInfo {
3176    match method_name {
3177        "root_node_type" => GoMethodCallInfo {
3178            call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
3179            is_pointer: false,
3180            value_cast: None,
3181        },
3182        "named_children_count" => GoMethodCallInfo {
3183            call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
3184            is_pointer: false,
3185            value_cast: Some("uint"),
3186        },
3187        "has_error_nodes" => GoMethodCallInfo {
3188            call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
3189            is_pointer: true,
3190            value_cast: None,
3191        },
3192        "error_count" | "tree_error_count" => GoMethodCallInfo {
3193            call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
3194            is_pointer: true,
3195            value_cast: Some("uint"),
3196        },
3197        "tree_to_sexp" => GoMethodCallInfo {
3198            call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
3199            is_pointer: true,
3200            value_cast: None,
3201        },
3202        "contains_node_type" => {
3203            let node_type = args
3204                .and_then(|a| a.get("node_type"))
3205                .and_then(|v| v.as_str())
3206                .unwrap_or("");
3207            GoMethodCallInfo {
3208                call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
3209                is_pointer: true,
3210                value_cast: None,
3211            }
3212        }
3213        "find_nodes_by_type" => {
3214            let node_type = args
3215                .and_then(|a| a.get("node_type"))
3216                .and_then(|v| v.as_str())
3217                .unwrap_or("");
3218            GoMethodCallInfo {
3219                call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
3220                is_pointer: true,
3221                value_cast: None,
3222            }
3223        }
3224        "run_query" => {
3225            let query_source = args
3226                .and_then(|a| a.get("query_source"))
3227                .and_then(|v| v.as_str())
3228                .unwrap_or("");
3229            let language = args
3230                .and_then(|a| a.get("language"))
3231                .and_then(|v| v.as_str())
3232                .unwrap_or("");
3233            let query_lit = go_string_literal(query_source);
3234            let lang_lit = go_string_literal(language);
3235            // RunQuery returns (*[]QueryMatch, error) — use is_error check type.
3236            GoMethodCallInfo {
3237                call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
3238                is_pointer: false,
3239                value_cast: None,
3240            }
3241        }
3242        other => {
3243            let method_pascal = other.to_upper_camel_case();
3244            GoMethodCallInfo {
3245                call_expr: format!("{result_var}.{method_pascal}()"),
3246                is_pointer: false,
3247                value_cast: None,
3248            }
3249        }
3250    }
3251}
3252
3253/// Convert a `serde_json::Value` to a Go literal string.
3254/// Recursively convert a JSON value for Go struct unmarshalling.
3255///
3256/// The Go binding's `ConversionOptions` struct uses:
3257/// - `snake_case` JSON field tags (e.g. `"code_block_style"` not `"codeBlockStyle"`)
3258/// - lowercase/snake_case string values for enums (e.g. `"indented"`, `"atx_closed"`)
3259///
3260/// Fixture JSON uses camelCase keys and PascalCase enum values (Python/TS conventions).
3261/// This function remaps both so the generated Go tests can unmarshal correctly.
3262fn convert_json_for_go(value: serde_json::Value) -> serde_json::Value {
3263    match value {
3264        serde_json::Value::Object(map) => {
3265            let new_map: serde_json::Map<String, serde_json::Value> = map
3266                .into_iter()
3267                .map(|(k, v)| (camel_to_snake_case(&k), convert_json_for_go(v)))
3268                .collect();
3269            serde_json::Value::Object(new_map)
3270        }
3271        serde_json::Value::Array(arr) => {
3272            // Check if this is a byte array (array of integers 0-255).
3273            // If so, encode as base64 string for Go json.Unmarshal compatibility.
3274            if is_byte_array(&arr) {
3275                let bytes: Vec<u8> = arr
3276                    .iter()
3277                    .filter_map(|v| v.as_u64().and_then(|n| if n <= 255 { Some(n as u8) } else { None }))
3278                    .collect();
3279                // Encode bytes as base64 for Go json.Unmarshal (Go expects []byte as base64 strings)
3280                let encoded = base64_encode(&bytes);
3281                serde_json::Value::String(encoded)
3282            } else {
3283                serde_json::Value::Array(arr.into_iter().map(convert_json_for_go).collect())
3284            }
3285        }
3286        serde_json::Value::String(s) => {
3287            // Convert PascalCase enum values to snake_case.
3288            // Only convert values that look like PascalCase (start with uppercase, no spaces).
3289            serde_json::Value::String(pascal_to_snake_case(&s))
3290        }
3291        other => other,
3292    }
3293}
3294
3295/// Check if an array looks like a byte array (all elements are integers 0-255).
3296fn is_byte_array(arr: &[serde_json::Value]) -> bool {
3297    if arr.is_empty() {
3298        return false;
3299    }
3300    arr.iter().all(|v| {
3301        if let serde_json::Value::Number(n) = v {
3302            n.is_u64() && n.as_u64().is_some_and(|u| u <= 255)
3303        } else {
3304            false
3305        }
3306    })
3307}
3308
3309/// Encode bytes as base64 string (standard alphabet without padding in this output,
3310/// though Go's json.Unmarshal handles both).
3311fn base64_encode(bytes: &[u8]) -> String {
3312    const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3313    let mut result = String::new();
3314    let mut i = 0;
3315
3316    while i + 2 < bytes.len() {
3317        let b1 = bytes[i];
3318        let b2 = bytes[i + 1];
3319        let b3 = bytes[i + 2];
3320
3321        result.push(TABLE[(b1 >> 2) as usize] as char);
3322        result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3323        result.push(TABLE[(((b2 & 0x0f) << 2) | (b3 >> 6)) as usize] as char);
3324        result.push(TABLE[(b3 & 0x3f) as usize] as char);
3325
3326        i += 3;
3327    }
3328
3329    // Handle remaining bytes
3330    if i < bytes.len() {
3331        let b1 = bytes[i];
3332        result.push(TABLE[(b1 >> 2) as usize] as char);
3333
3334        if i + 1 < bytes.len() {
3335            let b2 = bytes[i + 1];
3336            result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3337            result.push(TABLE[((b2 & 0x0f) << 2) as usize] as char);
3338            result.push('=');
3339        } else {
3340            result.push(TABLE[((b1 & 0x03) << 4) as usize] as char);
3341            result.push_str("==");
3342        }
3343    }
3344
3345    result
3346}
3347
3348/// Convert a camelCase or PascalCase string to snake_case.
3349fn camel_to_snake_case(s: &str) -> String {
3350    let mut result = String::new();
3351    let mut prev_upper = false;
3352    for (i, c) in s.char_indices() {
3353        if c.is_uppercase() {
3354            if i > 0 && !prev_upper {
3355                result.push('_');
3356            }
3357            result.push(c.to_lowercase().next().unwrap_or(c));
3358            prev_upper = true;
3359        } else {
3360            if prev_upper && i > 1 {
3361                // Handles sequences like "URLPath" → "url_path": insert _ before last uppercase
3362                // when transitioning from a run of uppercase back to lowercase.
3363                // This is tricky — use simple approach: detect Aa pattern.
3364            }
3365            result.push(c);
3366            prev_upper = false;
3367        }
3368    }
3369    result
3370}
3371
3372/// Convert a PascalCase string to snake_case (for enum values).
3373///
3374/// Only converts if the string looks like PascalCase (starts uppercase, no spaces/underscores).
3375/// Values that are already lowercase/snake_case are returned unchanged.
3376fn pascal_to_snake_case(s: &str) -> String {
3377    // Skip conversion for strings that already contain underscores, spaces, or start lowercase.
3378    let first_char = s.chars().next();
3379    if first_char.is_none() || !first_char.unwrap().is_uppercase() || s.contains('_') || s.contains(' ') {
3380        return s.to_string();
3381    }
3382    camel_to_snake_case(s)
3383}
3384
3385/// Map an `ArgMapping.element_type` to a Go slice type. Used for `json_object` args
3386/// whose fixture value is a JSON array. The element type is wrapped in `[]…` so an
3387/// element of `String` becomes `[]string` and `Vec<String>` becomes `[][]string`.
3388fn element_type_to_go_slice(element_type: Option<&str>, import_alias: &str) -> String {
3389    let elem = element_type.unwrap_or("String").trim();
3390    let go_elem = rust_type_to_go(elem, import_alias);
3391    format!("[]{go_elem}")
3392}
3393
3394/// Map a small subset of Rust scalar / `Vec<T>` types to their Go equivalents.
3395/// For unknown types, qualify with the import alias (e.g., "kreuzberg.BatchBytesItem").
3396fn rust_type_to_go(rust: &str, import_alias: &str) -> String {
3397    let trimmed = rust.trim();
3398    if let Some(inner) = trimmed.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
3399        return format!("[]{}", rust_type_to_go(inner, import_alias));
3400    }
3401    match trimmed {
3402        "String" | "&str" | "str" => "string".to_string(),
3403        "bool" => "bool".to_string(),
3404        "f32" => "float32".to_string(),
3405        "f64" => "float64".to_string(),
3406        "i8" => "int8".to_string(),
3407        "i16" => "int16".to_string(),
3408        "i32" => "int32".to_string(),
3409        "i64" | "isize" => "int64".to_string(),
3410        "u8" => "uint8".to_string(),
3411        "u16" => "uint16".to_string(),
3412        "u32" => "uint32".to_string(),
3413        "u64" | "usize" => "uint64".to_string(),
3414        _ => format!("{import_alias}.{trimmed}"),
3415    }
3416}
3417
3418fn json_to_go(value: &serde_json::Value) -> String {
3419    match value {
3420        serde_json::Value::String(s) => go_string_literal(s),
3421        serde_json::Value::Bool(b) => b.to_string(),
3422        serde_json::Value::Number(n) => n.to_string(),
3423        serde_json::Value::Null => "nil".to_string(),
3424        // For complex types, serialize to JSON string and pass as literal.
3425        other => go_string_literal(&other.to_string()),
3426    }
3427}
3428
3429// ---------------------------------------------------------------------------
3430// Visitor generation
3431// ---------------------------------------------------------------------------
3432
3433/// Derive a unique, exported Go struct name for a visitor from a fixture ID.
3434///
3435/// E.g. `visitor_continue_default` → `visitorContinueDefault` (unexported, avoids
3436/// polluting the exported API of the test package while still being package-level).
3437fn visitor_struct_name(fixture_id: &str) -> String {
3438    use heck::ToUpperCamelCase;
3439    // Use UpperCamelCase so Go treats it as exported — required for method sets.
3440    format!("testVisitor{}", fixture_id.to_upper_camel_case())
3441}
3442
3443/// Emit a package-level Go struct declaration and all its visitor methods.
3444///
3445/// The struct embeds `BaseVisitor` to satisfy all interface methods not
3446/// explicitly overridden by the fixture callbacks.
3447fn emit_go_visitor_struct(
3448    out: &mut String,
3449    struct_name: &str,
3450    visitor_spec: &crate::fixture::VisitorSpec,
3451    import_alias: &str,
3452) {
3453    let _ = writeln!(out, "type {struct_name} struct{{");
3454    let _ = writeln!(out, "\t{import_alias}.BaseVisitor");
3455    let _ = writeln!(out, "}}");
3456    for (method_name, action) in &visitor_spec.callbacks {
3457        emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
3458    }
3459}
3460
3461/// Emit a Go visitor method for a callback action on the named struct.
3462fn emit_go_visitor_method(
3463    out: &mut String,
3464    struct_name: &str,
3465    method_name: &str,
3466    action: &CallbackAction,
3467    import_alias: &str,
3468) {
3469    let camel_method = method_to_camel(method_name);
3470    // Parameter signatures must exactly match the htmltomarkdown.Visitor interface.
3471    // Optional fields use pointer types (*string, *uint32, etc.) to indicate nil-ability.
3472    let params = match method_name {
3473        "visit_link" => format!("_ {import_alias}.NodeContext, href string, text string, title *string"),
3474        "visit_image" => format!("_ {import_alias}.NodeContext, src string, alt string, title *string"),
3475        "visit_heading" => format!("_ {import_alias}.NodeContext, level uint32, text string, id *string"),
3476        "visit_code_block" => format!("_ {import_alias}.NodeContext, lang *string, code string"),
3477        "visit_code_inline"
3478        | "visit_strong"
3479        | "visit_emphasis"
3480        | "visit_strikethrough"
3481        | "visit_underline"
3482        | "visit_subscript"
3483        | "visit_superscript"
3484        | "visit_mark"
3485        | "visit_button"
3486        | "visit_summary"
3487        | "visit_figcaption"
3488        | "visit_definition_term"
3489        | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
3490        "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
3491        "visit_list_item" => {
3492            format!("_ {import_alias}.NodeContext, ordered bool, marker string, text string")
3493        }
3494        "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth uint"),
3495        "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
3496        "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName string, html string"),
3497        "visit_form" => format!("_ {import_alias}.NodeContext, action *string, method *string"),
3498        "visit_input" => {
3499            format!("_ {import_alias}.NodeContext, inputType string, name *string, value *string")
3500        }
3501        "visit_audio" | "visit_video" | "visit_iframe" => {
3502            format!("_ {import_alias}.NodeContext, src *string")
3503        }
3504        "visit_details" => format!("_ {import_alias}.NodeContext, open bool"),
3505        "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
3506            format!("_ {import_alias}.NodeContext, output string")
3507        }
3508        "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
3509        "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
3510        _ => format!("_ {import_alias}.NodeContext"),
3511    };
3512
3513    let _ = writeln!(
3514        out,
3515        "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
3516    );
3517    match action {
3518        CallbackAction::Skip => {
3519            let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip()");
3520        }
3521        CallbackAction::Continue => {
3522            let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue()");
3523        }
3524        CallbackAction::PreserveHtml => {
3525            let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHTML()");
3526        }
3527        CallbackAction::Custom { output } => {
3528            let escaped = go_string_literal(output);
3529            let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
3530        }
3531        CallbackAction::CustomTemplate { template, .. } => {
3532            // Convert {var} placeholders to %s format verbs and collect arg names.
3533            // E.g. `QUOTE: "{text}"` → fmt.Sprintf("QUOTE: \"%s\"", text)
3534            //
3535            // For pointer-typed params (e.g. `src *string`), dereference with `*`
3536            // — the test fixtures always supply a non-nil value for methods that
3537            // fire a custom template, so this is safe in practice.
3538            let ptr_params = go_visitor_ptr_params(method_name);
3539            let (fmt_str, fmt_args) = template_to_sprintf(template, &ptr_params);
3540            let escaped_fmt = go_string_literal(&fmt_str);
3541            if fmt_args.is_empty() {
3542                let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
3543            } else {
3544                let args_str = fmt_args.join(", ");
3545                let _ = writeln!(
3546                    out,
3547                    "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
3548                );
3549            }
3550        }
3551    }
3552    let _ = writeln!(out, "}}");
3553}
3554
3555/// Return the set of camelCase parameter names that are pointer types (`*string`) for a
3556/// given visitor method name.  Used to dereference pointers in template `fmt.Sprintf` calls.
3557fn go_visitor_ptr_params(method_name: &str) -> std::collections::HashSet<&'static str> {
3558    match method_name {
3559        "visit_link" => ["title"].into(),
3560        "visit_image" => ["title"].into(),
3561        "visit_heading" => ["id"].into(),
3562        "visit_code_block" => ["lang"].into(),
3563        "visit_form" => ["action", "method"].into(),
3564        "visit_input" => ["name", "value"].into(),
3565        "visit_audio" | "visit_video" | "visit_iframe" => ["src"].into(),
3566        _ => std::collections::HashSet::new(),
3567    }
3568}
3569
3570/// Convert a `{var}` template string into a `fmt.Sprintf` format string and argument list.
3571///
3572/// For example, `QUOTE: "{text}"` becomes `("QUOTE: \"%s\"", vec!["text"])`.
3573///
3574/// Placeholder names in the template use snake_case (matching fixture field names); they
3575/// are converted to Go camelCase parameter names using `go_param_name` so they match the
3576/// generated visitor method signatures (e.g. `{input_type}` → `inputType`).
3577///
3578/// `ptr_params` — camelCase names of parameters that are `*string`; these are
3579/// dereferenced with `*` when used as `fmt.Sprintf` arguments.  The fixtures that
3580/// use `custom_template` on pointer-param methods always supply a non-nil value.
3581fn template_to_sprintf(template: &str, ptr_params: &std::collections::HashSet<&str>) -> (String, Vec<String>) {
3582    let mut fmt_str = String::new();
3583    let mut args: Vec<String> = Vec::new();
3584    let mut chars = template.chars().peekable();
3585    while let Some(c) = chars.next() {
3586        if c == '{' {
3587            // Collect placeholder name until '}'.
3588            let mut name = String::new();
3589            for inner in chars.by_ref() {
3590                if inner == '}' {
3591                    break;
3592                }
3593                name.push(inner);
3594            }
3595            fmt_str.push_str("%s");
3596            // Convert snake_case placeholder to Go camelCase to match method param names.
3597            let go_name = go_param_name(&name);
3598            // Dereference pointer params so fmt.Sprintf receives a string value.
3599            let arg_expr = if ptr_params.contains(go_name.as_str()) {
3600                format!("*{go_name}")
3601            } else {
3602                go_name
3603            };
3604            args.push(arg_expr);
3605        } else {
3606            fmt_str.push(c);
3607        }
3608    }
3609    (fmt_str, args)
3610}
3611
3612/// Convert snake_case method names to Go camelCase.
3613fn method_to_camel(snake: &str) -> String {
3614    use heck::ToUpperCamelCase;
3615    snake.to_upper_camel_case()
3616}
3617
3618#[cfg(test)]
3619mod tests {
3620    use super::*;
3621    use crate::config::{CallConfig, E2eConfig};
3622    use crate::fixture::{Assertion, Fixture};
3623
3624    fn make_fixture(id: &str) -> Fixture {
3625        Fixture {
3626            id: id.to_string(),
3627            category: None,
3628            description: "test fixture".to_string(),
3629            tags: vec![],
3630            skip: None,
3631            env: None,
3632            call: None,
3633            input: serde_json::Value::Null,
3634            mock_response: Some(crate::fixture::MockResponse {
3635                status: 200,
3636                body: Some(serde_json::Value::Null),
3637                stream_chunks: None,
3638                headers: std::collections::HashMap::new(),
3639            }),
3640            source: String::new(),
3641            http: None,
3642            assertions: vec![Assertion {
3643                assertion_type: "not_error".to_string(),
3644                ..Default::default()
3645            }],
3646            visitor: None,
3647        }
3648    }
3649
3650    /// snake_case function names in `[e2e.call]` must be routed through `to_go_name`
3651    /// so the emitted Go call uses the idiomatic CamelCase (e.g. `CleanExtractedText`
3652    /// instead of `clean_extracted_text`).
3653    #[test]
3654    fn test_go_method_name_uses_go_casing() {
3655        let e2e_config = E2eConfig {
3656            call: CallConfig {
3657                function: "clean_extracted_text".to_string(),
3658                module: "github.com/example/mylib".to_string(),
3659                result_var: "result".to_string(),
3660                returns_result: true,
3661                ..CallConfig::default()
3662            },
3663            ..E2eConfig::default()
3664        };
3665
3666        let fixture = make_fixture("basic_text");
3667        let mut out = String::new();
3668        render_test_function(&mut out, &fixture, "kreuzberg", &e2e_config, &[]);
3669
3670        assert!(
3671            out.contains("kreuzberg.CleanExtractedText("),
3672            "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
3673        );
3674        assert!(
3675            !out.contains("kreuzberg.clean_extracted_text("),
3676            "must not emit raw snake_case method name, got:\n{out}"
3677        );
3678    }
3679
3680    #[test]
3681    fn test_streaming_fixture_emits_collect_snippet() {
3682        // A streaming fixture should emit `stream, err :=` and the collect loop.
3683        let streaming_fixture_json = r#"{
3684            "id": "basic_stream",
3685            "description": "basic streaming test",
3686            "call": "chat_stream",
3687            "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3688            "mock_response": {
3689                "status": 200,
3690                "stream_chunks": [{"delta": "hello"}]
3691            },
3692            "assertions": [
3693                {"type": "count_min", "field": "chunks", "value": 1}
3694            ]
3695        }"#;
3696        let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3697        assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3698
3699        let e2e_config = E2eConfig {
3700            call: CallConfig {
3701                function: "chat_stream".to_string(),
3702                module: "github.com/example/mylib".to_string(),
3703                result_var: "result".to_string(),
3704                returns_result: true,
3705                r#async: true,
3706                ..CallConfig::default()
3707            },
3708            ..E2eConfig::default()
3709        };
3710
3711        let mut out = String::new();
3712        render_test_function(&mut out, &fixture, "pkg", &e2e_config, &[]);
3713
3714        assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3715        assert!(
3716            out.contains("for chunk := range stream"),
3717            "should emit collect loop, got:\n{out}"
3718        );
3719    }
3720
3721    #[test]
3722    fn test_streaming_with_client_factory_and_json_arg() {
3723        // Mimics the real liter-llm setup: no returns_result on the call,
3724        // json_object arg (binding_returns_error=true), and client_factory from
3725        // the default Go call override.
3726        use alef_core::config::e2e::{ArgMapping, CallOverride};
3727        let streaming_fixture_json = r#"{
3728            "id": "basic_stream_client",
3729            "description": "basic streaming test with client",
3730            "call": "chat_stream",
3731            "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3732            "mock_response": {
3733                "status": 200,
3734                "stream_chunks": [{"delta": "hello"}]
3735            },
3736            "assertions": [
3737                {"type": "count_min", "field": "chunks", "value": 1}
3738            ]
3739        }"#;
3740        let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3741        assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3742
3743        let go_override = CallOverride {
3744            client_factory: Some("CreateClient".to_string()),
3745            ..Default::default()
3746        };
3747
3748        let mut call_overrides = std::collections::HashMap::new();
3749        call_overrides.insert("go".to_string(), go_override);
3750
3751        let e2e_config = E2eConfig {
3752            call: CallConfig {
3753                function: "chat_stream".to_string(),
3754                module: "github.com/example/mylib".to_string(),
3755                result_var: "result".to_string(),
3756                returns_result: false, // NOT true — like real liter-llm
3757                r#async: true,
3758                args: vec![ArgMapping {
3759                    name: "request".to_string(),
3760                    field: "input".to_string(),
3761                    arg_type: "json_object".to_string(),
3762                    optional: false,
3763                    owned: true,
3764                    element_type: None,
3765                    go_type: None,
3766                }],
3767                overrides: call_overrides,
3768                ..CallConfig::default()
3769            },
3770            ..E2eConfig::default()
3771        };
3772
3773        let mut out = String::new();
3774        render_test_function(&mut out, &fixture, "pkg", &e2e_config, &[]);
3775
3776        eprintln!("generated:\n{out}");
3777        assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3778        assert!(
3779            out.contains("for chunk := range stream"),
3780            "should emit collect loop, got:\n{out}"
3781        );
3782    }
3783
3784    /// When `segments` is an optional field (Option<Vec<T>>) and a fixture asserts on
3785    /// `segments[0].id`, the prefix guard must be `result.Segments != nil` — NOT
3786    /// `result.Segments[0] != nil`, which is a compile error for a value-typed element.
3787    #[test]
3788    fn test_indexed_element_prefix_guard_uses_array_not_element() {
3789        let mut optional_fields = std::collections::HashSet::new();
3790        optional_fields.insert("segments".to_string());
3791        let mut array_fields = std::collections::HashSet::new();
3792        array_fields.insert("segments".to_string());
3793
3794        let e2e_config = E2eConfig {
3795            call: CallConfig {
3796                function: "transcribe".to_string(),
3797                module: "github.com/example/mylib".to_string(),
3798                result_var: "result".to_string(),
3799                returns_result: true,
3800                ..CallConfig::default()
3801            },
3802            fields_optional: optional_fields,
3803            fields_array: array_fields,
3804            ..E2eConfig::default()
3805        };
3806
3807        let fixture = Fixture {
3808            id: "edge_transcribe_with_timestamps".to_string(),
3809            category: None,
3810            description: "Transcription with timestamp segments".to_string(),
3811            tags: vec![],
3812            skip: None,
3813            env: None,
3814            call: None,
3815            input: serde_json::Value::Null,
3816            mock_response: Some(crate::fixture::MockResponse {
3817                status: 200,
3818                body: Some(serde_json::Value::Null),
3819                stream_chunks: None,
3820                headers: std::collections::HashMap::new(),
3821            }),
3822            source: String::new(),
3823            http: None,
3824            assertions: vec![
3825                Assertion {
3826                    assertion_type: "not_error".to_string(),
3827                    ..Default::default()
3828                },
3829                Assertion {
3830                    assertion_type: "equals".to_string(),
3831                    field: Some("segments[0].id".to_string()),
3832                    value: Some(serde_json::Value::Number(serde_json::Number::from(0u64))),
3833                    ..Default::default()
3834                },
3835            ],
3836            visitor: None,
3837        };
3838
3839        let mut out = String::new();
3840        render_test_function(&mut out, &fixture, "pkg", &e2e_config, &[]);
3841
3842        eprintln!("generated:\n{out}");
3843
3844        // Must guard on the slice itself — not on the element.
3845        // Accepts either `Segments != nil` or `len(Segments) > 0`; both are
3846        // valid Go guards for the slice and avoid the invalid element nil
3847        // check.
3848        assert!(
3849            out.contains("result.Segments != nil") || out.contains("len(result.Segments) > 0"),
3850            "guard must be on Segments (the slice), not an element; got:\n{out}"
3851        );
3852        // Must NOT emit the invalid element nil check.
3853        assert!(
3854            !out.contains("result.Segments[0] != nil"),
3855            "must not emit Segments[0] != nil for a value-type element; got:\n{out}"
3856        );
3857    }
3858
3859    /// Regression test: a `result_is_simple` call with a `contains` assertion whose
3860    /// `field` ("result") is not a struct field must still bind the call to the result
3861    /// variable AND emit the `fmt`/`strings` imports.  The assertion renderer ignores
3862    /// the field for `result_is_simple` calls and emits `strings.Contains(fmt.Sprint(result), …)`,
3863    /// so binding to `_` (or omitting the imports) produces uncompilable Go.
3864    #[test]
3865    fn test_result_is_simple_contains_binds_result_and_emits_imports() {
3866        use alef_core::config::e2e::ArgMapping;
3867
3868        let e2e_config = E2eConfig {
3869            call: CallConfig {
3870                function: "detect_mime_type_from_bytes".to_string(),
3871                module: "github.com/example/mylib".to_string(),
3872                result_var: "result".to_string(),
3873                returns_result: true,
3874                result_is_simple: true,
3875                args: vec![ArgMapping {
3876                    name: "content".to_string(),
3877                    field: "input.data".to_string(),
3878                    arg_type: "bytes".to_string(),
3879                    optional: false,
3880                    owned: false,
3881                    element_type: None,
3882                    go_type: None,
3883                }],
3884                ..CallConfig::default()
3885            },
3886            ..E2eConfig::default()
3887        };
3888
3889        let fixture = Fixture {
3890            id: "mime_detect_bytes".to_string(),
3891            category: None,
3892            description: "Detect MIME type from file bytes".to_string(),
3893            tags: vec![],
3894            skip: None,
3895            env: None,
3896            call: None,
3897            input: serde_json::json!({"data": "pdf/fake_memo.pdf"}),
3898            mock_response: None,
3899            source: String::new(),
3900            http: None,
3901            assertions: vec![Assertion {
3902                assertion_type: "contains".to_string(),
3903                field: Some("result".to_string()),
3904                value: Some(serde_json::Value::String("pdf".to_string())),
3905                ..Default::default()
3906            }],
3907            visitor: None,
3908        };
3909
3910        let out = render_test_file(
3911            "mime_utilities",
3912            &[&fixture],
3913            "github.com/example/mylib",
3914            "kreuzberg",
3915            &e2e_config,
3916            &[],
3917        );
3918
3919        assert!(
3920            out.contains("result, err := kreuzberg.DetectMimeTypeFromBytes("),
3921            "expected the call to bind to `result`, not `_`; got:\n{out}"
3922        );
3923        assert!(
3924            out.contains("strings.Contains(") && out.contains("fmt.Sprint("),
3925            "expected `strings.Contains(fmt.Sprint(...))` rendering; got:\n{out}"
3926        );
3927        assert!(
3928            out.contains("\t\"fmt\""),
3929            "expected the `fmt` import to be emitted; got:\n{out}"
3930        );
3931        assert!(
3932            out.contains("\t\"strings\""),
3933            "expected the `strings` import to be emitted; got:\n{out}"
3934        );
3935    }
3936}