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