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, HttpFixture};
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;
17
18/// Go e2e code generator.
19pub struct GoCodegen;
20
21impl E2eCodegen for GoCodegen {
22    fn generate(
23        &self,
24        groups: &[FixtureGroup],
25        e2e_config: &E2eConfig,
26        alef_config: &AlefConfig,
27    ) -> Result<Vec<GeneratedFile>> {
28        let lang = self.language_name();
29        let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
30
31        let mut files = Vec::new();
32
33        // Resolve call config with overrides (for module path and import alias).
34        let call = &e2e_config.call;
35        let overrides = call.overrides.get(lang);
36        let module_path = overrides
37            .and_then(|o| o.module.as_ref())
38            .cloned()
39            .unwrap_or_else(|| call.module.clone());
40        let import_alias = overrides
41            .and_then(|o| o.alias.as_ref())
42            .cloned()
43            .unwrap_or_else(|| "pkg".to_string());
44
45        // Resolve package config.
46        let go_pkg = e2e_config.resolve_package("go");
47        let go_module_path = go_pkg
48            .as_ref()
49            .and_then(|p| p.module.as_ref())
50            .cloned()
51            .unwrap_or_else(|| module_path.clone());
52        let replace_path = go_pkg.as_ref().and_then(|p| p.path.as_ref()).cloned();
53        let go_version = go_pkg
54            .as_ref()
55            .and_then(|p| p.version.as_ref())
56            .cloned()
57            .unwrap_or_else(|| {
58                alef_config
59                    .resolved_version()
60                    .map(|v| format!("v{v}"))
61                    .unwrap_or_else(|| "v0.0.0".to_string())
62            });
63        let field_resolver = FieldResolver::new(
64            &e2e_config.fields,
65            &e2e_config.fields_optional,
66            &e2e_config.result_fields,
67            &e2e_config.fields_array,
68        );
69
70        // Generate go.mod. In registry mode, omit the `replace` directive so the
71        // module is fetched from the Go module proxy.
72        let effective_replace = match e2e_config.dep_mode {
73            crate::config::DependencyMode::Registry => None,
74            crate::config::DependencyMode::Local => replace_path.as_deref().map(String::from),
75        };
76        files.push(GeneratedFile {
77            path: output_base.join("go.mod"),
78            content: render_go_mod(&go_module_path, effective_replace.as_deref(), &go_version),
79            generated_header: false,
80        });
81
82        // Generate test files per category.
83        for group in groups {
84            let active: Vec<&Fixture> = group
85                .fixtures
86                .iter()
87                .filter(|f| f.skip.as_ref().is_none_or(|s| !s.should_skip(lang)))
88                .collect();
89
90            if active.is_empty() {
91                continue;
92            }
93
94            let filename = format!("{}_test.go", sanitize_filename(&group.category));
95            let content = render_test_file(
96                &group.category,
97                &active,
98                &module_path,
99                &import_alias,
100                &field_resolver,
101                e2e_config,
102            );
103            files.push(GeneratedFile {
104                path: output_base.join(filename),
105                content,
106                generated_header: true,
107            });
108        }
109
110        Ok(files)
111    }
112
113    fn language_name(&self) -> &'static str {
114        "go"
115    }
116}
117
118fn render_go_mod(go_module_path: &str, replace_path: Option<&str>, version: &str) -> String {
119    let mut out = String::new();
120    let _ = writeln!(out, "module e2e_go");
121    let _ = writeln!(out);
122    let _ = writeln!(out, "go 1.26");
123    let _ = writeln!(out);
124    let _ = writeln!(out, "require (");
125    let _ = writeln!(out, "\t{go_module_path} {version}");
126    let _ = writeln!(out, "\tgithub.com/stretchr/testify v1.11.1");
127    let _ = writeln!(out, ")");
128
129    if let Some(path) = replace_path {
130        let _ = writeln!(out);
131        let _ = writeln!(out, "replace {go_module_path} => {path}");
132    }
133
134    out
135}
136
137fn render_test_file(
138    category: &str,
139    fixtures: &[&Fixture],
140    go_module_path: &str,
141    import_alias: &str,
142    field_resolver: &FieldResolver,
143    e2e_config: &crate::config::E2eConfig,
144) -> String {
145    let mut out = String::new();
146
147    // Go convention: generated file marker must appear before the package declaration.
148    out.push_str(&hash::header(CommentStyle::DoubleSlash));
149    let _ = writeln!(out);
150
151    // Determine if any fixture actually uses the pkg import.
152    // Fixtures without mock_response are emitted as t.Skip() stubs and don't reference the
153    // package — omit the import when no fixture needs it to avoid the Go "imported and not
154    // used" compile error.
155    let needs_pkg = fixtures.iter().any(|f| f.mock_response.is_some());
156
157    // Determine if we need the "os" import (mock_url args).
158    // Check all resolved per-fixture call args.
159    let needs_os = fixtures.iter().any(|f| {
160        let call_args = &e2e_config.resolve_call(f.call.as_deref()).args;
161        call_args.iter().any(|a| a.arg_type == "mock_url")
162    });
163
164    // Determine if we need "encoding/json" (handle args with non-null config,
165    // or json_object args that will be unmarshalled into a typed struct).
166    let needs_json = fixtures.iter().any(|f| {
167        let call = e2e_config.resolve_call(f.call.as_deref());
168        let call_args = &call.args;
169        // handle args with non-null config value
170        let has_handle = call_args.iter().any(|a| a.arg_type == "handle") && {
171            call_args.iter().filter(|a| a.arg_type == "handle").any(|a| {
172                let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
173                let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
174                !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
175            })
176        };
177        // json_object args with options_type or array values (will use JSON unmarshal)
178        let go_override = call.overrides.get("go");
179        let opts_type = go_override.and_then(|o| o.options_type.as_deref()).or_else(|| {
180            e2e_config
181                .call
182                .overrides
183                .get("go")
184                .and_then(|o| o.options_type.as_deref())
185        });
186        let has_json_obj = call_args.iter().any(|a| {
187            if a.arg_type != "json_object" {
188                return false;
189            }
190            let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
191            let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
192            if v.is_array() {
193                return true;
194            } // array → []string unmarshal
195            opts_type.is_some() && v.is_object() && !v.as_object().is_some_and(|o| o.is_empty())
196        });
197        has_handle || has_json_obj
198    });
199
200    // Determine if we need "encoding/base64" (bytes-type args decoded at runtime).
201    let needs_base64 = fixtures.iter().any(|f| {
202        let call_args = &e2e_config.resolve_call(f.call.as_deref()).args;
203        call_args.iter().any(|a| {
204            if a.arg_type != "bytes" {
205                return false;
206            }
207            let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
208            matches!(f.input.get(field), Some(serde_json::Value::String(_)))
209        })
210    });
211
212    // Determine if we need the "fmt" import (CustomTemplate visitor actions with placeholders).
213    let needs_fmt = fixtures.iter().any(|f| {
214        f.visitor.as_ref().is_some_and(|v| {
215            v.callbacks.values().any(|action| {
216                if let CallbackAction::CustomTemplate { template } = action {
217                    template.contains('{')
218                } else {
219                    false
220                }
221            })
222        })
223    });
224
225    // Determine if we need the "strings" import.
226    // Only count assertions whose fields are actually valid for the result type.
227    let needs_strings = fixtures.iter().any(|f| {
228        f.assertions.iter().any(|a| {
229            let type_needs_strings = if a.assertion_type == "equals" {
230                // equals with string values needs strings.TrimSpace
231                a.value.as_ref().is_some_and(|v| v.is_string())
232            } else {
233                matches!(
234                    a.assertion_type.as_str(),
235                    "contains" | "contains_all" | "contains_any" | "not_contains" | "starts_with" | "ends_with"
236                )
237            };
238            let field_valid = a
239                .field
240                .as_ref()
241                .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
242                .unwrap_or(true);
243            type_needs_strings && field_valid
244        })
245    });
246
247    // Determine if we need the testify assert import (used for count_min, count_max,
248    // is_true, is_false, and method_result assertions).
249    let needs_assert = fixtures.iter().any(|f| {
250        f.assertions.iter().any(|a| {
251            let field_valid = a
252                .field
253                .as_ref()
254                .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
255                .unwrap_or(true);
256            let type_needs_assert = matches!(
257                a.assertion_type.as_str(),
258                "count_min"
259                    | "count_max"
260                    | "is_true"
261                    | "is_false"
262                    | "method_result"
263                    | "min_length"
264                    | "max_length"
265                    | "matches_regex"
266            );
267            type_needs_assert && field_valid
268        })
269    });
270
271    // Determine if we need "net/http" and "io" (HTTP server tests via HTTP client).
272    let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
273    let needs_http = has_http_fixtures;
274
275    // Determine if we need "reflect" (for HTTP response body JSON comparison).
276    let needs_reflect = fixtures.iter().any(|f| {
277        if let Some(http) = &f.http {
278            if let Some(body) = &http.expected_response.body {
279                matches!(body, serde_json::Value::Object(_) | serde_json::Value::Array(_))
280            } else {
281                false
282            }
283        } else {
284            false
285        }
286    });
287
288    let _ = writeln!(out, "// E2e tests for category: {category}");
289    let _ = writeln!(out, "package e2e_test");
290    let _ = writeln!(out);
291    let _ = writeln!(out, "import (");
292    if needs_base64 {
293        let _ = writeln!(out, "\t\"encoding/base64\"");
294    }
295    if needs_json || needs_reflect {
296        let _ = writeln!(out, "\t\"encoding/json\"");
297    }
298    if needs_fmt {
299        let _ = writeln!(out, "\t\"fmt\"");
300    }
301    if needs_http {
302        let _ = writeln!(out, "\t\"io\"");
303    }
304    if needs_http {
305        let _ = writeln!(out, "\t\"net/http\"");
306    }
307    if needs_os {
308        let _ = writeln!(out, "\t\"os\"");
309    }
310    if needs_reflect {
311        let _ = writeln!(out, "\t\"reflect\"");
312    }
313    if needs_strings || needs_http {
314        let _ = writeln!(out, "\t\"strings\"");
315    }
316    let _ = writeln!(out, "\t\"testing\"");
317    if needs_assert {
318        let _ = writeln!(out);
319        let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
320    }
321    if needs_pkg {
322        let _ = writeln!(out);
323        let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
324    }
325    let _ = writeln!(out, ")");
326    let _ = writeln!(out);
327
328    // Emit package-level visitor structs (must be outside any function in Go).
329    for fixture in fixtures.iter() {
330        if let Some(visitor_spec) = &fixture.visitor {
331            let struct_name = visitor_struct_name(&fixture.id);
332            emit_go_visitor_struct(&mut out, &struct_name, visitor_spec, import_alias);
333            let _ = writeln!(out);
334        }
335    }
336
337    for (i, fixture) in fixtures.iter().enumerate() {
338        render_test_function(&mut out, fixture, import_alias, field_resolver, e2e_config);
339        if i + 1 < fixtures.len() {
340            let _ = writeln!(out);
341        }
342    }
343
344    // Clean up trailing newlines.
345    while out.ends_with("\n\n") {
346        out.pop();
347    }
348    if !out.ends_with('\n') {
349        out.push('\n');
350    }
351    out
352}
353
354fn render_test_function(
355    out: &mut String,
356    fixture: &Fixture,
357    import_alias: &str,
358    field_resolver: &FieldResolver,
359    e2e_config: &crate::config::E2eConfig,
360) {
361    let fn_name = fixture.id.to_upper_camel_case();
362    let description = &fixture.description;
363
364    // Delegate HTTP fixtures to the HTTP-specific renderer.
365    if let Some(http) = &fixture.http {
366        render_http_test_function(out, fixture, http);
367        return;
368    }
369
370    // The Go binding wraps a C FFI layer and does not expose a HandleRequest or equivalent
371    // function that can be called from e2e tests. Emit compilable stubs for non-HTTP fixtures
372    // that don't have Rust FFI bindings.
373    if fixture.mock_response.is_none() {
374        let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
375        let _ = writeln!(out, "\t// {description}");
376        let _ = writeln!(
377            out,
378            "\tt.Skip(\"TODO: implement Go e2e tests via the spikard Go binding API\")"
379        );
380        let _ = writeln!(out, "}}");
381        return;
382    }
383
384    // Resolve call config per-fixture (supports named calls via fixture.call).
385    let call_config = e2e_config.resolve_call(fixture.call.as_deref());
386    let lang = "go";
387    let overrides = call_config.overrides.get(lang);
388    let function_name = to_go_name(
389        overrides
390            .and_then(|o| o.function.as_ref())
391            .map(String::as_str)
392            .unwrap_or(&call_config.function),
393    );
394    let result_var = &call_config.result_var;
395    let args = &call_config.args;
396
397    // Whether the function returns (value, error) or just (error) or just (value).
398    // Check Go override first, fall back to call-level returns_result.
399    let returns_result = overrides
400        .and_then(|o| o.returns_result)
401        .unwrap_or(call_config.returns_result);
402
403    // Whether the function returns only error (no value component), i.e. Result<(), E>.
404    // When returns_result=true and returns_void=true, Go emits `err :=` not `_, err :=`.
405    let returns_void = call_config.returns_void;
406
407    // result_is_simple: result is a scalar (*string, *bool, etc.) not a struct.
408    // Check Go override first, then Rust override as fallback.
409    let result_is_simple = overrides.map(|o| o.result_is_simple).unwrap_or_else(|| {
410        call_config
411            .overrides
412            .get("rust")
413            .map(|o| o.result_is_simple)
414            .unwrap_or(false)
415    });
416
417    // result_is_array: the simple result is a slice/array type (e.g., []string).
418    // Only relevant when result_is_simple is true.
419    let result_is_array = overrides.map(|o| o.result_is_array).unwrap_or(false);
420
421    // Per-call Go options_type, falling back to the default call's Go override.
422    let call_options_type = overrides.and_then(|o| o.options_type.as_deref()).or_else(|| {
423        e2e_config
424            .call
425            .overrides
426            .get("go")
427            .and_then(|o| o.options_type.as_deref())
428    });
429
430    let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
431
432    let (mut setup_lines, args_str) =
433        build_args_and_setup(&fixture.input, args, import_alias, call_options_type, &fixture.id);
434
435    // Build visitor if present — struct is at package level, just instantiate here.
436    let mut visitor_arg = String::new();
437    if fixture.visitor.is_some() {
438        let struct_name = visitor_struct_name(&fixture.id);
439        setup_lines.push(format!("visitor := &{struct_name}{{}}"));
440        visitor_arg = "visitor".to_string();
441    }
442
443    let final_args = if visitor_arg.is_empty() {
444        args_str
445    } else {
446        format!("{args_str}, {visitor_arg}")
447    };
448
449    let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
450    let _ = writeln!(out, "\t// {description}");
451
452    for line in &setup_lines {
453        let _ = writeln!(out, "\t{line}");
454    }
455
456    if expects_error {
457        if returns_result && !returns_void {
458            let _ = writeln!(out, "\t_, err := {import_alias}.{function_name}({final_args})");
459        } else {
460            let _ = writeln!(out, "\terr := {import_alias}.{function_name}({final_args})");
461        }
462        let _ = writeln!(out, "\tif err == nil {{");
463        let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
464        let _ = writeln!(out, "\t}}");
465        let _ = writeln!(out, "}}");
466        return;
467    }
468
469    // Check if any assertion actually uses the result variable.
470    // If all assertions are skipped (field not on result type), use `_` to avoid
471    // Go's "declared and not used" compile error.
472    let has_usable_assertion = fixture.assertions.iter().any(|a| {
473        if a.assertion_type == "not_error" || a.assertion_type == "error" {
474            return false;
475        }
476        // method_result assertions always use the result variable.
477        if a.assertion_type == "method_result" {
478            return true;
479        }
480        match &a.field {
481            Some(f) if !f.is_empty() => field_resolver.is_valid_for_result(f),
482            _ => true,
483        }
484    });
485
486    // For result_is_simple functions, the result variable IS the value (e.g. *string, *bool).
487    // We create a local `value` that dereferences it so assertions can use a plain type.
488    // For functions that return (value, error): emit `result, err :=`
489    // For functions that return only error: emit `err :=`
490    // For functions that return only a value (result_is_simple, no error): emit `result :=`
491    if !returns_result && result_is_simple {
492        // Function returns a single value, no error (e.g. *string, *bool).
493        let result_binding = if has_usable_assertion {
494            result_var.to_string()
495        } else {
496            "_".to_string()
497        };
498        // In Go, `_ :=` is invalid — must use `_ =` for the blank identifier.
499        let assign_op = if result_binding == "_" { "=" } else { ":=" };
500        let _ = writeln!(
501            out,
502            "\t{result_binding} {assign_op} {import_alias}.{function_name}({final_args})"
503        );
504        if has_usable_assertion && result_binding != "_" {
505            // Emit nil check and dereference for simple pointer results.
506            let _ = writeln!(out, "\tif {result_var} == nil {{");
507            let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
508            let _ = writeln!(out, "\t}}");
509            let _ = writeln!(out, "\tvalue := *{result_var}");
510        }
511    } else if !returns_result || returns_void {
512        // Function returns only error (either returns_result=false, or returns_result=true
513        // with returns_void=true meaning the Go function signature is `func(...) error`).
514        let _ = writeln!(out, "\terr := {import_alias}.{function_name}({final_args})");
515        let _ = writeln!(out, "\tif err != nil {{");
516        let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
517        let _ = writeln!(out, "\t}}");
518        // No result variable to use in assertions.
519        let _ = writeln!(out, "}}");
520        return;
521    } else {
522        // returns_result = true, returns_void = false: function returns (value, error).
523        let result_binding = if has_usable_assertion {
524            result_var.to_string()
525        } else {
526            "_".to_string()
527        };
528        let _ = writeln!(
529            out,
530            "\t{result_binding}, err := {import_alias}.{function_name}({final_args})"
531        );
532        let _ = writeln!(out, "\tif err != nil {{");
533        let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
534        let _ = writeln!(out, "\t}}");
535        if result_is_simple && has_usable_assertion && result_binding != "_" {
536            // Emit nil check and dereference for simple pointer results.
537            let _ = writeln!(out, "\tif {result_var} == nil {{");
538            let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
539            let _ = writeln!(out, "\t}}");
540            let _ = writeln!(out, "\tvalue := *{result_var}");
541        }
542    }
543
544    // For result_is_simple functions, assertions reference `value` (the dereferenced result).
545    let effective_result_var = if result_is_simple && has_usable_assertion {
546        "value".to_string()
547    } else {
548        result_var.to_string()
549    };
550
551    // Collect optional fields referenced by assertions and emit nil-safe
552    // dereference blocks so that assertions can use plain string locals.
553    // Only dereference fields whose assertion values are strings (or that are
554    // used in string-oriented assertions like equals/contains with string values).
555    let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
556    for assertion in &fixture.assertions {
557        if let Some(f) = &assertion.field {
558            if !f.is_empty() {
559                let resolved = field_resolver.resolve(f);
560                if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
561                    // Only create deref locals for string-valued fields that are NOT arrays.
562                    // Array fields (e.g., *[]string) must keep their pointer form so
563                    // render_assertion can emit strings.Join(*field, " ") rather than
564                    // treating them as plain strings.
565                    let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
566                    let is_array_field = field_resolver.is_array(resolved);
567                    if !is_string_field || is_array_field {
568                        // Non-string optional fields (e.g., *uint64) and array optional
569                        // fields (e.g., *[]string) are handled by nil guards in render_assertion.
570                        continue;
571                    }
572                    let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
573                    let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
574                    if field_resolver.has_map_access(f) {
575                        // Go map access returns a value type (string), not a pointer.
576                        // Use the value directly — empty string means not present.
577                        let _ = writeln!(out, "\t{local_var} := {field_expr}");
578                    } else {
579                        let _ = writeln!(out, "\tvar {local_var} string");
580                        let _ = writeln!(out, "\tif {field_expr} != nil {{");
581                        let _ = writeln!(out, "\t\t{local_var} = *{field_expr}");
582                        let _ = writeln!(out, "\t}}");
583                    }
584                    optional_locals.insert(f.clone(), local_var);
585                }
586            }
587        }
588    }
589
590    // Emit assertions, wrapping in nil guards when an intermediate path segment is optional.
591    for assertion in &fixture.assertions {
592        if let Some(f) = &assertion.field {
593            if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
594                // Check if any prefix of the dotted path is optional (pointer in Go).
595                // e.g., "document.nodes" — if "document" is optional, guard the whole block.
596                let parts: Vec<&str> = f.split('.').collect();
597                let mut guard_expr: Option<String> = None;
598                for i in 1..parts.len() {
599                    let prefix = parts[..i].join(".");
600                    let resolved_prefix = field_resolver.resolve(&prefix);
601                    if field_resolver.is_optional(resolved_prefix) {
602                        let accessor = field_resolver.accessor(&prefix, "go", &effective_result_var);
603                        guard_expr = Some(accessor);
604                        break;
605                    }
606                }
607                if let Some(guard) = guard_expr {
608                    // Only emit nil guard if the assertion will actually produce code
609                    // (not just a skip comment), to avoid empty branches (SA9003).
610                    if field_resolver.is_valid_for_result(f) {
611                        let _ = writeln!(out, "\tif {guard} != nil {{");
612                        // Render into a temporary buffer so we can re-indent by one
613                        // tab level to sit inside the nil-guard block.
614                        let mut nil_buf = String::new();
615                        render_assertion(
616                            &mut nil_buf,
617                            assertion,
618                            &effective_result_var,
619                            import_alias,
620                            field_resolver,
621                            &optional_locals,
622                            result_is_simple,
623                            result_is_array,
624                        );
625                        for line in nil_buf.lines() {
626                            let _ = writeln!(out, "\t{line}");
627                        }
628                        let _ = writeln!(out, "\t}}");
629                    } else {
630                        render_assertion(
631                            out,
632                            assertion,
633                            &effective_result_var,
634                            import_alias,
635                            field_resolver,
636                            &optional_locals,
637                            result_is_simple,
638                            result_is_array,
639                        );
640                    }
641                    continue;
642                }
643            }
644        }
645        render_assertion(
646            out,
647            assertion,
648            &effective_result_var,
649            import_alias,
650            field_resolver,
651            &optional_locals,
652            result_is_simple,
653            result_is_array,
654        );
655    }
656
657    let _ = writeln!(out, "}}");
658}
659
660/// Render an HTTP server test function using net/http against MOCK_SERVER_URL.
661///
662/// The mock server registers each fixture at `/fixtures/<fixture_id>` and returns the
663/// pre-canned response. Tests send the correct HTTP method and headers to that endpoint.
664fn render_http_test_function(out: &mut String, fixture: &Fixture, http: &HttpFixture) {
665    let fn_name = fixture.id.to_upper_camel_case();
666    let description = &fixture.description;
667    let request = &http.request;
668    let expected = &http.expected_response;
669    let method = request.method.to_uppercase();
670    let fixture_id = &fixture.id;
671    let expected_status = expected.status_code;
672
673    let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
674    let _ = writeln!(out, "\t// {description}");
675    let _ = writeln!(out, "\tbaseURL := os.Getenv(\"MOCK_SERVER_URL\")");
676    let _ = writeln!(out, "\tif baseURL == \"\" {{");
677    let _ = writeln!(out, "\t\tbaseURL = \"http://localhost:8080\"");
678    let _ = writeln!(out, "\t}}");
679
680    // Build request body.
681    let body_expr = if let Some(body) = &request.body {
682        let json = serde_json::to_string(body).unwrap_or_default();
683        let escaped = go_string_literal(&json);
684        format!("strings.NewReader({})", escaped)
685    } else {
686        "strings.NewReader(\"\")".to_string()
687    };
688
689    let _ = writeln!(out, "\tbody := {body_expr}");
690    let _ = writeln!(
691        out,
692        "\treq, err := http.NewRequest(\"{method}\", baseURL+\"/fixtures/{fixture_id}\", body)"
693    );
694    let _ = writeln!(out, "\tif err != nil {{");
695    let _ = writeln!(out, "\t\tt.Fatalf(\"new request failed: %v\", err)");
696    let _ = writeln!(out, "\t}}");
697
698    // Set headers.
699    let content_type = request.content_type.as_deref().unwrap_or("application/json");
700    if request.body.is_some() {
701        let _ = writeln!(out, "\treq.Header.Set(\"Content-Type\", \"{content_type}\")");
702    }
703
704    for (name, value) in &request.headers {
705        let escaped_name = go_string_literal(name);
706        let escaped_value = go_string_literal(value);
707        let _ = writeln!(out, "\treq.Header.Set({escaped_name}, {escaped_value})");
708    }
709
710    // Add cookies.
711    if !request.cookies.is_empty() {
712        for (name, value) in &request.cookies {
713            let escaped_name = go_string_literal(name);
714            let escaped_value = go_string_literal(value);
715            let _ = writeln!(
716                out,
717                "\treq.AddCookie(&http.Cookie{{Name: {escaped_name}, Value: {escaped_value}}})"
718            );
719        }
720    }
721
722    // Send request.
723    let _ = writeln!(out, "\tresp, err := http.DefaultClient.Do(req)");
724    let _ = writeln!(out, "\tif err != nil {{");
725    let _ = writeln!(out, "\t\tt.Fatalf(\"request failed: %v\", err)");
726    let _ = writeln!(out, "\t}}");
727    let _ = writeln!(out, "\tdefer resp.Body.Close()");
728
729    // Read body.
730    let _ = writeln!(out, "\tbodyBytes, err := io.ReadAll(resp.Body)");
731    let _ = writeln!(out, "\tif err != nil {{");
732    let _ = writeln!(out, "\t\tt.Fatalf(\"read body failed: %v\", err)");
733    let _ = writeln!(out, "\t}}");
734
735    // Assert status code.
736    let _ = writeln!(out, "\tif resp.StatusCode != {expected_status} {{");
737    let _ = writeln!(
738        out,
739        "\t\tt.Fatalf(\"status: got %d want {expected_status}\", resp.StatusCode)"
740    );
741    let _ = writeln!(out, "\t}}");
742
743    // Assert body if expected.
744    if let Some(expected_body) = &expected.body {
745        match expected_body {
746            serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
747                let json_str = serde_json::to_string(expected_body).unwrap_or_default();
748                let escaped = go_string_literal(&json_str);
749                let _ = writeln!(out, "\tvar got map[string]any");
750                let _ = writeln!(out, "\tvar want map[string]any");
751                let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &got); err != nil {{");
752                let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal got: %v\", err)");
753                let _ = writeln!(out, "\t}}");
754                let _ = writeln!(
755                    out,
756                    "\tif err := json.Unmarshal([]byte({escaped}), &want); err != nil {{"
757                );
758                let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal want: %v\", err)");
759                let _ = writeln!(out, "\t}}");
760                let _ = writeln!(out, "\tif !reflect.DeepEqual(got, want) {{");
761                let _ = writeln!(out, "\t\tt.Fatalf(\"body mismatch: got %v want %v\", got, want)");
762                let _ = writeln!(out, "\t}}");
763            }
764            serde_json::Value::String(s) => {
765                let escaped = go_string_literal(s);
766                let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != {escaped} {{");
767                let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %s want {escaped}\", string(bodyBytes))");
768                let _ = writeln!(out, "\t}}");
769            }
770            other => {
771                let escaped = go_string_literal(&other.to_string());
772                let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != {escaped} {{");
773                let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %s want {escaped}\", string(bodyBytes))");
774                let _ = writeln!(out, "\t}}");
775            }
776        }
777    }
778
779    // Assert response headers if specified (skip special tokens and non-applicable headers).
780    for (name, value) in &expected.headers {
781        if value == "<<absent>>" || value == "<<present>>" || value == "<<uuid>>" {
782            // Skip special-token assertions for now.
783            continue;
784        }
785        let escaped_name = go_string_literal(name);
786        let escaped_value = go_string_literal(value);
787        let _ = writeln!(
788            out,
789            "\tif !strings.Contains(resp.Header.Get({escaped_name}), {escaped_value}) {{"
790        );
791        let _ = writeln!(
792            out,
793            "\t\tt.Fatalf(\"header {escaped_name} mismatch: got %s want to contain {escaped_value}\", resp.Header.Get({escaped_name}))"
794        );
795        let _ = writeln!(out, "\t}}");
796    }
797
798    let _ = writeln!(out, "}}");
799}
800
801/// Build setup lines (e.g. handle creation) and the argument list for the function call.
802///
803/// Returns `(setup_lines, args_string)`.
804fn build_args_and_setup(
805    input: &serde_json::Value,
806    args: &[crate::config::ArgMapping],
807    import_alias: &str,
808    options_type: Option<&str>,
809    fixture_id: &str,
810) -> (Vec<String>, String) {
811    use heck::ToUpperCamelCase;
812
813    if args.is_empty() {
814        return (Vec::new(), String::new());
815    }
816
817    let mut setup_lines: Vec<String> = Vec::new();
818    let mut parts: Vec<String> = Vec::new();
819
820    for arg in args {
821        if arg.arg_type == "mock_url" {
822            setup_lines.push(format!(
823                "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
824                arg.name,
825            ));
826            parts.push(arg.name.clone());
827            continue;
828        }
829
830        if arg.arg_type == "handle" {
831            // Generate a CreateEngine (or equivalent) call and pass the variable.
832            let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
833            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
834            let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
835            if config_value.is_null()
836                || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
837            {
838                setup_lines.push(format!(
839                    "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}",
840                    name = arg.name,
841                ));
842            } else {
843                let json_str = serde_json::to_string(config_value).unwrap_or_default();
844                let go_literal = go_string_literal(&json_str);
845                let name = &arg.name;
846                setup_lines.push(format!(
847                    "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}}"
848                ));
849                setup_lines.push(format!(
850                    "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}"
851                ));
852            }
853            parts.push(arg.name.clone());
854            continue;
855        }
856
857        let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
858        let val = input.get(field);
859
860        // Handle bytes type: fixture stores base64-encoded bytes.
861        // Emit a Go base64.StdEncoding.DecodeString call to decode at runtime.
862        if arg.arg_type == "bytes" {
863            let var_name = format!("{}Bytes", arg.name);
864            match val {
865                None | Some(serde_json::Value::Null) => {
866                    if arg.optional {
867                        parts.push("nil".to_string());
868                    } else {
869                        parts.push("[]byte{}".to_string());
870                    }
871                }
872                Some(serde_json::Value::String(s)) => {
873                    let go_b64 = go_string_literal(s);
874                    setup_lines.push(format!("{var_name}, _ := base64.StdEncoding.DecodeString({go_b64})"));
875                    parts.push(var_name);
876                }
877                Some(other) => {
878                    parts.push(format!("[]byte({})", json_to_go(other)));
879                }
880            }
881            continue;
882        }
883
884        match val {
885            None | Some(serde_json::Value::Null) if arg.optional => {
886                // Optional arg absent: emit Go zero/nil for the type.
887                match arg.arg_type.as_str() {
888                    "string" => {
889                        // Optional string in Go bindings is *string → nil.
890                        parts.push("nil".to_string());
891                    }
892                    "json_object" => {
893                        // Optional config struct → zero-value struct.
894                        if let Some(opts_type) = options_type {
895                            parts.push(format!("{import_alias}.{opts_type}{{}}"));
896                        } else {
897                            parts.push("nil".to_string());
898                        }
899                    }
900                    _ => {
901                        parts.push("nil".to_string());
902                    }
903                }
904            }
905            None | Some(serde_json::Value::Null) => {
906                // Required arg with no fixture value: pass a language-appropriate default.
907                let default_val = match arg.arg_type.as_str() {
908                    "string" => "\"\"".to_string(),
909                    "int" | "integer" | "i64" => "0".to_string(),
910                    "float" | "number" => "0.0".to_string(),
911                    "bool" | "boolean" => "false".to_string(),
912                    "json_object" => {
913                        if let Some(opts_type) = options_type {
914                            format!("{import_alias}.{opts_type}{{}}")
915                        } else {
916                            "nil".to_string()
917                        }
918                    }
919                    _ => "nil".to_string(),
920                };
921                parts.push(default_val);
922            }
923            Some(v) => {
924                match arg.arg_type.as_str() {
925                    "json_object" => {
926                        // JSON arrays unmarshal into []string (Go slices).
927                        // JSON objects with a known options_type unmarshal into that type.
928                        let is_array = v.is_array();
929                        let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
930                        if is_empty_obj {
931                            if let Some(opts_type) = options_type {
932                                parts.push(format!("{import_alias}.{opts_type}{{}}"));
933                            } else {
934                                parts.push("nil".to_string());
935                            }
936                        } else if is_array {
937                            // Array type — unmarshal into []string (typical for paths/texts).
938                            let json_str = serde_json::to_string(v).unwrap_or_default();
939                            let go_literal = go_string_literal(&json_str);
940                            let var_name = &arg.name;
941                            setup_lines.push(format!(
942                                "var {var_name} []string\n\tif err := json.Unmarshal([]byte({go_literal}), &{var_name}); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
943                            ));
944                            parts.push(var_name.to_string());
945                        } else if let Some(opts_type) = options_type {
946                            // Object with known type — unmarshal into typed struct.
947                            let json_str = serde_json::to_string(v).unwrap_or_default();
948                            let go_literal = go_string_literal(&json_str);
949                            let var_name = &arg.name;
950                            setup_lines.push(format!(
951                                "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}}"
952                            ));
953                            parts.push(var_name.to_string());
954                        } else {
955                            parts.push(json_to_go(v));
956                        }
957                    }
958                    "string" if arg.optional => {
959                        // Optional string in Go is *string — take address of a local.
960                        let var_name = format!("{}Val", arg.name);
961                        let go_val = json_to_go(v);
962                        setup_lines.push(format!("{var_name} := {go_val}"));
963                        parts.push(format!("&{var_name}"));
964                    }
965                    _ => {
966                        parts.push(json_to_go(v));
967                    }
968                }
969            }
970        }
971    }
972
973    (setup_lines, parts.join(", "))
974}
975
976#[allow(clippy::too_many_arguments)]
977fn render_assertion(
978    out: &mut String,
979    assertion: &Assertion,
980    result_var: &str,
981    import_alias: &str,
982    field_resolver: &FieldResolver,
983    optional_locals: &std::collections::HashMap<String, String>,
984    result_is_simple: bool,
985    result_is_array: bool,
986) {
987    // Handle synthetic / derived fields before the is_valid_for_result check
988    // so they are never treated as struct field accesses on the result.
989    if !result_is_simple {
990        if let Some(f) = &assertion.field {
991            // embed_texts returns *[][]float32; the embedding matrix is *result_var.
992            // We emit inline func() expressions so we don't need additional variables.
993            let embed_deref = format!("(*{result_var})");
994            match f.as_str() {
995                "chunks_have_content" => {
996                    let pred = format!(
997                        "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range *chunks {{ if c.Content == \"\" {{ return false }} }}; return true }}()"
998                    );
999                    match assertion.assertion_type.as_str() {
1000                        "is_true" => {
1001                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1002                        }
1003                        "is_false" => {
1004                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1005                        }
1006                        _ => {
1007                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1008                        }
1009                    }
1010                    return;
1011                }
1012                "chunks_have_embeddings" => {
1013                    let pred = format!(
1014                        "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 }}()"
1015                    );
1016                    match assertion.assertion_type.as_str() {
1017                        "is_true" => {
1018                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1019                        }
1020                        "is_false" => {
1021                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1022                        }
1023                        _ => {
1024                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1025                        }
1026                    }
1027                    return;
1028                }
1029                "embeddings" => {
1030                    match assertion.assertion_type.as_str() {
1031                        "count_equals" => {
1032                            if let Some(val) = &assertion.value {
1033                                if let Some(n) = val.as_u64() {
1034                                    let _ = writeln!(
1035                                        out,
1036                                        "\tassert.Equal(t, {n}, len({embed_deref}), \"expected exactly {n} elements\")"
1037                                    );
1038                                }
1039                            }
1040                        }
1041                        "count_min" => {
1042                            if let Some(val) = &assertion.value {
1043                                if let Some(n) = val.as_u64() {
1044                                    let _ = writeln!(
1045                                        out,
1046                                        "\tassert.GreaterOrEqual(t, len({embed_deref}), {n}, \"expected at least {n} elements\")"
1047                                    );
1048                                }
1049                            }
1050                        }
1051                        "not_empty" => {
1052                            let _ = writeln!(
1053                                out,
1054                                "\tassert.NotEmpty(t, {embed_deref}, \"expected non-empty embeddings\")"
1055                            );
1056                        }
1057                        "is_empty" => {
1058                            let _ = writeln!(out, "\tassert.Empty(t, {embed_deref}, \"expected empty embeddings\")");
1059                        }
1060                        _ => {
1061                            let _ = writeln!(
1062                                out,
1063                                "\t// skipped: unsupported assertion type on synthetic field 'embeddings'"
1064                            );
1065                        }
1066                    }
1067                    return;
1068                }
1069                "embedding_dimensions" => {
1070                    let expr = format!(
1071                        "func() int {{ if len({embed_deref}) == 0 {{ return 0 }}; return len({embed_deref}[0]) }}()"
1072                    );
1073                    match assertion.assertion_type.as_str() {
1074                        "equals" => {
1075                            if let Some(val) = &assertion.value {
1076                                if let Some(n) = val.as_u64() {
1077                                    let _ = writeln!(
1078                                        out,
1079                                        "\tif {expr} != {n} {{\n\t\tt.Errorf(\"equals mismatch: got %v\", {expr})\n\t}}"
1080                                    );
1081                                }
1082                            }
1083                        }
1084                        "greater_than" => {
1085                            if let Some(val) = &assertion.value {
1086                                if let Some(n) = val.as_u64() {
1087                                    let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
1088                                }
1089                            }
1090                        }
1091                        _ => {
1092                            let _ = writeln!(
1093                                out,
1094                                "\t// skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
1095                            );
1096                        }
1097                    }
1098                    return;
1099                }
1100                "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
1101                    let pred = match f.as_str() {
1102                        "embeddings_valid" => {
1103                            format!(
1104                                "func() bool {{ for _, e := range {embed_deref} {{ if len(e) == 0 {{ return false }} }}; return true }}()"
1105                            )
1106                        }
1107                        "embeddings_finite" => {
1108                            format!(
1109                                "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 }}()"
1110                            )
1111                        }
1112                        "embeddings_non_zero" => {
1113                            format!(
1114                                "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 }}()"
1115                            )
1116                        }
1117                        "embeddings_normalized" => {
1118                            format!(
1119                                "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 }}()"
1120                            )
1121                        }
1122                        _ => unreachable!(),
1123                    };
1124                    match assertion.assertion_type.as_str() {
1125                        "is_true" => {
1126                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1127                        }
1128                        "is_false" => {
1129                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1130                        }
1131                        _ => {
1132                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1133                        }
1134                    }
1135                    return;
1136                }
1137                // ---- keywords / keywords_count ----
1138                // Go ExtractionResult does not expose extracted_keywords; skip.
1139                "keywords" | "keywords_count" => {
1140                    let _ = writeln!(out, "\t// skipped: field '{f}' not available on Go ExtractionResult");
1141                    return;
1142                }
1143                _ => {}
1144            }
1145        }
1146    }
1147
1148    // Skip assertions on fields that don't exist on the result type.
1149    // When result_is_simple, all field assertions operate on the scalar result directly.
1150    if !result_is_simple {
1151        if let Some(f) = &assertion.field {
1152            if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1153                let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
1154                return;
1155            }
1156        }
1157    }
1158
1159    let field_expr = if result_is_simple {
1160        // The result IS the value — field access is irrelevant.
1161        result_var.to_string()
1162    } else {
1163        match &assertion.field {
1164            Some(f) if !f.is_empty() => {
1165                // Use the local variable if the field was dereferenced above.
1166                if let Some(local_var) = optional_locals.get(f.as_str()) {
1167                    local_var.clone()
1168                } else {
1169                    field_resolver.accessor(f, "go", result_var)
1170                }
1171            }
1172            _ => result_var.to_string(),
1173        }
1174    };
1175
1176    // Check if the field (after resolution) is optional, which means it's a pointer in Go.
1177    // Also check if a `.length` suffix's parent is optional (e.g., metadata.headings.length
1178    // where metadata.headings is optional → len() needs dereference).
1179    let is_optional = assertion
1180        .field
1181        .as_ref()
1182        .map(|f| {
1183            let resolved = field_resolver.resolve(f);
1184            let check_path = resolved
1185                .strip_suffix(".length")
1186                .or_else(|| resolved.strip_suffix(".count"))
1187                .or_else(|| resolved.strip_suffix(".size"))
1188                .unwrap_or(resolved);
1189            field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
1190        })
1191        .unwrap_or(false);
1192
1193    // When field_expr is `len(X)` and X is an optional (pointer) field, rewrite to `len(*X)`
1194    // and we'll wrap with a nil guard in the assertion handlers.
1195    let field_expr = if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') {
1196        let inner = &field_expr[4..field_expr.len() - 1];
1197        format!("len(*{inner})")
1198    } else {
1199        field_expr
1200    };
1201    // Build the nil-guard expression for the inner pointer (without len wrapper).
1202    let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
1203        Some(field_expr[5..field_expr.len() - 1].to_string())
1204    } else {
1205        None
1206    };
1207
1208    // For optional non-string fields that weren't dereferenced into locals,
1209    // we need to dereference the pointer in comparisons.
1210    let deref_field_expr = if is_optional && !field_expr.starts_with("len(") {
1211        format!("*{field_expr}")
1212    } else {
1213        field_expr.clone()
1214    };
1215
1216    // Detect array element access (e.g., `result.Assets[0].ContentHash`).
1217    // When the field_expr contains `[0]`, we must guard against an out-of-bounds
1218    // panic by checking that the array is non-empty first.
1219    // Extract the array slice expression (everything before `[0]`).
1220    let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
1221        let array_expr = &field_expr[..idx];
1222        Some(array_expr.to_string())
1223    } else {
1224        None
1225    };
1226
1227    // Render the assertion into a temporary buffer first, then wrap with the array
1228    // bounds guard (if needed) by adding one extra level of indentation.
1229    let mut assertion_buf = String::new();
1230    let out_ref = &mut assertion_buf;
1231
1232    match assertion.assertion_type.as_str() {
1233        "equals" => {
1234            if let Some(expected) = &assertion.value {
1235                let go_val = json_to_go(expected);
1236                // For string equality, trim whitespace to handle trailing newlines from the converter.
1237                if expected.is_string() {
1238                    // Wrap field expression with strings.TrimSpace() for string comparisons.
1239                    let trimmed_field = if is_optional && !field_expr.starts_with("len(") {
1240                        format!("strings.TrimSpace(*{field_expr})")
1241                    } else {
1242                        format!("strings.TrimSpace({field_expr})")
1243                    };
1244                    if is_optional && !field_expr.starts_with("len(") {
1245                        let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
1246                    } else {
1247                        let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
1248                    }
1249                } else if is_optional && !field_expr.starts_with("len(") {
1250                    let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
1251                } else {
1252                    let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
1253                }
1254                let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
1255                let _ = writeln!(out_ref, "\t}}");
1256            }
1257        }
1258        "contains" => {
1259            if let Some(expected) = &assertion.value {
1260                let go_val = json_to_go(expected);
1261                // Determine the "string view" of the field expression.
1262                // - *[]string → strings.Join(*field_expr, " ") for a nil-guarded check
1263                // - *string → string(*field_expr)
1264                // - string → string(field_expr) (or just field_expr for plain strings)
1265                // - result_is_array (result_is_simple + array result) → strings.Join(field_expr, " ")
1266                let resolved_field = assertion.field.as_deref().unwrap_or("");
1267                let resolved_name = field_resolver.resolve(resolved_field);
1268                let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
1269                let is_opt =
1270                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1271                let field_for_contains = if is_opt && field_is_array {
1272                    format!("strings.Join(*{field_expr}, \" \")")
1273                } else if is_opt {
1274                    format!("string(*{field_expr})")
1275                } else if field_is_array {
1276                    format!("strings.Join({field_expr}, \" \")")
1277                } else {
1278                    format!("string({field_expr})")
1279                };
1280                if is_opt {
1281                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1282                    let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
1283                    let _ = writeln!(
1284                        out_ref,
1285                        "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
1286                    );
1287                    let _ = writeln!(out_ref, "\t}}");
1288                    let _ = writeln!(out_ref, "\t}}");
1289                } else {
1290                    let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
1291                    let _ = writeln!(
1292                        out_ref,
1293                        "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
1294                    );
1295                    let _ = writeln!(out_ref, "\t}}");
1296                }
1297            }
1298        }
1299        "contains_all" => {
1300            if let Some(values) = &assertion.values {
1301                let resolved_field = assertion.field.as_deref().unwrap_or("");
1302                let resolved_name = field_resolver.resolve(resolved_field);
1303                let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
1304                let is_opt =
1305                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1306                for val in values {
1307                    let go_val = json_to_go(val);
1308                    let field_for_contains = if is_opt && field_is_array {
1309                        format!("strings.Join(*{field_expr}, \" \")")
1310                    } else if is_opt {
1311                        format!("string(*{field_expr})")
1312                    } else if field_is_array {
1313                        format!("strings.Join({field_expr}, \" \")")
1314                    } else {
1315                        format!("string({field_expr})")
1316                    };
1317                    if is_opt {
1318                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1319                        let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
1320                        let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
1321                        let _ = writeln!(out_ref, "\t}}");
1322                        let _ = writeln!(out_ref, "\t}}");
1323                    } else {
1324                        let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
1325                        let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
1326                        let _ = writeln!(out_ref, "\t}}");
1327                    }
1328                }
1329            }
1330        }
1331        "not_contains" => {
1332            if let Some(expected) = &assertion.value {
1333                let go_val = json_to_go(expected);
1334                let resolved_field = assertion.field.as_deref().unwrap_or("");
1335                let resolved_name = field_resolver.resolve(resolved_field);
1336                let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
1337                let is_opt =
1338                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1339                let field_for_contains = if is_opt && field_is_array {
1340                    format!("strings.Join(*{field_expr}, \" \")")
1341                } else if is_opt {
1342                    format!("string(*{field_expr})")
1343                } else if field_is_array {
1344                    format!("strings.Join({field_expr}, \" \")")
1345                } else {
1346                    format!("string({field_expr})")
1347                };
1348                let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
1349                let _ = writeln!(
1350                    out_ref,
1351                    "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
1352                );
1353                let _ = writeln!(out_ref, "\t}}");
1354            }
1355        }
1356        "not_empty" => {
1357            // For optional struct pointers (not arrays), just check != nil.
1358            // For optional slice/string pointers, check nil and len.
1359            let field_is_array = {
1360                let rf = assertion.field.as_deref().unwrap_or("");
1361                let rn = field_resolver.resolve(rf);
1362                field_resolver.is_array(rn)
1363            };
1364            if is_optional && !field_is_array {
1365                // Struct pointer: non-empty means not nil.
1366                let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
1367            } else if is_optional {
1368                let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
1369            } else {
1370                let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
1371            }
1372            let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
1373            let _ = writeln!(out_ref, "\t}}");
1374        }
1375        "is_empty" => {
1376            let field_is_array = {
1377                let rf = assertion.field.as_deref().unwrap_or("");
1378                let rn = field_resolver.resolve(rf);
1379                field_resolver.is_array(rn)
1380            };
1381            if is_optional && !field_is_array {
1382                // Struct pointer: empty means nil.
1383                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1384            } else if is_optional {
1385                let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
1386            } else {
1387                let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
1388            }
1389            let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
1390            let _ = writeln!(out_ref, "\t}}");
1391        }
1392        "contains_any" => {
1393            if let Some(values) = &assertion.values {
1394                let resolved_field = assertion.field.as_deref().unwrap_or("");
1395                let resolved_name = field_resolver.resolve(resolved_field);
1396                let field_is_array = field_resolver.is_array(resolved_name);
1397                let is_opt =
1398                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1399                let field_for_contains = if is_opt && field_is_array {
1400                    format!("strings.Join(*{field_expr}, \" \")")
1401                } else if is_opt {
1402                    format!("*{field_expr}")
1403                } else if field_is_array {
1404                    format!("strings.Join({field_expr}, \" \")")
1405                } else {
1406                    field_expr.clone()
1407                };
1408                let _ = writeln!(out_ref, "\t{{");
1409                let _ = writeln!(out_ref, "\t\tfound := false");
1410                for val in values {
1411                    let go_val = json_to_go(val);
1412                    let _ = writeln!(
1413                        out_ref,
1414                        "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
1415                    );
1416                }
1417                let _ = writeln!(out_ref, "\t\tif !found {{");
1418                let _ = writeln!(
1419                    out_ref,
1420                    "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
1421                );
1422                let _ = writeln!(out_ref, "\t\t}}");
1423                let _ = writeln!(out_ref, "\t}}");
1424            }
1425        }
1426        "greater_than" => {
1427            if let Some(val) = &assertion.value {
1428                let go_val = json_to_go(val);
1429                // Use `< N+1` instead of `<= N` to avoid golangci-lint sloppyLen
1430                // warning when N is 0 (len(x) <= 0 → len(x) < 1).
1431                // For optional (pointer) fields, dereference and guard with nil check.
1432                if is_optional {
1433                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1434                    if let Some(n) = val.as_u64() {
1435                        let next = n + 1;
1436                        let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
1437                    } else {
1438                        let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
1439                    }
1440                    let _ = writeln!(
1441                        out_ref,
1442                        "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
1443                    );
1444                    let _ = writeln!(out_ref, "\t\t}}");
1445                    let _ = writeln!(out_ref, "\t}}");
1446                } else if let Some(n) = val.as_u64() {
1447                    let next = n + 1;
1448                    let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
1449                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
1450                    let _ = writeln!(out_ref, "\t}}");
1451                } else {
1452                    let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
1453                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
1454                    let _ = writeln!(out_ref, "\t}}");
1455                }
1456            }
1457        }
1458        "less_than" => {
1459            if let Some(val) = &assertion.value {
1460                let go_val = json_to_go(val);
1461                let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
1462                let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
1463                let _ = writeln!(out_ref, "\t}}");
1464            }
1465        }
1466        "greater_than_or_equal" => {
1467            if let Some(val) = &assertion.value {
1468                let go_val = json_to_go(val);
1469                if let Some(ref guard) = nil_guard_expr {
1470                    let _ = writeln!(out_ref, "\tif {guard} != nil {{");
1471                    let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
1472                    let _ = writeln!(
1473                        out_ref,
1474                        "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
1475                    );
1476                    let _ = writeln!(out_ref, "\t\t}}");
1477                    let _ = writeln!(out_ref, "\t}}");
1478                } else if is_optional && !field_expr.starts_with("len(") {
1479                    // Optional pointer field: nil-guard and dereference before comparison.
1480                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1481                    let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
1482                    let _ = writeln!(
1483                        out_ref,
1484                        "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
1485                    );
1486                    let _ = writeln!(out_ref, "\t\t}}");
1487                    let _ = writeln!(out_ref, "\t}}");
1488                } else {
1489                    let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
1490                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
1491                    let _ = writeln!(out_ref, "\t}}");
1492                }
1493            }
1494        }
1495        "less_than_or_equal" => {
1496            if let Some(val) = &assertion.value {
1497                let go_val = json_to_go(val);
1498                let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
1499                let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
1500                let _ = writeln!(out_ref, "\t}}");
1501            }
1502        }
1503        "starts_with" => {
1504            if let Some(expected) = &assertion.value {
1505                let go_val = json_to_go(expected);
1506                let field_for_prefix = if is_optional
1507                    && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1508                {
1509                    format!("string(*{field_expr})")
1510                } else {
1511                    format!("string({field_expr})")
1512                };
1513                let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
1514                let _ = writeln!(
1515                    out_ref,
1516                    "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
1517                );
1518                let _ = writeln!(out_ref, "\t}}");
1519            }
1520        }
1521        "count_min" => {
1522            if let Some(val) = &assertion.value {
1523                if let Some(n) = val.as_u64() {
1524                    if is_optional {
1525                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1526                        let _ = writeln!(
1527                            out_ref,
1528                            "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected at least {n} elements\")"
1529                        );
1530                        let _ = writeln!(out_ref, "\t}}");
1531                    } else {
1532                        let _ = writeln!(
1533                            out_ref,
1534                            "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
1535                        );
1536                    }
1537                }
1538            }
1539        }
1540        "count_equals" => {
1541            if let Some(val) = &assertion.value {
1542                if let Some(n) = val.as_u64() {
1543                    if is_optional {
1544                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1545                        let _ = writeln!(
1546                            out_ref,
1547                            "\t\tassert.Equal(t, len(*{field_expr}), {n}, \"expected exactly {n} elements\")"
1548                        );
1549                        let _ = writeln!(out_ref, "\t}}");
1550                    } else {
1551                        let _ = writeln!(
1552                            out_ref,
1553                            "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
1554                        );
1555                    }
1556                }
1557            }
1558        }
1559        "is_true" => {
1560            if is_optional {
1561                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1562                let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
1563                let _ = writeln!(out_ref, "\t}}");
1564            } else {
1565                let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
1566            }
1567        }
1568        "is_false" => {
1569            if is_optional {
1570                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1571                let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
1572                let _ = writeln!(out_ref, "\t}}");
1573            } else {
1574                let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
1575            }
1576        }
1577        "method_result" => {
1578            if let Some(method_name) = &assertion.method {
1579                let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
1580                let check = assertion.check.as_deref().unwrap_or("is_true");
1581                // For pointer-returning functions, dereference with `*`. Value-returning
1582                // functions (e.g., NodeInfo field access) are used directly.
1583                let deref_expr = if info.is_pointer {
1584                    format!("*{}", info.call_expr)
1585                } else {
1586                    info.call_expr.clone()
1587                };
1588                match check {
1589                    "equals" => {
1590                        if let Some(val) = &assertion.value {
1591                            if val.is_boolean() {
1592                                if val.as_bool() == Some(true) {
1593                                    let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
1594                                } else {
1595                                    let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
1596                                }
1597                            } else {
1598                                // Apply type cast to numeric literals when the method returns
1599                                // a typed uint (e.g., *uint) to avoid reflect.DeepEqual
1600                                // mismatches between int and uint in testify's assert.Equal.
1601                                let go_val = if let Some(cast) = info.value_cast {
1602                                    if val.is_number() {
1603                                        format!("{cast}({})", json_to_go(val))
1604                                    } else {
1605                                        json_to_go(val)
1606                                    }
1607                                } else {
1608                                    json_to_go(val)
1609                                };
1610                                let _ = writeln!(
1611                                    out_ref,
1612                                    "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
1613                                );
1614                            }
1615                        }
1616                    }
1617                    "is_true" => {
1618                        let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
1619                    }
1620                    "is_false" => {
1621                        let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
1622                    }
1623                    "greater_than_or_equal" => {
1624                        if let Some(val) = &assertion.value {
1625                            let n = val.as_u64().unwrap_or(0);
1626                            // Use the value_cast type if available (e.g., uint for named_children_count).
1627                            let cast = info.value_cast.unwrap_or("uint");
1628                            let _ = writeln!(
1629                                out_ref,
1630                                "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
1631                            );
1632                        }
1633                    }
1634                    "count_min" => {
1635                        if let Some(val) = &assertion.value {
1636                            let n = val.as_u64().unwrap_or(0);
1637                            let _ = writeln!(
1638                                out_ref,
1639                                "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
1640                            );
1641                        }
1642                    }
1643                    "contains" => {
1644                        if let Some(val) = &assertion.value {
1645                            let go_val = json_to_go(val);
1646                            let _ = writeln!(
1647                                out_ref,
1648                                "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
1649                            );
1650                        }
1651                    }
1652                    "is_error" => {
1653                        let _ = writeln!(out_ref, "\t{{");
1654                        let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
1655                        let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
1656                        let _ = writeln!(out_ref, "\t}}");
1657                    }
1658                    other_check => {
1659                        panic!("Go e2e generator: unsupported method_result check type: {other_check}");
1660                    }
1661                }
1662            } else {
1663                panic!("Go e2e generator: method_result assertion missing 'method' field");
1664            }
1665        }
1666        "min_length" => {
1667            if let Some(val) = &assertion.value {
1668                if let Some(n) = val.as_u64() {
1669                    if is_optional {
1670                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1671                        let _ = writeln!(
1672                            out_ref,
1673                            "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
1674                        );
1675                        let _ = writeln!(out_ref, "\t}}");
1676                    } else {
1677                        let _ = writeln!(
1678                            out_ref,
1679                            "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
1680                        );
1681                    }
1682                }
1683            }
1684        }
1685        "max_length" => {
1686            if let Some(val) = &assertion.value {
1687                if let Some(n) = val.as_u64() {
1688                    if is_optional {
1689                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1690                        let _ = writeln!(
1691                            out_ref,
1692                            "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
1693                        );
1694                        let _ = writeln!(out_ref, "\t}}");
1695                    } else {
1696                        let _ = writeln!(
1697                            out_ref,
1698                            "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
1699                        );
1700                    }
1701                }
1702            }
1703        }
1704        "ends_with" => {
1705            if let Some(expected) = &assertion.value {
1706                let go_val = json_to_go(expected);
1707                let field_for_suffix = if is_optional
1708                    && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1709                {
1710                    format!("string(*{field_expr})")
1711                } else {
1712                    format!("string({field_expr})")
1713                };
1714                let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
1715                let _ = writeln!(
1716                    out_ref,
1717                    "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
1718                );
1719                let _ = writeln!(out_ref, "\t}}");
1720            }
1721        }
1722        "matches_regex" => {
1723            if let Some(expected) = &assertion.value {
1724                let go_val = json_to_go(expected);
1725                let field_for_regex = if is_optional
1726                    && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1727                {
1728                    format!("*{field_expr}")
1729                } else {
1730                    field_expr.clone()
1731                };
1732                let _ = writeln!(
1733                    out_ref,
1734                    "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
1735                );
1736            }
1737        }
1738        "not_error" => {
1739            // Already handled by the `if err != nil` check above.
1740        }
1741        "error" => {
1742            // Handled at the test function level.
1743        }
1744        other => {
1745            panic!("Go e2e generator: unsupported assertion type: {other}");
1746        }
1747    }
1748
1749    // If the assertion accesses an array element via [0], wrap the generated code in a
1750    // bounds check to prevent an index-out-of-range panic when the array is empty.
1751    if let Some(ref arr) = array_guard {
1752        if !assertion_buf.is_empty() {
1753            let _ = writeln!(out, "\tif len({arr}) > 0 {{");
1754            // Re-indent each line by one additional tab level.
1755            for line in assertion_buf.lines() {
1756                let _ = writeln!(out, "\t{line}");
1757            }
1758            let _ = writeln!(out, "\t}}");
1759        }
1760    } else {
1761        out.push_str(&assertion_buf);
1762    }
1763}
1764
1765/// Metadata about the return type of a Go method call for `method_result` assertions.
1766struct GoMethodCallInfo {
1767    /// The call expression string.
1768    call_expr: String,
1769    /// Whether the return type is a pointer (needs `*` dereference for value comparison).
1770    is_pointer: bool,
1771    /// Optional Go type cast to apply to numeric literal values in `equals` assertions
1772    /// (e.g., `"uint"` so that `0` becomes `uint(0)` to match `*uint` deref type).
1773    value_cast: Option<&'static str>,
1774}
1775
1776/// Build a Go call expression for a `method_result` assertion on a tree-sitter Tree.
1777///
1778/// Maps method names to the appropriate Go function calls, matching the Go binding API
1779/// in `packages/go/binding.go`. Returns a [`GoMethodCallInfo`] describing the call and
1780/// its return type characteristics.
1781///
1782/// Return types by method:
1783/// - `has_error_nodes`, `contains_node_type` → `*bool` (pointer)
1784/// - `error_count` → `*uint` (pointer, value_cast = "uint")
1785/// - `tree_to_sexp` → `*string` (pointer)
1786/// - `root_node_type` → `string` via `RootNodeInfo(tree).Kind` (value)
1787/// - `named_children_count` → `uint` via `RootNodeInfo(tree).NamedChildCount` (value, value_cast = "uint")
1788/// - `find_nodes_by_type` → `*[]NodeInfo` (pointer to slice)
1789/// - `run_query` → `(*[]QueryMatch, error)` (pointer + error; use `is_error` check type)
1790fn build_go_method_call(
1791    result_var: &str,
1792    method_name: &str,
1793    args: Option<&serde_json::Value>,
1794    import_alias: &str,
1795) -> GoMethodCallInfo {
1796    match method_name {
1797        "root_node_type" => GoMethodCallInfo {
1798            call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
1799            is_pointer: false,
1800            value_cast: None,
1801        },
1802        "named_children_count" => GoMethodCallInfo {
1803            call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
1804            is_pointer: false,
1805            value_cast: Some("uint"),
1806        },
1807        "has_error_nodes" => GoMethodCallInfo {
1808            call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
1809            is_pointer: true,
1810            value_cast: None,
1811        },
1812        "error_count" | "tree_error_count" => GoMethodCallInfo {
1813            call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
1814            is_pointer: true,
1815            value_cast: Some("uint"),
1816        },
1817        "tree_to_sexp" => GoMethodCallInfo {
1818            call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
1819            is_pointer: true,
1820            value_cast: None,
1821        },
1822        "contains_node_type" => {
1823            let node_type = args
1824                .and_then(|a| a.get("node_type"))
1825                .and_then(|v| v.as_str())
1826                .unwrap_or("");
1827            GoMethodCallInfo {
1828                call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
1829                is_pointer: true,
1830                value_cast: None,
1831            }
1832        }
1833        "find_nodes_by_type" => {
1834            let node_type = args
1835                .and_then(|a| a.get("node_type"))
1836                .and_then(|v| v.as_str())
1837                .unwrap_or("");
1838            GoMethodCallInfo {
1839                call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
1840                is_pointer: true,
1841                value_cast: None,
1842            }
1843        }
1844        "run_query" => {
1845            let query_source = args
1846                .and_then(|a| a.get("query_source"))
1847                .and_then(|v| v.as_str())
1848                .unwrap_or("");
1849            let language = args
1850                .and_then(|a| a.get("language"))
1851                .and_then(|v| v.as_str())
1852                .unwrap_or("");
1853            let query_lit = go_string_literal(query_source);
1854            let lang_lit = go_string_literal(language);
1855            // RunQuery returns (*[]QueryMatch, error) — use is_error check type.
1856            GoMethodCallInfo {
1857                call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
1858                is_pointer: false,
1859                value_cast: None,
1860            }
1861        }
1862        other => {
1863            let method_pascal = other.to_upper_camel_case();
1864            GoMethodCallInfo {
1865                call_expr: format!("{result_var}.{method_pascal}()"),
1866                is_pointer: false,
1867                value_cast: None,
1868            }
1869        }
1870    }
1871}
1872
1873/// Convert a `serde_json::Value` to a Go literal string.
1874fn json_to_go(value: &serde_json::Value) -> String {
1875    match value {
1876        serde_json::Value::String(s) => go_string_literal(s),
1877        serde_json::Value::Bool(b) => b.to_string(),
1878        serde_json::Value::Number(n) => n.to_string(),
1879        serde_json::Value::Null => "nil".to_string(),
1880        // For complex types, serialize to JSON string and pass as literal.
1881        other => go_string_literal(&other.to_string()),
1882    }
1883}
1884
1885// ---------------------------------------------------------------------------
1886// Visitor generation
1887// ---------------------------------------------------------------------------
1888
1889/// Derive a unique, exported Go struct name for a visitor from a fixture ID.
1890///
1891/// E.g. `visitor_continue_default` → `visitorContinueDefault` (unexported, avoids
1892/// polluting the exported API of the test package while still being package-level).
1893fn visitor_struct_name(fixture_id: &str) -> String {
1894    use heck::ToUpperCamelCase;
1895    // Use UpperCamelCase so Go treats it as exported — required for method sets.
1896    format!("testVisitor{}", fixture_id.to_upper_camel_case())
1897}
1898
1899/// Emit a package-level Go struct declaration and all its visitor methods.
1900fn emit_go_visitor_struct(
1901    out: &mut String,
1902    struct_name: &str,
1903    visitor_spec: &crate::fixture::VisitorSpec,
1904    import_alias: &str,
1905) {
1906    let _ = writeln!(out, "type {struct_name} struct{{}}");
1907    for (method_name, action) in &visitor_spec.callbacks {
1908        emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
1909    }
1910}
1911
1912/// Emit a Go visitor method for a callback action on the named struct.
1913fn emit_go_visitor_method(
1914    out: &mut String,
1915    struct_name: &str,
1916    method_name: &str,
1917    action: &CallbackAction,
1918    import_alias: &str,
1919) {
1920    let camel_method = method_to_camel(method_name);
1921    let params = match method_name {
1922        "visit_link" => format!("_ {import_alias}.NodeContext, href, text, title string"),
1923        "visit_image" => format!("_ {import_alias}.NodeContext, src, alt, title string"),
1924        "visit_heading" => format!("_ {import_alias}.NodeContext, level int, text, id string"),
1925        "visit_code_block" => format!("_ {import_alias}.NodeContext, lang, code string"),
1926        "visit_code_inline"
1927        | "visit_strong"
1928        | "visit_emphasis"
1929        | "visit_strikethrough"
1930        | "visit_underline"
1931        | "visit_subscript"
1932        | "visit_superscript"
1933        | "visit_mark"
1934        | "visit_button"
1935        | "visit_summary"
1936        | "visit_figcaption"
1937        | "visit_definition_term"
1938        | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
1939        "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
1940        "visit_list_item" => {
1941            format!("_ {import_alias}.NodeContext, ordered bool, marker, text string")
1942        }
1943        "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth int"),
1944        "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
1945        "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName, html string"),
1946        "visit_form" => format!("_ {import_alias}.NodeContext, actionUrl, method string"),
1947        "visit_input" => format!("_ {import_alias}.NodeContext, inputType, name, value string"),
1948        "visit_audio" | "visit_video" | "visit_iframe" => {
1949            format!("_ {import_alias}.NodeContext, src string")
1950        }
1951        "visit_details" => format!("_ {import_alias}.NodeContext, isOpen bool"),
1952        "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
1953            format!("_ {import_alias}.NodeContext, output string")
1954        }
1955        "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
1956        "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
1957        _ => format!("_ {import_alias}.NodeContext"),
1958    };
1959
1960    let _ = writeln!(
1961        out,
1962        "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
1963    );
1964    match action {
1965        CallbackAction::Skip => {
1966            let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip");
1967        }
1968        CallbackAction::Continue => {
1969            let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue");
1970        }
1971        CallbackAction::PreserveHtml => {
1972            let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHtml");
1973        }
1974        CallbackAction::Custom { output } => {
1975            let escaped = go_string_literal(output);
1976            let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
1977        }
1978        CallbackAction::CustomTemplate { template } => {
1979            // Convert {var} placeholders to %s format verbs and collect arg names.
1980            // E.g. `QUOTE: "{text}"` → fmt.Sprintf("QUOTE: \"%s\"", text)
1981            let (fmt_str, fmt_args) = template_to_sprintf(template);
1982            let escaped_fmt = go_string_literal(&fmt_str);
1983            if fmt_args.is_empty() {
1984                let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
1985            } else {
1986                let args_str = fmt_args.join(", ");
1987                let _ = writeln!(
1988                    out,
1989                    "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
1990                );
1991            }
1992        }
1993    }
1994    let _ = writeln!(out, "}}");
1995}
1996
1997/// Convert a `{var}` template string into a `fmt.Sprintf` format string and argument list.
1998///
1999/// For example, `QUOTE: "{text}"` becomes `("QUOTE: \"%s\"", vec!["text"])`.
2000fn template_to_sprintf(template: &str) -> (String, Vec<String>) {
2001    let mut fmt_str = String::new();
2002    let mut args: Vec<String> = Vec::new();
2003    let mut chars = template.chars().peekable();
2004    while let Some(c) = chars.next() {
2005        if c == '{' {
2006            // Collect placeholder name until '}'.
2007            let mut name = String::new();
2008            for inner in chars.by_ref() {
2009                if inner == '}' {
2010                    break;
2011                }
2012                name.push(inner);
2013            }
2014            fmt_str.push_str("%s");
2015            args.push(name);
2016        } else {
2017            fmt_str.push(c);
2018        }
2019    }
2020    (fmt_str, args)
2021}
2022
2023/// Convert snake_case method names to Go camelCase.
2024fn method_to_camel(snake: &str) -> String {
2025    use heck::ToUpperCamelCase;
2026    snake.to_upper_camel_case()
2027}
2028
2029#[cfg(test)]
2030mod tests {
2031    use super::*;
2032    use crate::config::{CallConfig, E2eConfig};
2033    use crate::field_access::FieldResolver;
2034    use crate::fixture::{Assertion, Fixture};
2035
2036    fn make_fixture(id: &str) -> Fixture {
2037        Fixture {
2038            id: id.to_string(),
2039            category: None,
2040            description: "test fixture".to_string(),
2041            tags: vec![],
2042            skip: None,
2043            call: None,
2044            input: serde_json::Value::Null,
2045            mock_response: None,
2046            source: String::new(),
2047            http: None,
2048            assertions: vec![Assertion {
2049                assertion_type: "not_error".to_string(),
2050                field: None,
2051                value: None,
2052                values: None,
2053                method: None,
2054                args: None,
2055                check: None,
2056            }],
2057            visitor: None,
2058        }
2059    }
2060
2061    /// snake_case function names in `[e2e.call]` must be routed through `to_go_name`
2062    /// so the emitted Go call uses the idiomatic CamelCase (e.g. `CleanExtractedText`
2063    /// instead of `clean_extracted_text`).
2064    #[test]
2065    fn test_go_method_name_uses_go_casing() {
2066        let e2e_config = E2eConfig {
2067            call: CallConfig {
2068                function: "clean_extracted_text".to_string(),
2069                module: "github.com/example/mylib".to_string(),
2070                result_var: "result".to_string(),
2071                r#async: false,
2072                path: None,
2073                method: None,
2074                args: vec![],
2075                overrides: std::collections::HashMap::new(),
2076                returns_result: true,
2077                returns_void: false,
2078                skip_languages: vec![],
2079            },
2080            ..E2eConfig::default()
2081        };
2082
2083        let fixture = make_fixture("basic_text");
2084        let resolver = FieldResolver::new(
2085            &std::collections::HashMap::new(),
2086            &std::collections::HashSet::new(),
2087            &std::collections::HashSet::new(),
2088            &std::collections::HashSet::new(),
2089        );
2090        let mut out = String::new();
2091        render_test_function(&mut out, &fixture, "kreuzberg", &resolver, &e2e_config);
2092
2093        assert!(
2094            out.contains("kreuzberg.CleanExtractedText("),
2095            "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
2096        );
2097        assert!(
2098            !out.contains("kreuzberg.clean_extracted_text("),
2099            "must not emit raw snake_case method name, got:\n{out}"
2100        );
2101    }
2102}