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