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