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