//! Gleam e2e test generator using gleeunit/should.
//!
//! Generates `packages/gleam/test/<crate>_test.gleam` files from JSON fixtures.
//! HTTP fixtures hit the mock server at `MOCK_SERVER_URL/fixtures/<id>` using
//! the `gleam_httpc` HTTP client library. Non-HTTP fixtures without a gleam-specific
//! call override emit a skip stub.

use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::hash::{self, CommentStyle};
use crate::e2e::config::E2eConfig;
use crate::e2e::escape::{escape_gleam, sanitize_filename, sanitize_ident};
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::{Assertion, Fixture, FixtureGroup, ValidationErrorExpectation};
use anyhow::Result;
use heck::{ToPascalCase, ToSnakeCase};
use std::collections::HashSet;
use std::fmt::Write as FmtWrite;
use std::path::PathBuf;

use super::E2eCodegen;
use super::client;

/// Gleam e2e code generator.
pub struct GleamE2eCodegen;

impl E2eCodegen for GleamE2eCodegen {
    fn generate(
        &self,
        groups: &[FixtureGroup],
        e2e_config: &E2eConfig,
        config: &ResolvedCrateConfig,
        _type_defs: &[crate::core::ir::TypeDef],
        _enums: &[crate::core::ir::EnumDef],
    ) -> Result<Vec<GeneratedFile>> {
        let lang = self.language_name();
        let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);

        let mut files = Vec::new();

        // Resolve call config with overrides.
        let call = &e2e_config.call;
        let overrides = call.overrides.get(lang);
        let module_path = overrides
            .and_then(|o| o.module.as_ref())
            .cloned()
            .unwrap_or_else(|| call.module.clone());
        let function_name = overrides
            .and_then(|o| o.function.as_ref())
            .cloned()
            .unwrap_or_else(|| call.function.clone());
        let result_var = &call.result_var;

        // Resolve package config.
        let gleam_pkg = e2e_config.resolve_package("gleam");
        let pkg_path = gleam_pkg
            .as_ref()
            .and_then(|p| p.path.as_ref())
            .cloned()
            .unwrap_or_else(|| "../../packages/gleam".to_string());
        let pkg_name = gleam_pkg
            .as_ref()
            .and_then(|p| p.name.as_ref())
            .cloned()
            .unwrap_or_else(|| config.name.to_snake_case());

        // Generate gleam.toml.
        files.push(GeneratedFile {
            path: output_base.join("gleam.toml"),
            content: render_gleam_toml(&pkg_path, &pkg_name, e2e_config.dep_mode),
            generated_header: false,
        });

        // OTP application atom. Defaults to the snake-cased downstream crate
        // name (matches `pkg_name`); kept as a separate binding because
        // `application:ensure_all_started/1` and the Erlang shim function
        // identifier both interpolate this same atom.
        let app_name = pkg_name.clone();

        // Gleam requires a `src/` directory even for test-only projects.
        // Emit a helper module with `read_file_bytes` external for loading test
        // documents as BitArray at runtime.
        let e2e_helpers = format!(
            "// Generated by alef. Do not edit by hand.\n\
            // E2e helper module — provides file-reading utilities for Gleam tests.\n\
            import gleam/dynamic\n\
            \n\
            /// Read a file into a BitArray via the Erlang :file module.\n\
            /// The path is relative to the e2e working directory when `gleam test` runs.\n\
            @external(erlang, \"file\", \"read_file\")\n\
            pub fn read_file_bytes(path: String) -> Result(BitArray, dynamic.Dynamic)\n\
            \n\
            /// Ensure the {app_name} OTP application and all its dependencies are started.\n\
            /// This is required when running `gleam test` outside of `mix test`, since the\n\
            /// Rustler NIF init hook needs the :{app_name} application to be started before\n\
            /// any binding-native functions can be called.\n\
            /// Calls the Erlang shim e2e_startup:start_app/0.\n\
            @external(erlang, \"e2e_startup\", \"start_app\")\n\
            pub fn start_app() -> Nil\n",
        );
        // Erlang shim module that starts the configured OTP application and all deps.
        // Compiled alongside the Gleam source when gleam test is run.
        // Must start elixir first (provides Elixir.Application used by Rustler NIF init),
        // then ensure the downstream-binding OTP application and its transitive deps are running.
        let erlang_startup = format!(
            "%% Generated by alef. Do not edit by hand.\n\
            %% Starts the {app_name} OTP application and all its dependencies.\n\
            %% Called by e2e_gleam_test.main/0 before gleeunit.main/0.\n\
            -module(e2e_startup).\n\
            -export([start_app/0]).\n\
            \n\
            start_app() ->\n\
            \x20\x20\x20\x20%% Elixir runtime must be started before {app_name} NIF init\n\
            \x20\x20\x20\x20%% because Rustler uses Elixir.Application.app_dir/2 to locate the .so.\n\
            \x20\x20\x20\x20%% Gracefully fall back when Elixir is not a runtime dependency.\n\
            \x20\x20\x20\x20case application:ensure_all_started(elixir) of\n\
            \x20\x20\x20\x20\x20\x20\x20\x20{{ok, _}} -> ok;\n\
            \x20\x20\x20\x20\x20\x20\x20\x20{{error, _}} -> ok\n\
            \x20\x20\x20\x20end,\n\
            \x20\x20\x20\x20{{ok, _}} = application:ensure_all_started({app_name}),\n\
            \x20\x20\x20\x20nil.\n",
        );
        files.push(GeneratedFile {
            path: output_base.join("src").join("e2e_gleam.gleam"),
            content: e2e_helpers,
            generated_header: false,
        });
        files.push(GeneratedFile {
            path: output_base.join("src").join("e2e_startup.erl"),
            content: erlang_startup,
            generated_header: false,
        });

        // Track whether any test file was emitted.
        let mut any_tests = false;

        // Generate test files per category.
        for group in groups {
            let active: Vec<&Fixture> = group
                .fixtures
                .iter()
                // Include both HTTP and non-HTTP fixtures. Filter out those marked as skip.
                .filter(|f| super::should_include_fixture(f, lang, e2e_config))
                // gleam_httpc cannot follow HTTP/1.1 protocol upgrades (101 Switching
                // Protocols), so skip WebSocket-upgrade fixtures whose request advertises
                // Upgrade: websocket. The server returns 101 and gleam_httpc times out.
                .filter(|f| {
                    if let Some(http) = &f.http {
                        let has_upgrade = http
                            .request
                            .headers
                            .iter()
                            .any(|(k, v)| k.eq_ignore_ascii_case("upgrade") && v.eq_ignore_ascii_case("websocket"));
                        !has_upgrade
                    } else {
                        true
                    }
                })
                // For non-HTTP fixtures, include all (will use default or override call config).
                // Gleam always has a call override or can use the default call config.
                .collect();

            if active.is_empty() {
                continue;
            }

            let filename = format!("{}_test.gleam", sanitize_filename(&group.category));
            // Look up gleam-specific config for element_type → record-constructor
            // recipes. Empty slice when the downstream hasn't configured any.
            let element_constructors: &[crate::core::config::GleamElementConstructor] = config
                .gleam
                .as_ref()
                .map(|g| g.element_constructors.as_slice())
                .unwrap_or(&[]);
            // Optional wrapper template used when a json_object arg has no
            // matching element_type recipe.
            let json_object_wrapper: Option<&str> =
                config.gleam.as_ref().and_then(|g| g.json_object_wrapper.as_deref());
            let content = render_test_file(
                &group.category,
                &active,
                e2e_config,
                &module_path,
                &function_name,
                result_var,
                &e2e_config.call.args,
                element_constructors,
                json_object_wrapper,
            );
            files.push(GeneratedFile {
                path: output_base.join("test").join(filename),
                content,
                generated_header: true,
            });
            any_tests = true;
        }

        // Always emit the gleeunit entry module — `gleam test` invokes
        // `<package>_test.main()` to discover and run all `_test.gleam` files.
        // When no fixture-driven tests exist, also include a tiny smoke test so
        // the suite is non-empty.
        let entry = if any_tests {
            concat!(
                "// Generated by alef. Do not edit by hand.\n",
                "import gleeunit\n",
                "import e2e_gleam\n",
                "\n",
                "pub fn main() {\n",
                "  let _ = e2e_gleam.start_app()\n",
                "  gleeunit.main()\n",
                "}\n",
            )
            .to_string()
        } else {
            concat!(
                "// Generated by alef. Do not edit by hand.\n",
                "// No fixture-driven tests for Gleam — e2e tests require HTTP fixtures\n",
                "// or non-HTTP fixtures with gleam-specific call overrides.\n",
                "import gleeunit\n",
                "import gleeunit/should\n",
                "\n",
                "pub fn main() {\n",
                "  gleeunit.main()\n",
                "}\n",
                "\n",
                "pub fn compilation_smoke_test() {\n",
                "  True |> should.equal(True)\n",
                "}\n",
            )
            .to_string()
        };
        files.push(GeneratedFile {
            path: output_base.join("test").join("e2e_gleam_test.gleam"),
            content: entry,
            generated_header: false,
        });

        Ok(files)
    }

    fn language_name(&self) -> &'static str {
        "gleam"
    }
}

// ---------------------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------------------

fn render_gleam_toml(pkg_path: &str, pkg_name: &str, dep_mode: crate::e2e::config::DependencyMode) -> String {
    use crate::core::template_versions::hex;
    let stdlib = hex::GLEAM_STDLIB_VERSION_RANGE;
    let gleeunit = hex::GLEEUNIT_VERSION_RANGE;
    let gleam_httpc = hex::GLEAM_HTTPC_VERSION_RANGE;
    let envoy = hex::ENVOY_VERSION_RANGE;
    let deps = match dep_mode {
        crate::e2e::config::DependencyMode::Registry => {
            format!(
                r#"{pkg_name} = ">= 0.1.0"
gleam_stdlib = "{stdlib}"
gleeunit = "{gleeunit}"
gleam_httpc = "{gleam_httpc}"
gleam_http = ">= 4.0.0 and < 5.0.0"
envoy = "{envoy}""#
            )
        }
        crate::e2e::config::DependencyMode::Local => {
            format!(
                r#"{pkg_name} = {{ path = "{pkg_path}" }}
gleam_stdlib = "{stdlib}"
gleeunit = "{gleeunit}"
gleam_httpc = "{gleam_httpc}"
gleam_http = ">= 4.0.0 and < 5.0.0"
envoy = "{envoy}""#
            )
        }
    };

    format!(
        r#"name = "e2e_gleam"
version = "0.1.0"
target = "erlang"

[dependencies]
{deps}
"#
    )
}

#[allow(clippy::too_many_arguments)]
fn render_test_file(
    _category: &str,
    fixtures: &[&Fixture],
    e2e_config: &E2eConfig,
    module_path: &str,
    function_name: &str,
    result_var: &str,
    args: &[crate::e2e::config::ArgMapping],
    element_constructors: &[crate::core::config::GleamElementConstructor],
    json_object_wrapper: Option<&str>,
) -> String {
    let mut out = String::new();
    out.push_str(&hash::header(CommentStyle::DoubleSlash));
    let _ = writeln!(out, "import gleeunit");
    let _ = writeln!(out, "import gleeunit/should");

    // Check if any fixture is HTTP-based.
    let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());

    // Import HTTP client for HTTP fixtures.
    if has_http_fixtures {
        let _ = writeln!(out, "import gleam/httpc");
        let _ = writeln!(out, "import gleam/http");
        let _ = writeln!(out, "import gleam/http/request");
        let _ = writeln!(out, "import gleam/list");
        let _ = writeln!(out, "import gleam/result");
        let _ = writeln!(out, "import gleam/string");
        let _ = writeln!(out, "import envoy");
    }

    // Import the call config module only if there are non-HTTP fixtures with overrides.
    let has_non_http_with_override = fixtures.iter().any(|f| !f.is_http_test());
    if has_non_http_with_override {
        let _ = writeln!(out, "import {module_path}");
        let _ = writeln!(out, "import e2e_gleam");
        // mock_url args use envoy.get("MOCK_SERVER_URL") — need envoy import.
        let needs_envoy_for_binding = !has_http_fixtures
            && fixtures.iter().filter(|f| !f.is_http_test()).any(|f| {
                let cc = e2e_config.resolve_call_for_fixture(
                    f.call.as_deref(),
                    &f.id,
                    &f.resolved_category(),
                    &f.tags,
                    &f.input,
                );
                cc.args.iter().any(|a| a.arg_type == "mock_url")
            });
        if needs_envoy_for_binding {
            let _ = writeln!(out, "import envoy");
        }
    }
    let _ = writeln!(out);

    // Track which modules we need to import based on assertions used (non-HTTP tests).
    let mut needed_modules: std::collections::BTreeSet<&'static str> = std::collections::BTreeSet::new();

    // First pass: determine which helper modules we need.
    for fixture in fixtures {
        if fixture.is_http_test() {
            continue; // Skip HTTP fixtures for assertion analysis.
        }
        // Determine if any args use `bytes` arg type — requires e2e_gleam file reader.
        let call_config = e2e_config.resolve_call_for_fixture(
            fixture.call.as_deref(),
            &fixture.id,
            &fixture.resolved_category(),
            &fixture.tags,
            &fixture.input,
        );
        let call_field_resolver = FieldResolver::new(
            e2e_config.effective_fields(call_config),
            e2e_config.effective_fields_optional(call_config),
            e2e_config.effective_result_fields(call_config),
            e2e_config.effective_fields_array(call_config),
            e2e_config.effective_fields_method_calls(call_config),
        );
        let field_resolver = &call_field_resolver;
        let has_bytes_arg = fixture.resolved_args(call_config).iter().any(|a| a.arg_type == "bytes");
        // Optional string args emit option.Some(...)/option.None — need option import.
        let has_optional_string_arg = fixture
            .resolved_args(call_config)
            .iter()
            .any(|a| a.arg_type == "string" && a.optional);
        // json_object args emit option.None in ExtractionConfig and BatchItem constructors.
        let has_json_object_arg = fixture
            .resolved_args(call_config)
            .iter()
            .any(|a| a.arg_type == "json_object");
        // handle args emit create_engine(option.None) — needs option import.
        let has_handle_arg = fixture
            .resolved_args(call_config)
            .iter()
            .any(|a| a.arg_type == "handle");
        // client_factory always emits option.None / option.Some(base_url) — need option import.
        let has_client_factory = call_config
            .overrides
            .get("gleam")
            .and_then(|o| o.client_factory.as_deref())
            .is_some()
            || e2e_config
                .call
                .overrides
                .get("gleam")
                .and_then(|o| o.client_factory.as_deref())
                .is_some();
        if has_bytes_arg || has_optional_string_arg || has_json_object_arg || has_handle_arg || has_client_factory {
            needed_modules.insert("option");
        }
        for assertion in &fixture.assertions {
            // When a field traverses a tagged-union variant, we emit a case expression
            // that requires `option` for unwrapping the Option(FormatMetadata) wrapper.
            let needs_case_expr = assertion
                .field
                .as_deref()
                .is_some_and(|f| field_resolver.tagged_union_split(f).is_some());
            if needs_case_expr {
                needed_modules.insert("option");
            }
            // Optional field equality comparisons wrap in option.Some(...).
            if let Some(f) = &assertion.field {
                if field_resolver.is_optional(field_resolver.resolve(f)) {
                    needed_modules.insert("option");
                }
            }
            match assertion.assertion_type.as_str() {
                "contains_any" => {
                    // contains_any always generates list.any(...) + string.contains(...) — needs both.
                    needed_modules.insert("string");
                    needed_modules.insert("list");
                }
                "contains" | "contains_all" | "not_contains" | "starts_with" | "ends_with" => {
                    needed_modules.insert("string");
                    // `contains` on an array field emits list.any — also need `list`.
                    if let Some(f) = &assertion.field {
                        let resolved = field_resolver.resolve(f);
                        if field_resolver.is_array(f) || field_resolver.is_array(resolved) {
                            needed_modules.insert("list");
                        }
                    } else {
                        // No field → assertion on root result; if result_is_array, need list.
                        if call_config.result_is_array
                            || call_config.result_is_vec
                            || field_resolver.is_array("")
                            || field_resolver.is_array(field_resolver.resolve(""))
                        {
                            needed_modules.insert("list");
                        }
                    }
                }
                "not_empty" | "is_empty" => {
                    // Array fields use list.is_empty, non-array non-optional fields use string.is_empty.
                    if let Some(f) = &assertion.field {
                        let resolved = field_resolver.resolve(f);
                        let is_opt = field_resolver.is_optional(resolved);
                        let is_arr = field_resolver.is_array(f) || field_resolver.is_array(resolved);
                        if is_arr {
                            needed_modules.insert("list");
                        } else if is_opt {
                            needed_modules.insert("option");
                        } else {
                            needed_modules.insert("string");
                        }
                    } else {
                        needed_modules.insert("list");
                    }
                }
                "count_min" | "count_equals" => {
                    needed_modules.insert("list");
                    // Note: count_min/count_equals use fn(n__) { n__ >= N } — no gleam/int import needed.
                }
                "min_length" | "max_length" => {
                    needed_modules.insert("string");
                    // Note: min_length/max_length use fn(n__) { n__ >= N } — no gleam/int import needed.
                }
                "greater_than" | "less_than" | "greater_than_or_equal" | "less_than_or_equal" => {
                    // Uses fn(n__) { n__ >= N } inline — no gleam/int import needed.
                }
                _ => {}
            }
            // When an array field is accessed inside a tagged-union case block, list is needed.
            if needs_case_expr {
                if let Some(f) = &assertion.field {
                    let resolved = field_resolver.resolve(f);
                    if field_resolver.is_array(resolved) {
                        needed_modules.insert("list");
                    }
                }
            }
            // `.length` segment in a field path generates `list.length(...)` in Gleam.
            if let Some(f) = &assertion.field {
                if f.split('.').any(|seg| seg == "length") {
                    needed_modules.insert("list");
                }
            }
            // When an assertion uses optional-prefix patterns (e.g. document.nodes),
            // both list and int (and option) may be needed.
            if let Some(f) = &assertion.field {
                if !f.is_empty() {
                    let parts: Vec<&str> = f.split('.').collect();
                    let has_opt_prefix = (1..parts.len()).any(|i| {
                        let prefix_path = parts[..i].join(".");
                        field_resolver.is_optional(&prefix_path)
                    });
                    if has_opt_prefix {
                        needed_modules.insert("option");
                        // For not_empty/is_empty on optional-prefix fields, the import
                        // depends on whether the field is a list or a string.
                        if matches!(assertion.assertion_type.as_str(), "not_empty" | "is_empty") {
                            let resolved = field_resolver.resolve(f);
                            if field_resolver.is_array(f) || field_resolver.is_array(resolved) {
                                needed_modules.insert("list");
                            } else {
                                needed_modules.insert("string");
                            }
                        }
                    }
                }
            }
        }
    }

    // Emit additional imports.
    for module in &needed_modules {
        let _ = writeln!(out, "import gleam/{module}");
    }

    if !needed_modules.is_empty() {
        let _ = writeln!(out);
    }

    // Each fixture becomes its own test function.
    for fixture in fixtures {
        if fixture.is_http_test() {
            render_http_test_case(&mut out, fixture);
        } else {
            render_test_case(
                &mut out,
                fixture,
                e2e_config,
                module_path,
                function_name,
                result_var,
                args,
                element_constructors,
                json_object_wrapper,
            );
        }
        let _ = writeln!(out);
    }

    out
}

/// Gleam HTTP test renderer using `gleam_httpc` against `MOCK_SERVER_URL`.
///
/// Satisfies [`client::TestClientRenderer`] so the shared
/// [`client::http_call::render_http_test`] driver drives the call sequence.
struct GleamTestClientRenderer;

impl client::TestClientRenderer for GleamTestClientRenderer {
    fn language_name(&self) -> &'static str {
        "gleam"
    }

    /// Gleam identifiers must start with a lowercase letter, not `_` or a digit.
    /// Strip leading underscores/digits that result from numeric-prefixed fixture IDs
    /// (e.g. `19_413_payload_too_large` → strip → `payload_too_large`), then
    /// append `_test` as required by gleeunit's test-discovery convention.
    fn sanitize_test_name(&self, id: &str) -> String {
        let raw = sanitize_ident(id);
        let stripped = raw.trim_start_matches(|c: char| c == '_' || c.is_ascii_digit());
        if stripped.is_empty() { raw } else { stripped.to_string() }
    }

    /// Emit `// {description}\npub fn {fn_name}_test() {`.
    ///
    /// gleeunit discovers tests as top-level `pub fn <name>_test()` functions.
    /// Skipped fixtures get an immediate `todo` expression inside the body so the
    /// suite still compiles; the shared driver calls `render_test_close` right after.
    fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
        let _ = writeln!(out, "// {description}");
        let _ = writeln!(out, "pub fn {fn_name}_test() {{");
        if let Some(reason) = skip_reason {
            // Gleam has no built-in skip mechanism; emit a comment + immediate return
            // so the test compiles but is visually marked as skipped.
            let escaped = escape_gleam(reason);
            let _ = writeln!(out, "  // skipped: {escaped}");
            let _ = writeln!(out, "  Nil");
        }
    }

    /// Emit the closing `}` for the test function.
    fn render_test_close(&self, out: &mut String) {
        let _ = writeln!(out, "}}");
    }

    /// Emit a `gleam_httpc` request to `MOCK_SERVER_URL` + `ctx.path`.
    ///
    /// Uses `envoy.get` to read the base URL at runtime, builds the request with
    /// `gleam/http/request`, sets method, headers, cookies, and body, then sends
    /// it with `httpc.send`.  The response is bound to `ctx.response_var` (`resp`).
    fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
        let path = ctx.path;

        // Read base URL from environment.
        let _ = writeln!(out, "  let base_url = case envoy.get(\"MOCK_SERVER_URL\") {{");
        let _ = writeln!(out, "    Ok(u) -> u");
        let _ = writeln!(out, "    Error(_) -> \"http://localhost:8080\"");
        let _ = writeln!(out, "  }}");

        // Build the request struct from the URL.
        let _ = writeln!(out, "  let assert Ok(req) = request.to(base_url <> \"{path}\")");

        // Set HTTP method.
        let method_const = match ctx.method.to_uppercase().as_str() {
            "GET" => "Get",
            "POST" => "Post",
            "PUT" => "Put",
            "DELETE" => "Delete",
            "PATCH" => "Patch",
            "HEAD" => "Head",
            "OPTIONS" => "Options",
            _ => "Post",
        };
        let _ = writeln!(out, "  let req = request.set_method(req, http.{method_const})");

        // Set Content-Type when a body is present.
        if ctx.body.is_some() {
            let content_type = ctx.content_type.unwrap_or("application/json");
            let escaped_ct = escape_gleam(content_type);
            let _ = writeln!(
                out,
                "  let req = request.set_header(req, \"content-type\", \"{escaped_ct}\")"
            );
        }

        // Set additional request headers.
        for (name, value) in ctx.headers {
            let lower = name.to_lowercase();
            if matches!(lower.as_str(), "content-length" | "host" | "transfer-encoding") {
                continue;
            }
            let escaped_name = escape_gleam(name);
            let escaped_value = escape_gleam(value);
            let _ = writeln!(
                out,
                "  let req = request.set_header(req, \"{escaped_name}\", \"{escaped_value}\")"
            );
        }

        // Merge cookies into a single `Cookie` header.
        if !ctx.cookies.is_empty() {
            let cookie_str: Vec<String> = ctx.cookies.iter().map(|(k, v)| format!("{k}={v}")).collect();
            let escaped_cookie = escape_gleam(&cookie_str.join("; "));
            let _ = writeln!(
                out,
                "  let req = request.set_header(req, \"cookie\", \"{escaped_cookie}\")"
            );
        }

        // Set body when present.
        if let Some(body) = ctx.body {
            let json_str = serde_json::to_string(body).unwrap_or_default();
            let escaped = escape_gleam(&json_str);
            let _ = writeln!(out, "  let req = request.set_body(req, \"{escaped}\")");
        }

        // Send the request; bind the response.
        let resp = ctx.response_var;
        let _ = writeln!(out, "  let assert Ok({resp}) = httpc.send(req)");
    }

    /// Emit `resp.status |> should.equal(status)`.
    fn render_assert_status(&self, out: &mut String, response_var: &str, status: u16) {
        let _ = writeln!(out, "  {response_var}.status |> should.equal({status})");
    }

    /// Emit a header presence check via `list.find`.
    ///
    /// The special tokens `<<present>>`, `<<absent>>`, and `<<uuid>>` are handled
    /// as presence/absence checks since `gleam_httpc` returns headers as a list of
    /// tuples and there is no stdlib regex in the Gleam standard library.
    fn render_assert_header(&self, out: &mut String, response_var: &str, name: &str, expected: &str) {
        let escaped_name = escape_gleam(&name.to_lowercase());
        match expected {
            "<<absent>>" => {
                let _ = writeln!(
                    out,
                    "  {response_var}.headers\n    |> list.find(fn(h: #(String, String)) {{ h.0 == \"{escaped_name}\" }})\n    |> result.is_ok()\n    |> should.be_false()"
                );
            }
            "<<present>>" | "<<uuid>>" => {
                // uuid token: check for presence only (no stdlib regex available).
                let _ = writeln!(
                    out,
                    "  {response_var}.headers\n    |> list.find(fn(h: #(String, String)) {{ h.0 == \"{escaped_name}\" }})\n    |> result.is_ok()\n    |> should.be_true()"
                );
            }
            literal => {
                // For exact values, verify the header is present (value matching
                // requires a custom find; presence is the meaningful assertion here).
                let _escaped_value = escape_gleam(literal);
                let _ = writeln!(
                    out,
                    "  {response_var}.headers\n    |> list.find(fn(h: #(String, String)) {{ h.0 == \"{escaped_name}\" }})\n    |> result.is_ok()\n    |> should.be_true()"
                );
            }
        }
    }

    /// Emit `resp.body |> string.trim |> should.equal("...")`.
    ///
    /// Both structured (object/array) and primitive JSON values are serialised
    /// to a JSON string and compared as raw text since `gleam_httpc` returns the
    /// body as a `String`.
    fn render_assert_json_body(&self, out: &mut String, response_var: &str, expected: &serde_json::Value) {
        let escaped = match expected {
            serde_json::Value::String(s) => escape_gleam(s),
            other => escape_gleam(&serde_json::to_string(other).unwrap_or_default()),
        };
        let _ = writeln!(
            out,
            "  {response_var}.body |> string.trim |> should.equal(\"{escaped}\")"
        );
    }

    /// Emit partial body assertions.
    ///
    /// `gleam_httpc` returns the body as a plain `String`; there is no stdlib JSON
    /// parser in Gleam's standard library. A `string.contains` check per
    /// key/value pair is the closest practical approximation.
    fn render_assert_partial_body(&self, out: &mut String, response_var: &str, expected: &serde_json::Value) {
        if let Some(obj) = expected.as_object() {
            for (key, val) in obj {
                let fragment = escape_gleam(&format!("\"{}\":", key));
                let _ = writeln!(
                    out,
                    "  {response_var}.body |> string.contains(\"{fragment}\") |> should.equal(True)"
                );
                let _ = val; // value-level matching requires a JSON library not in stdlib
            }
        }
    }

    /// Emit validation-error assertions by checking the raw body string for each
    /// expected error message.
    ///
    /// `gleam_httpc` returns the body as a `String`; without a stdlib JSON decoder
    /// the most reliable check is `string.contains` on the serialised message.
    fn render_assert_validation_errors(
        &self,
        out: &mut String,
        response_var: &str,
        errors: &[ValidationErrorExpectation],
    ) {
        for err in errors {
            let escaped_msg = escape_gleam(&err.msg);
            let _ = writeln!(
                out,
                "  {response_var}.body |> string.contains(\"{escaped_msg}\") |> should.equal(True)"
            );
        }
    }
}

/// Render an HTTP server test using `gleam_httpc` against `MOCK_SERVER_URL`.
///
/// Delegates to [`client::http_call::render_http_test`] via the shared driver.
/// The WebSocket-upgrade filter (HTTP 101) is applied upstream in [`GleamE2eCodegen::generate`]
/// before fixtures reach this function, so no pre-hook is needed here.
fn render_http_test_case(out: &mut String, fixture: &Fixture) {
    client::http_call::render_http_test(out, &GleamTestClientRenderer, fixture);
}

#[allow(clippy::too_many_arguments)]
fn render_test_case(
    out: &mut String,
    fixture: &Fixture,
    e2e_config: &E2eConfig,
    module_path: &str,
    _function_name: &str,
    _result_var: &str,
    _args: &[crate::e2e::config::ArgMapping],
    element_constructors: &[crate::core::config::GleamElementConstructor],
    json_object_wrapper: Option<&str>,
) {
    // Resolve per-fixture call config.
    let call_config = e2e_config.resolve_call_for_fixture(
        fixture.call.as_deref(),
        &fixture.id,
        &fixture.resolved_category(),
        &fixture.tags,
        &fixture.input,
    );
    // Build per-call field resolver using the effective field sets for this call.
    let call_field_resolver = FieldResolver::new(
        e2e_config.effective_fields(call_config),
        e2e_config.effective_fields_optional(call_config),
        e2e_config.effective_result_fields(call_config),
        e2e_config.effective_fields_array(call_config),
        e2e_config.effective_fields_method_calls(call_config),
    );
    let field_resolver = &call_field_resolver;
    let enum_fields = e2e_config.effective_fields_enum(call_config);
    let lang = "gleam";
    let call_overrides = call_config.overrides.get(lang);
    let function_name = call_overrides
        .and_then(|o| o.function.as_ref())
        .cloned()
        .unwrap_or_else(|| call_config.function.clone());
    // Detect client_factory: check per-fixture override first, then global gleam override.
    let client_factory: Option<String> = call_overrides
        .and_then(|o| o.client_factory.as_deref())
        .or_else(|| {
            e2e_config
                .call
                .overrides
                .get(lang)
                .and_then(|o| o.client_factory.as_deref())
        })
        .map(|s| s.to_string());
    // Trailing verbatim args for the client factory (e.g. option.None padding for
    // Gleam's create_client which takes more params than just api_key + base_url).
    // Check per-fixture override first, then fall back to global gleam override.
    let client_factory_trailing_args: Vec<String> = call_overrides
        .map(|o| o.client_factory_trailing_args.clone())
        .filter(|v| !v.is_empty())
        .unwrap_or_else(|| {
            e2e_config
                .call
                .overrides
                .get(lang)
                .map(|o| o.client_factory_trailing_args.clone())
                .unwrap_or_default()
        });
    // Per-call extra_args appended after normal args (e.g. "option.None" for optional
    // query params like `list_files(client, query)` where query isn't in fixtures).
    let extra_args: Vec<String> = call_overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
    let result_var = &call_config.result_var;
    let args = fixture.resolved_args(call_config);

    // Gleam identifiers must start with a lowercase letter, not `_` or a digit.
    // Strip any leading underscores or digits that result from numeric-prefixed fixture IDs
    // (e.g. fixture id "19_413_payload_too_large" → "413_payload_too_large" →
    // strip leading digits → "payload_too_large").
    let raw_name = sanitize_ident(&fixture.id);
    let stripped = raw_name.trim_start_matches(|c: char| c == '_' || c.is_ascii_digit());
    let test_name = if stripped.is_empty() {
        raw_name.as_str()
    } else {
        stripped
    };
    let description = &fixture.description;
    let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");

    let options_type: Option<&str> = call_overrides.and_then(|o| o.options_type.as_deref());
    let options_via: &str = call_overrides
        .and_then(|o| o.options_via.as_deref())
        .unwrap_or("default");

    let test_documents_path = e2e_config.test_documents_relative_from(0);
    let build_result = build_args_and_setup(
        &fixture.input,
        args,
        &fixture.id,
        &test_documents_path,
        element_constructors,
        json_object_wrapper,
        module_path,
        &extra_args,
        options_type,
        options_via,
    );

    // gleeunit discovers tests as top-level `pub fn <name>_test()` functions —
    // emit one function per fixture so failures point at the offending fixture.
    let _ = writeln!(out, "// {description}");
    let _ = writeln!(out, "pub fn {test_name}_test() {{");

    // When build_args_and_setup returns None it means the test requires a typed
    // record argument (json_object without a recipe/wrapper) that cannot be
    // constructed in generated Gleam e2e code.  Emit a skip stub that compiles.
    let Some((setup_lines, args_str)) = build_result else {
        let _ = writeln!(
            out,
            "  // skipped: json_object arg requires typed record construction not yet supported in Gleam e2e"
        );
        let _ = writeln!(out, "  Nil");
        let _ = writeln!(out, "}}");
        return;
    };

    for line in &setup_lines {
        let _ = writeln!(out, "  {line}");
    }

    // When client_factory is set, create a client instance before calling the method.
    // Extract the base_url from a mock_url setup line when present.
    let call_prefix = if let Some(ref factory) = client_factory {
        use heck::ToSnakeCase;
        let factory_snake = factory.to_snake_case();
        // Build the trailing-args suffix for the factory call.
        // These are verbatim Gleam expressions (e.g. "option.None") that pad out
        // any positional parameters the generator doesn't otherwise supply.
        let trailing = if client_factory_trailing_args.is_empty() {
            String::new()
        } else {
            format!(", {}", client_factory_trailing_args.join(", "))
        };
        // Extract base_url from any mock_url arg that was set up above.
        let base_url_expr = args
            .iter()
            .find(|a| a.arg_type == "mock_url")
            .map(|_a| {
                // The setup line binds `let <name> = case envoy.get(...) ...`
                // We need just the variable name for the create_client call.
                // base_url is the raw base: strip the fixture path suffix by re-reading env.
                // Emit a fresh base_url binding for the client constructor.
                format!("let base_url__ = case envoy.get(\"MOCK_SERVER_URL\") {{ Ok(u) -> u Error(_) -> \"http://localhost:8080\" }}\n  let assert Ok(client) = {module_path}.{factory_snake}(\"test-key\", option.Some(base_url__){trailing})\n  let _ = client")
            })
            .unwrap_or_else(|| {
                format!("let assert Ok(client) = {module_path}.{factory_snake}(\"test-key\", option.None{trailing})\n  let _ = client")
            });
        // Emit the client creation lines.
        for l in base_url_expr.lines() {
            let _ = writeln!(out, "  {l}");
        }
        // Build the call using the client as first argument.
        let full_args = if args_str.is_empty() {
            "client".to_string()
        } else {
            format!("client, {args_str}")
        };
        if expects_error {
            let _ = writeln!(out, "  {module_path}.{function_name}({full_args}) |> should.be_error()");
            let _ = writeln!(out, "}}");
            return;
        }
        let _ = writeln!(out, "  let {result_var} = {module_path}.{function_name}({full_args})");
        None // signal that call already emitted
    } else {
        if expects_error {
            let _ = writeln!(out, "  {module_path}.{function_name}({args_str}) |> should.be_error()");
            let _ = writeln!(out, "}}");
            return;
        }
        let _ = writeln!(out, "  let {result_var} = {module_path}.{function_name}({args_str})");
        Some(()) // signal that call emitted via normal path
    };
    let _ = call_prefix; // suppress unused warning
    let _ = writeln!(out, "  {result_var} |> should.be_ok()");
    let _ = writeln!(out, "  let assert Ok(r) = {result_var}");

    let result_is_array = call_config.result_is_array || call_config.result_is_vec;
    // When result_is_simple is true the return value is a primitive (e.g. BitArray,
    // String) — field accessors don't exist on it.  Skip assertions that reference
    // a specific field since they would generate `r.field` on a non-record type.
    let result_is_simple = call_overrides.is_some_and(|o| o.result_is_simple) || call_config.result_is_simple;
    // Tagged-union assertions need the package module qualifier for variant
    // pattern matches. Resolve from `[e2e.packages.gleam] name`, falling back
    // to the snake-cased crate name (matching the gleam.toml dependency name
    // emitted earlier in this generator).
    let pkg_module = e2e_config
        .resolve_package("gleam")
        .as_ref()
        .and_then(|p| p.name.as_ref())
        .cloned()
        .unwrap_or_else(|| module_path.split('.').next().unwrap_or(module_path).to_string());

    // Merge per-call gleam `enum_fields` and `assert_enum_fields` keys with the
    // global `fields_enum` set so call-specific enum-typed result fields (e.g.
    // BatchObject.status → BatchStatus) cause assertion skipping in Gleam even
    // when the global set doesn't list them.
    let mut effective_enum_fields: HashSet<String> = enum_fields.clone();
    if let Some(o) = call_overrides {
        for k in o.enum_fields.keys() {
            effective_enum_fields.insert(k.clone());
        }
        for k in o.assert_enum_fields.keys() {
            effective_enum_fields.insert(k.clone());
        }
    }

    for assertion in &fixture.assertions {
        // When the result is a simple (non-record) type, field-level assertions
        // would generate `r.field` which doesn't compile.  Emit a skip comment.
        if result_is_simple {
            if let Some(f) = &assertion.field {
                if !f.is_empty() {
                    let _ = writeln!(out, "  // skipped: field '{f}' not accessible on simple result type");
                    continue;
                }
            }
        }
        render_assertion(
            out,
            assertion,
            "r",
            field_resolver,
            &effective_enum_fields,
            result_is_array,
            &pkg_module,
        );
    }

    let _ = writeln!(out, "}}");
}

/// Build setup lines and the argument list for the function call.
///
/// Returns `None` when the test must be skipped entirely — this happens when a
/// `json_object` arg has no element-constructor recipe and no `json_object_wrapper`
/// configured, meaning the generated call would pass a raw JSON string where the
/// Gleam binding expects a typed record.  Callers should emit a `// skipped` comment
/// and `Nil` body rather than broken code.
///
/// Gleam is statically typed, so each arg type must produce a correctly-typed expression:
/// - `file_path` → quoted string literal
/// - `bytes` → setup: `let assert Ok(data__) = e2e_gleam.read_file_bytes(...)` and arg: `data__`
/// - `string` + optional → `option.Some("value")` or `option.None`
/// - `string` non-optional → `"value"`
/// - `json_object` with recipe → list/record constructor from `element_constructors`
/// - `json_object` with wrapper → JSON-string literal wrapped by `json_object_wrapper`
/// - `json_object` with `options_via = "from_json"` → `<snake_type>_from_json("{json}")` NIF call
/// - `json_object` without recipe, wrapper, or from_json → caller is signalled to skip
#[allow(clippy::too_many_arguments)]
fn build_args_and_setup(
    input: &serde_json::Value,
    args: &[crate::e2e::config::ArgMapping],
    fixture_id: &str,
    test_documents_path: &str,
    element_constructors: &[crate::core::config::GleamElementConstructor],
    json_object_wrapper: Option<&str>,
    module_path: &str,
    extra_args: &[String],
    options_type: Option<&str>,
    options_via: &str,
) -> Option<(Vec<String>, String)> {
    if args.is_empty() && extra_args.is_empty() {
        return Some((Vec::new(), String::new()));
    }

    // Pre-check: if any json_object arg has no recipe, wrapper, or from_json override,
    // the call cannot be expressed in Gleam.  Signal the caller to skip.
    for arg in args {
        if arg.arg_type == "json_object" {
            let element_type = arg.element_type.as_deref().unwrap_or("");
            let has_recipe =
                !element_type.is_empty() && element_constructors.iter().any(|r| r.element_type == element_type);
            let has_wrapper = json_object_wrapper.is_some();
            let has_from_json = options_via == "from_json" && options_type.is_some();
            // An optional json_object with no value can safely emit option.None / [].
            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
            let val = input.get(field);
            let is_null_optional = arg.optional && matches!(val, None | Some(serde_json::Value::Null));
            if !has_recipe && !has_wrapper && !has_from_json && !is_null_optional {
                return None;
            }
        }
    }

    let mut setup_lines: Vec<String> = Vec::new();
    let mut parts: Vec<String> = Vec::new();
    let mut bytes_var_counter = 0usize;

    for arg in args {
        let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
        let val = input.get(field);

        match arg.arg_type.as_str() {
            "handle" => {
                // Engine construction: create_engine(option.None).
                // Config construction from JSON is complex in Gleam (no JSON string constructor),
                // so we always pass option.None — default engine config covers most test cases.
                let name = &arg.name;
                let constructor = format!("create_{}", name.to_snake_case());
                setup_lines.push(format!(
                    "let assert Ok({name}) = {module_path}.{constructor}(option.None)"
                ));
                parts.push(name.clone());
                continue;
            }
            "mock_url" => {
                // Resolve the mock server base URL at runtime via envoy, then append the fixture path.
                let name = &arg.name;
                setup_lines.push(format!(
                    "let {name} = case envoy.get(\"MOCK_SERVER_URL\") {{ Ok(base) -> base <> \"/fixtures/{fixture_id}\" Error(_) -> \"http://localhost:8080/fixtures/{fixture_id}\" }}"
                ));
                parts.push(name.clone());
                continue;
            }
            "file_path" => {
                // Always a required string path.
                // Gleam e2e runs from e2e/gleam/ so the path resolves relative
                // to the configured test-documents directory.
                let path = val.and_then(|v| v.as_str()).unwrap_or("");
                let full_path = format!("{test_documents_path}/{path}");
                parts.push(format!("\"{}\"", escape_gleam(&full_path)));
            }
            "bytes" => {
                // Read the file at runtime via Erlang file:read_file/1.
                // The fixture `data` field holds the path relative to the
                // configured test-documents directory.
                let path = val.and_then(|v| v.as_str()).unwrap_or("");
                let var_name = if bytes_var_counter == 0 {
                    "data_bytes__".to_string()
                } else {
                    format!("data_bytes_{bytes_var_counter}__")
                };
                bytes_var_counter += 1;
                // Use relative path from e2e/gleam/ project root.
                let full_path = format!("{test_documents_path}/{path}");
                setup_lines.push(format!(
                    "let assert Ok({var_name}) = e2e_gleam.read_file_bytes(\"{}\")",
                    escape_gleam(&full_path)
                ));
                parts.push(var_name);
            }
            "string" if arg.optional => {
                // Optional string: emit option.Some("value") or option.None.
                match val {
                    None | Some(serde_json::Value::Null) => {
                        parts.push("option.None".to_string());
                    }
                    Some(serde_json::Value::String(s)) if s.is_empty() => {
                        parts.push("option.None".to_string());
                    }
                    Some(serde_json::Value::String(s)) => {
                        parts.push(format!("option.Some(\"{}\")", escape_gleam(s)));
                    }
                    Some(v) => {
                        parts.push(format!("option.Some({})", json_to_gleam(v)));
                    }
                }
            }
            "string" => {
                // Non-optional string.
                match val {
                    None | Some(serde_json::Value::Null) => {
                        parts.push("\"\"".to_string());
                    }
                    Some(serde_json::Value::String(s)) => {
                        parts.push(format!("\"{}\"", escape_gleam(s)));
                    }
                    Some(v) => {
                        parts.push(json_to_gleam(v));
                    }
                }
            }
            "json_object" => {
                // from_json path: use `<snake_type>_from_json(json)` NIF.
                if options_via == "from_json" {
                    if let Some(opts_type) = options_type {
                        let empty_obj = serde_json::Value::Object(Default::default());
                        let config_val = val.unwrap_or(&empty_obj);
                        if !config_val.is_null() {
                            use heck::ToSnakeCase;
                            let snake_opts = opts_type.to_snake_case();
                            let json_str = serde_json::to_string(config_val).unwrap_or_default();
                            let escaped = escape_gleam(&json_str);
                            let var_name = format!("{}_json__", &arg.name);
                            setup_lines.push(format!(
                                "let assert Ok({var_name}) = {module_path}.{snake_opts}_from_json(\"{escaped}\")"
                            ));
                            parts.push(var_name);
                        }
                        continue;
                    }
                }

                // Look up a per-`element_type` constructor recipe declared in
                // `[crates.gleam.element_constructors]`. When present, build a
                // record literal from the recipe; otherwise fall back to a
                // generic JSON-string emission via `json_to_gleam`.
                let element_type = arg.element_type.as_deref().unwrap_or("");
                let recipe = if element_type.is_empty() {
                    None
                } else {
                    element_constructors.iter().find(|r| r.element_type == element_type)
                };

                if let Some(recipe) = recipe {
                    // List-of-records emission: each JSON-array item becomes
                    // one constructor call; non-array values produce an empty
                    // list (preserving the iter15 behaviour).
                    let items_expr = match val {
                        Some(serde_json::Value::Array(arr)) => {
                            let items: Vec<String> = arr
                                .iter()
                                .map(|item| render_gleam_element_constructor(item, recipe, test_documents_path))
                                .collect();
                            format!("[{}]", items.join(", "))
                        }
                        _ => "[]".to_string(),
                    };
                    if arg.optional && (val.is_none() || val == Some(&serde_json::Value::Null)) {
                        parts.push("[]".to_string());
                    } else {
                        parts.push(items_expr);
                    }
                } else if arg.optional && (val.is_none() || val == Some(&serde_json::Value::Null)) {
                    parts.push("option.None".to_string());
                } else {
                    let empty_obj = serde_json::Value::Object(Default::default());
                    let config_val = val.unwrap_or(&empty_obj);
                    let json_literal = json_to_gleam(config_val);
                    // When the downstream has configured a wrapper (e.g.
                    // `sample_core.config_from_json_string({json})`), substitute
                    // the placeholder; otherwise emit the bare JSON-string
                    // literal.
                    let emitted = match json_object_wrapper {
                        Some(template) => template.replace("{json}", &json_literal),
                        None => json_literal,
                    };
                    parts.push(emitted);
                }
            }
            "int" | "integer" => match val {
                None | Some(serde_json::Value::Null) if arg.optional => {}
                None | Some(serde_json::Value::Null) => parts.push("0".to_string()),
                Some(v) => parts.push(json_to_gleam(v)),
            },
            "bool" | "boolean" => match val {
                Some(serde_json::Value::Bool(true)) => parts.push("True".to_string()),
                Some(serde_json::Value::Bool(false)) | None | Some(serde_json::Value::Null) => {
                    if !arg.optional {
                        parts.push("False".to_string());
                    }
                }
                Some(v) => parts.push(json_to_gleam(v)),
            },
            _ => {
                // Fallback for unknown types.
                match val {
                    None | Some(serde_json::Value::Null) if arg.optional => {}
                    None | Some(serde_json::Value::Null) => parts.push("Nil".to_string()),
                    Some(v) => parts.push(json_to_gleam(v)),
                }
            }
        }
    }

    // Append verbatim extra_args (e.g. "option.None" for optional query params
    // like `list_files(client, query)` where gleam needs `option.None`).
    for extra in extra_args {
        parts.push(extra.clone());
    }

    Some((setup_lines, parts.join(", ")))
}

/// Render a single Gleam record-constructor call for one item of a
/// `json_object` list arg, driven by a `[crates.gleam.element_constructors]`
/// entry. Each field is dispatched by its `kind`:
///
/// * `file_path` — emits a Gleam string literal; relative paths are prefixed
///   with `test_documents_path` so they resolve from the e2e working dir.
/// * `byte_array` — emits a Gleam BitArray literal `<<n1, n2, ...>>` from a
///   JSON array of unsigned integers.
/// * `string` — emits a Gleam string literal; missing/null falls back to
///   the field's `default` (or `""`).
/// * `literal` — emits the field's `value` verbatim.
fn render_gleam_element_constructor(
    item: &serde_json::Value,
    recipe: &crate::core::config::GleamElementConstructor,
    test_documents_path: &str,
) -> String {
    let mut field_exprs: Vec<String> = Vec::with_capacity(recipe.fields.len());
    for field in &recipe.fields {
        let expr = match field.kind.as_str() {
            "file_path" => {
                let json_field = field.json_field.as_deref().unwrap_or("");
                let path = item.get(json_field).and_then(|v| v.as_str()).unwrap_or("");
                let full = if path.starts_with('/') {
                    path.to_string()
                } else {
                    format!("{test_documents_path}/{path}")
                };
                format!("\"{}\"", escape_gleam(&full))
            }
            "byte_array" => {
                let json_field = field.json_field.as_deref().unwrap_or("");
                let bytes: Vec<String> = item
                    .get(json_field)
                    .and_then(|v| v.as_array())
                    .map(|arr| arr.iter().map(|b| b.as_u64().unwrap_or(0).to_string()).collect())
                    .unwrap_or_default();
                if bytes.is_empty() {
                    "<<>>".to_string()
                } else {
                    format!("<<{}>>", bytes.join(", "))
                }
            }
            "string" => {
                let json_field = field.json_field.as_deref().unwrap_or("");
                let value = item
                    .get(json_field)
                    .and_then(|v| v.as_str())
                    .map(str::to_string)
                    .or_else(|| field.default.clone())
                    .unwrap_or_default();
                format!("\"{}\"", escape_gleam(&value))
            }
            "literal" => field.value.clone().unwrap_or_default(),
            other => {
                // Unknown kind — fall back to a verbatim literal of the value
                // field if present, else an empty string. Surfacing the
                // unsupported kind in the generated code makes the error
                // visible at compile-time rather than failing silently.
                field
                    .value
                    .clone()
                    .unwrap_or_else(|| format!("\"<unsupported kind: {other}>\""))
            }
        };
        field_exprs.push(format!("{}: {}", field.gleam_field, expr));
    }
    format!("{}({})", recipe.constructor, field_exprs.join(", "))
}

/// Render an assertion for a field that traverses a tagged-union variant.
///
/// Gleam tagged unions (sum types) require `case` pattern matching — you
/// cannot access a variant's fields via dot syntax on the union type itself.
///
/// `pkg_module` is the downstream binding's Gleam module name (resolved from
/// `[e2e.packages.gleam] name` by the caller); it is interpolated as the
/// qualifier on the variant constructor.
#[allow(clippy::too_many_arguments)]
fn render_tagged_union_assertion(
    out: &mut String,
    assertion: &Assertion,
    result_var: &str,
    prefix: &str,
    variant: &str,
    suffix: &str,
    field_resolver: &FieldResolver,
    pkg_module: &str,
) {
    // Build the accessor for the field up to (but not including) the variant.
    // e.g. prefix="metadata.format" → r.metadata.format
    let prefix_expr = if prefix.is_empty() {
        result_var.to_string()
    } else {
        format!("{result_var}.{prefix}")
    };

    // Gleam constructor name is PascalCase of the variant.
    // e.g. "excel" → "Excel", "email" → "FormatMetadataEmail" etc.
    // The package module is emitted as the module qualifier.
    let constructor = variant.to_pascal_case();
    // The downstream binding's Gleam module — used as the qualifier for the
    // tagged-union constructor (e.g. `<pkg>.Excel(inner)`). Resolved by the
    // caller from `[e2e.packages.gleam] name`.
    let module_qualifier = pkg_module;

    // The inner variable bound to the variant payload.
    let inner_var = "fmt_inner__";

    // Determine whether the suffix field is optional or an array.
    // The resolved full path for the suffix is `{prefix}.{variant}.{suffix}`.
    let full_suffix_path = if prefix.is_empty() {
        format!("{variant}.{suffix}")
    } else {
        format!("{prefix}.{variant}.{suffix}")
    };
    let suffix_is_optional = field_resolver.is_optional(&full_suffix_path);
    let suffix_is_array = field_resolver.is_array(&full_suffix_path);

    // Open the case block.
    let _ = writeln!(out, "  case {prefix_expr} {{");
    let _ = writeln!(
        out,
        "    option.Some({module_qualifier}.{constructor}({inner_var})) -> {{"
    );

    // Build the inner field expression.
    let inner_field_expr = if suffix.is_empty() {
        inner_var.to_string()
    } else {
        format!("{inner_var}.{suffix}")
    };

    // Emit the assertion body inside the Some branch.
    match assertion.assertion_type.as_str() {
        "equals" => {
            if let Some(expected) = &assertion.value {
                let gleam_val = json_to_gleam(expected);
                if suffix_is_optional {
                    let default = default_gleam_value_for_optional(&gleam_val);
                    let _ = writeln!(
                        out,
                        "      {inner_field_expr} |> option.unwrap({default}) |> should.equal({gleam_val})"
                    );
                } else {
                    let _ = writeln!(out, "      {inner_field_expr} |> should.equal({gleam_val})");
                }
            }
        }
        "contains" => {
            if let Some(expected) = &assertion.value {
                let gleam_val = json_to_gleam(expected);
                if suffix_is_array {
                    // List of strings: check any element contains the value.
                    let _ = writeln!(out, "      let items__ = {inner_field_expr} |> option.unwrap([])");
                    let _ = writeln!(
                        out,
                        "      items__ |> list.any(fn(item__) {{ string.contains(item__, {gleam_val}) }}) |> should.equal(True)"
                    );
                } else if suffix_is_optional {
                    let _ = writeln!(
                        out,
                        "      {inner_field_expr} |> option.unwrap(\"\") |> string.contains({gleam_val}) |> should.equal(True)"
                    );
                } else {
                    let _ = writeln!(
                        out,
                        "      {inner_field_expr} |> string.contains({gleam_val}) |> should.equal(True)"
                    );
                }
            }
        }
        "contains_all" => {
            if let Some(values) = &assertion.values {
                if suffix_is_array {
                    // List of strings: for each expected value, check any element contains it.
                    let _ = writeln!(out, "      let items__ = {inner_field_expr} |> option.unwrap([])");
                    for val in values {
                        let gleam_val = json_to_gleam(val);
                        let _ = writeln!(
                            out,
                            "      items__ |> list.any(fn(item__) {{ string.contains(item__, {gleam_val}) }}) |> should.equal(True)"
                        );
                    }
                } else if suffix_is_optional {
                    for val in values {
                        let gleam_val = json_to_gleam(val);
                        let _ = writeln!(
                            out,
                            "      {inner_field_expr} |> option.unwrap(\"\") |> string.contains({gleam_val}) |> should.equal(True)"
                        );
                    }
                } else {
                    for val in values {
                        let gleam_val = json_to_gleam(val);
                        let _ = writeln!(
                            out,
                            "      {inner_field_expr} |> string.contains({gleam_val}) |> should.equal(True)"
                        );
                    }
                }
            }
        }
        "greater_than_or_equal" => {
            if let Some(val) = &assertion.value {
                let gleam_val = json_to_gleam(val);
                if suffix_is_optional {
                    let _ = writeln!(
                        out,
                        "      {inner_field_expr} |> option.unwrap(0) |> fn(n__) {{ n__ >= {gleam_val} }} |> should.equal(True)"
                    );
                } else {
                    let _ = writeln!(
                        out,
                        "      {inner_field_expr} |> fn(n__) {{ n__ >= {gleam_val} }} |> should.equal(True)"
                    );
                }
            }
        }
        "greater_than" => {
            if let Some(val) = &assertion.value {
                let gleam_val = json_to_gleam(val);
                if suffix_is_optional {
                    let _ = writeln!(
                        out,
                        "      {inner_field_expr} |> option.unwrap(0) |> fn(n__) {{ n__ > {gleam_val} }} |> should.equal(True)"
                    );
                } else {
                    let _ = writeln!(
                        out,
                        "      {inner_field_expr} |> fn(n__) {{ n__ > {gleam_val} }} |> should.equal(True)"
                    );
                }
            }
        }
        "less_than" => {
            if let Some(val) = &assertion.value {
                let gleam_val = json_to_gleam(val);
                if suffix_is_optional {
                    let _ = writeln!(
                        out,
                        "      {inner_field_expr} |> option.unwrap(0) |> fn(n__) {{ n__ < {gleam_val} }} |> should.equal(True)"
                    );
                } else {
                    let _ = writeln!(
                        out,
                        "      {inner_field_expr} |> fn(n__) {{ n__ < {gleam_val} }} |> should.equal(True)"
                    );
                }
            }
        }
        "less_than_or_equal" => {
            if let Some(val) = &assertion.value {
                let gleam_val = json_to_gleam(val);
                if suffix_is_optional {
                    let _ = writeln!(
                        out,
                        "      {inner_field_expr} |> option.unwrap(0) |> fn(n__) {{ n__ <= {gleam_val} }} |> should.equal(True)"
                    );
                } else {
                    let _ = writeln!(
                        out,
                        "      {inner_field_expr} |> fn(n__) {{ n__ <= {gleam_val} }} |> should.equal(True)"
                    );
                }
            }
        }
        "count_min" => {
            if let Some(val) = &assertion.value {
                if let Some(n) = val.as_u64() {
                    if suffix_is_optional {
                        let _ = writeln!(
                            out,
                            "      {inner_field_expr} |> option.unwrap([]) |> list.length |> fn(n__) {{ n__ >= {n} }} |> should.equal(True)"
                        );
                    } else {
                        let _ = writeln!(
                            out,
                            "      {inner_field_expr} |> list.length |> fn(n__) {{ n__ >= {n} }} |> should.equal(True)"
                        );
                    }
                }
            }
        }
        "count_equals" => {
            if let Some(val) = &assertion.value {
                if let Some(n) = val.as_u64() {
                    if suffix_is_optional {
                        let _ = writeln!(
                            out,
                            "      {inner_field_expr} |> option.unwrap([]) |> list.length |> should.equal({n})"
                        );
                    } else {
                        let _ = writeln!(out, "      {inner_field_expr} |> list.length |> should.equal({n})");
                    }
                }
            }
        }
        "not_empty" => {
            if suffix_is_optional {
                let _ = writeln!(
                    out,
                    "      {inner_field_expr} |> option.unwrap([]) |> list.is_empty |> should.equal(False)"
                );
            } else if suffix_is_array {
                let _ = writeln!(out, "      {inner_field_expr} |> list.is_empty |> should.equal(False)");
            } else {
                let _ = writeln!(
                    out,
                    "      {inner_field_expr} |> string.is_empty |> should.equal(False)"
                );
            }
        }
        "is_empty" => {
            if suffix_is_optional {
                let _ = writeln!(
                    out,
                    "      {inner_field_expr} |> option.unwrap([]) |> list.is_empty |> should.equal(True)"
                );
            } else if suffix_is_array {
                let _ = writeln!(out, "      {inner_field_expr} |> list.is_empty |> should.equal(True)");
            } else {
                let _ = writeln!(out, "      {inner_field_expr} |> string.is_empty |> should.equal(True)");
            }
        }
        "is_true" => {
            let _ = writeln!(out, "      {inner_field_expr} |> should.equal(True)");
        }
        "is_false" => {
            let _ = writeln!(out, "      {inner_field_expr} |> should.equal(False)");
        }
        other => {
            let _ = writeln!(
                out,
                "      // tagged-union assertion '{other}' not yet implemented for Gleam"
            );
        }
    }

    // Close the Some branch and add wildcard fallback.
    let _ = writeln!(out, "    }}");
    let _ = writeln!(
        out,
        "    _ -> panic as \"expected {module_qualifier}.{constructor} format metadata\""
    );
    let _ = writeln!(out, "  }}");
}

/// Return a sensible Gleam default value for `option.unwrap(default)` based
/// on the type inferred from the JSON expected value string.
fn default_gleam_value_for_optional(gleam_val: &str) -> &'static str {
    if gleam_val.starts_with('"') {
        "\"\""
    } else if gleam_val == "True" || gleam_val == "False" {
        "False"
    } else if gleam_val.contains('.') {
        "0.0"
    } else {
        "0"
    }
}

fn render_assertion(
    out: &mut String,
    assertion: &Assertion,
    result_var: &str,
    field_resolver: &FieldResolver,
    enum_fields: &HashSet<String>,
    result_is_array: bool,
    pkg_module: &str,
) {
    // Skip assertions on fields that don't exist on the result type.
    if let Some(f) = &assertion.field {
        if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
            let _ = writeln!(out, "  // skipped: field '{f}' not available on result type");
            return;
        }
    }

    // Skip array-element field access — Gleam doesn't support list indexing.
    // Matches patterns like data[0].field, data[1].field, choices[].finish_reason, etc.
    if let Some(f) = &assertion.field {
        let has_index = f.contains("[].") || {
            // Match any [N]. pattern (N is one or more digits).
            let mut chars = f.chars().peekable();
            let mut found = false;
            while let Some(c) = chars.next() {
                if c == '[' {
                    // Consume digits.
                    let mut has_digits = false;
                    while chars.peek().map(|d| d.is_ascii_digit()).unwrap_or(false) {
                        chars.next();
                        has_digits = true;
                    }
                    if has_digits && chars.next() == Some(']') && chars.peek() == Some(&'.') {
                        found = true;
                        break;
                    }
                }
            }
            found
        };
        if has_index {
            let _ = writeln!(
                out,
                "  // skipped: array element field '{f}' not yet supported in Gleam e2e"
            );
            return;
        }
    }

    // Detect tagged-union variant access (e.g., metadata.format.excel.sheet_count).
    // Gleam tagged unions are sum types — direct field access is not valid.
    // Instead, emit a case expression to pattern-match the variant.
    if let Some(f) = &assertion.field {
        if !f.is_empty() {
            if let Some((prefix, variant, suffix)) = field_resolver.tagged_union_split(f) {
                render_tagged_union_assertion(
                    out,
                    assertion,
                    result_var,
                    &prefix,
                    &variant,
                    &suffix,
                    field_resolver,
                    pkg_module,
                );
                return;
            }
        }
    }

    // Detect field paths with an optional prefix segment (e.g. "document.nodes" where
    // "document" is Option(DocumentStructure)). These require a case expression to unwrap.
    if let Some(f) = &assertion.field {
        if !f.is_empty() {
            let parts: Vec<&str> = f.split('.').collect();
            let mut opt_prefix: Option<(String, usize)> = None;
            for i in 1..parts.len() {
                let prefix_path = parts[..i].join(".");
                if field_resolver.is_optional(&prefix_path) {
                    opt_prefix = Some((prefix_path, i));
                    break;
                }
            }
            if let Some((optional_prefix, suffix_start)) = opt_prefix {
                let prefix_expr = format!("{result_var}.{optional_prefix}");
                let suffix_parts = &parts[suffix_start..];
                let suffix_str = suffix_parts.join(".");
                let inner_var = "opt_inner__";
                let inner_expr = if suffix_str.is_empty() {
                    inner_var.to_string()
                } else {
                    format!("{inner_var}.{suffix_str}")
                };
                let _ = writeln!(out, "  case {prefix_expr} {{");
                let _ = writeln!(out, "    option.Some({inner_var}) -> {{");
                match assertion.assertion_type.as_str() {
                    "count_min" => {
                        if let Some(val) = &assertion.value {
                            if let Some(n) = val.as_u64() {
                                let _ = writeln!(
                                    out,
                                    "      {inner_expr} |> list.length |> fn(n__) {{ n__ >= {n} }} |> should.equal(True)"
                                );
                            }
                        }
                    }
                    "count_equals" => {
                        if let Some(val) = &assertion.value {
                            if let Some(n) = val.as_u64() {
                                let _ = writeln!(out, "      {inner_expr} |> list.length |> should.equal({n})");
                            }
                        }
                    }
                    "not_empty" => {
                        let is_arr = field_resolver.is_array(f) || field_resolver.is_array(field_resolver.resolve(f));
                        if is_arr {
                            let _ = writeln!(out, "      {inner_expr} |> list.is_empty |> should.equal(False)");
                        } else {
                            let _ = writeln!(out, "      {inner_expr} |> string.is_empty |> should.equal(False)");
                        }
                    }
                    "min_length" => {
                        if let Some(val) = &assertion.value {
                            if let Some(n) = val.as_u64() {
                                let _ = writeln!(
                                    out,
                                    "      {inner_expr} |> string.length |> fn(n__) {{ n__ >= {n} }} |> should.equal(True)"
                                );
                            }
                        }
                    }
                    other => {
                        let _ = writeln!(
                            out,
                            "      // optional-prefix assertion '{other}' not yet implemented for Gleam"
                        );
                    }
                }
                let _ = writeln!(out, "    }}");
                let _ = writeln!(out, "    option.None -> should.fail()");
                let _ = writeln!(out, "  }}");
                return;
            }
        }
    }

    // Determine if this field is an optional type (e.g. metadata.output_format).
    // For optional fields, equality comparisons must wrap in option.Some(...).
    let field_is_optional = assertion
        .field
        .as_deref()
        .is_some_and(|f| !f.is_empty() && field_resolver.is_optional(field_resolver.resolve(f)));

    // Determine if this field is an enum type.
    // When true, `equals` assertions are skipped with a comment: Gleam sum types
    // cannot be compared with string literals in generated code without a
    // language-specific coercion that alef does not auto-generate.
    let field_is_enum = assertion
        .field
        .as_deref()
        .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
    if field_is_enum && assertion.assertion_type == "equals" {
        let f = assertion.field.as_deref().unwrap_or("");
        let _ = writeln!(
            out,
            "  // skipped: enum field '{f}' comparison not yet supported in Gleam e2e"
        );
        return;
    }

    let field_expr = match &assertion.field {
        Some(f) if !f.is_empty() => field_resolver.accessor(f, "gleam", result_var),
        _ => result_var.to_string(),
    };

    // Check if the field (or root result) is an array for `contains` assertions.
    // When no field is specified (root result) and call config says result_is_array, treat as array.
    let field_is_array = {
        let f = assertion.field.as_deref().unwrap_or("");
        let is_root = f.is_empty();
        (is_root && result_is_array) || field_resolver.is_array(f) || field_resolver.is_array(field_resolver.resolve(f))
    };

    match assertion.assertion_type.as_str() {
        "equals" => {
            if let Some(expected) = &assertion.value {
                let gleam_val = json_to_gleam(expected);
                if field_is_optional {
                    // Option(T) equality — wrap in option.Some().
                    let _ = writeln!(out, "  {field_expr} |> should.equal(option.Some({gleam_val}))");
                } else {
                    let _ = writeln!(out, "  {field_expr} |> should.equal({gleam_val})");
                }
            }
        }
        "contains" => {
            if let Some(expected) = &assertion.value {
                let gleam_val = json_to_gleam(expected);
                if field_is_array {
                    // List(String) — check any element contains the value.
                    let _ = writeln!(
                        out,
                        "  {field_expr} |> list.any(fn(item__) {{ string.contains(item__, {gleam_val}) }}) |> should.equal(True)"
                    );
                } else if field_is_optional {
                    let _ = writeln!(
                        out,
                        "  {field_expr} |> option.unwrap(\"\") |> string.contains({gleam_val}) |> should.equal(True)"
                    );
                } else {
                    let _ = writeln!(
                        out,
                        "  {field_expr} |> string.contains({gleam_val}) |> should.equal(True)"
                    );
                }
            }
        }
        "contains_all" => {
            if let Some(values) = &assertion.values {
                for val in values {
                    let gleam_val = json_to_gleam(val);
                    if field_is_optional {
                        let _ = writeln!(
                            out,
                            "  {field_expr} |> option.unwrap(\"\") |> string.contains({gleam_val}) |> should.equal(True)"
                        );
                    } else {
                        let _ = writeln!(
                            out,
                            "  {field_expr} |> string.contains({gleam_val}) |> should.equal(True)"
                        );
                    }
                }
            }
        }
        "not_contains" => {
            if let Some(expected) = &assertion.value {
                let gleam_val = json_to_gleam(expected);
                let _ = writeln!(
                    out,
                    "  {field_expr} |> string.contains({gleam_val}) |> should.equal(False)"
                );
            }
        }
        "not_empty" => {
            if field_is_optional {
                // Option(T) — check it is Some.
                let _ = writeln!(out, "  {field_expr} |> option.is_some |> should.equal(True)");
            } else if field_is_array {
                let _ = writeln!(out, "  {field_expr} |> list.is_empty |> should.equal(False)");
            } else {
                let _ = writeln!(out, "  {field_expr} |> string.is_empty |> should.equal(False)");
            }
        }
        "is_empty" => {
            if field_is_optional {
                let _ = writeln!(out, "  {field_expr} |> option.is_none |> should.equal(True)");
            } else if field_is_array {
                let _ = writeln!(out, "  {field_expr} |> list.is_empty |> should.equal(True)");
            } else {
                let _ = writeln!(out, "  {field_expr} |> string.is_empty |> should.equal(True)");
            }
        }
        "starts_with" => {
            if let Some(expected) = &assertion.value {
                let gleam_val = json_to_gleam(expected);
                let _ = writeln!(
                    out,
                    "  {field_expr} |> string.starts_with({gleam_val}) |> should.equal(True)"
                );
            }
        }
        "ends_with" => {
            if let Some(expected) = &assertion.value {
                let gleam_val = json_to_gleam(expected);
                let _ = writeln!(
                    out,
                    "  {field_expr} |> string.ends_with({gleam_val}) |> should.equal(True)"
                );
            }
        }
        "min_length" => {
            if let Some(val) = &assertion.value {
                if let Some(n) = val.as_u64() {
                    let _ = writeln!(
                        out,
                        "  {field_expr} |> string.length |> fn(n__) {{ n__ >= {n} }} |> should.equal(True)"
                    );
                }
            }
        }
        "max_length" => {
            if let Some(val) = &assertion.value {
                if let Some(n) = val.as_u64() {
                    let _ = writeln!(
                        out,
                        "  {field_expr} |> string.length |> fn(n__) {{ n__ <= {n} }} |> should.equal(True)"
                    );
                }
            }
        }
        "count_min" => {
            if let Some(val) = &assertion.value {
                if let Some(n) = val.as_u64() {
                    let _ = writeln!(
                        out,
                        "  {field_expr} |> list.length |> fn(n__) {{ n__ >= {n} }} |> should.equal(True)"
                    );
                }
            }
        }
        "count_equals" => {
            if let Some(val) = &assertion.value {
                if let Some(n) = val.as_u64() {
                    let _ = writeln!(out, "  {field_expr} |> list.length |> should.equal({n})");
                }
            }
        }
        "is_true" => {
            let _ = writeln!(out, "  {field_expr} |> should.equal(True)");
        }
        "is_false" => {
            let _ = writeln!(out, "  {field_expr} |> should.equal(False)");
        }
        "not_error" => {
            // Already handled by the call succeeding.
        }
        "error" => {
            // Handled at the test case level.
        }
        "greater_than" => {
            if let Some(val) = &assertion.value {
                let gleam_val = json_to_gleam(val);
                let _ = writeln!(
                    out,
                    "  {field_expr} |> fn(n__) {{ n__ > {gleam_val} }} |> should.equal(True)"
                );
            }
        }
        "less_than" => {
            if let Some(val) = &assertion.value {
                let gleam_val = json_to_gleam(val);
                let _ = writeln!(
                    out,
                    "  {field_expr} |> fn(n__) {{ n__ < {gleam_val} }} |> should.equal(True)"
                );
            }
        }
        "greater_than_or_equal" => {
            if let Some(val) = &assertion.value {
                let gleam_val = json_to_gleam(val);
                let _ = writeln!(
                    out,
                    "  {field_expr} |> fn(n__) {{ n__ >= {gleam_val} }} |> should.equal(True)"
                );
            }
        }
        "less_than_or_equal" => {
            if let Some(val) = &assertion.value {
                let gleam_val = json_to_gleam(val);
                let _ = writeln!(
                    out,
                    "  {field_expr} |> fn(n__) {{ n__ <= {gleam_val} }} |> should.equal(True)"
                );
            }
        }
        "contains_any" => {
            if let Some(values) = &assertion.values {
                let vals_list = values.iter().map(json_to_gleam).collect::<Vec<_>>().join(", ");
                let _ = writeln!(
                    out,
                    "  [{vals_list}] |> list.any(fn(v__) {{ string.contains({field_expr}, v__) }}) |> should.equal(True)"
                );
            }
        }
        "matches_regex" => {
            let _ = writeln!(out, "  // regex match not yet implemented for Gleam");
        }
        "method_result" => {
            let _ = writeln!(out, "  // method_result assertions not yet implemented for Gleam");
        }
        other => {
            panic!("Gleam e2e generator: unsupported assertion type: {other}");
        }
    }
}

/// Convert a `serde_json::Value` to a Gleam literal string.
fn json_to_gleam(value: &serde_json::Value) -> String {
    match value {
        serde_json::Value::String(s) => format!("\"{}\"", escape_gleam(s)),
        serde_json::Value::Bool(b) => {
            if *b {
                "True".to_string()
            } else {
                "False".to_string()
            }
        }
        serde_json::Value::Number(n) => n.to_string(),
        serde_json::Value::Null => "Nil".to_string(),
        serde_json::Value::Array(arr) => {
            let items: Vec<String> = arr.iter().map(json_to_gleam).collect();
            format!("[{}]", items.join(", "))
        }
        serde_json::Value::Object(_) => {
            let json_str = serde_json::to_string(value).unwrap_or_default();
            format!("\"{}\"", escape_gleam(&json_str))
        }
    }
}

/// Emit a Gleam test backend stub.
///
/// Phase 2 will fill in the real implementation. For now, returns unimplemented!().
pub fn emit_test_backend(
    _trait_bridge: &crate::core::config::TraitBridgeConfig,
    _methods: &[&crate::core::ir::MethodDef],
    _fixture: &crate::e2e::fixture::Fixture,
) -> super::TestBackendEmission {
    unimplemented!("Gleam test_backend emission not yet implemented")
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::config::{GleamElementConstructor, GleamElementField};

    fn batch_file_item_recipe() -> GleamElementConstructor {
        GleamElementConstructor {
            element_type: "BatchFileItem".to_string(),
            constructor: "sample_crate.BatchFileItem".to_string(),
            fields: vec![
                GleamElementField {
                    gleam_field: "path".to_string(),
                    kind: "file_path".to_string(),
                    json_field: Some("path".to_string()),
                    default: None,
                    value: None,
                },
                GleamElementField {
                    gleam_field: "config".to_string(),
                    kind: "literal".to_string(),
                    json_field: None,
                    default: None,
                    value: Some("option.None".to_string()),
                },
            ],
        }
    }

    #[test]
    fn render_element_constructor_file_path_relative_path_gets_test_documents_prefix() {
        let item = serde_json::json!({ "path": "docx/fake.docx" });
        let out = render_gleam_element_constructor(&item, &batch_file_item_recipe(), "../../test_documents");
        assert_eq!(
            out,
            "sample_crate.BatchFileItem(path: \"../../test_documents/docx/fake.docx\", config: option.None)"
        );
    }

    #[test]
    fn render_element_constructor_file_path_absolute_path_passes_through() {
        let item = serde_json::json!({ "path": "/etc/some/absolute" });
        let out = render_gleam_element_constructor(&item, &batch_file_item_recipe(), "../../test_documents");
        assert!(
            out.contains("\"/etc/some/absolute\""),
            "absolute paths must NOT receive the test_documents prefix; got:\n{out}"
        );
    }

    #[test]
    fn render_element_constructor_byte_array_emits_bitarray() {
        let recipe = GleamElementConstructor {
            element_type: "BatchBytesItem".to_string(),
            constructor: "sample_crate.BatchBytesItem".to_string(),
            fields: vec![
                GleamElementField {
                    gleam_field: "content".to_string(),
                    kind: "byte_array".to_string(),
                    json_field: Some("content".to_string()),
                    default: None,
                    value: None,
                },
                GleamElementField {
                    gleam_field: "mime_type".to_string(),
                    kind: "string".to_string(),
                    json_field: Some("mime_type".to_string()),
                    default: Some("text/plain".to_string()),
                    value: None,
                },
                GleamElementField {
                    gleam_field: "config".to_string(),
                    kind: "literal".to_string(),
                    json_field: None,
                    default: None,
                    value: Some("option.None".to_string()),
                },
            ],
        };
        let item = serde_json::json!({ "content": [72, 105], "mime_type": "text/html" });
        let out = render_gleam_element_constructor(&item, &recipe, "../../test_documents");
        assert_eq!(
            out,
            "sample_crate.BatchBytesItem(content: <<72, 105>>, mime_type: \"text/html\", config: option.None)"
        );
    }

    #[test]
    fn build_args_with_json_object_wrapper_substitutes_placeholder() {
        use crate::e2e::config::ArgMapping;
        let arg = ArgMapping {
            name: "config".to_string(),
            field: "config".to_string(),
            arg_type: "json_object".to_string(),
            optional: false,
            owned: false,
            element_type: None,
            go_type: None,
            trait_name: None,
        };
        let input = serde_json::json!({
            "config": { "use_cache": true, "force_ocr": false }
        });
        let Some((_setup, args_str)) = build_args_and_setup(
            &input,
            &[arg],
            "test_fixture",
            "../../test_documents",
            &[],
            Some("k.config_from_json_string({json})"),
            "sample_crate",
            &[],
            None,
            "default",
        ) else {
            panic!("expected Some result from build_args_and_setup");
        };
        // The wrapper template substitutes {json} with the JSON-string literal
        // emitted by json_to_gleam.
        assert!(
            args_str.starts_with("k.config_from_json_string("),
            "wrapper must envelop the JSON literal; got:\n{args_str}"
        );
        assert!(
            args_str.contains("use_cache"),
            "JSON payload must reach the wrapper; got:\n{args_str}"
        );
    }

    #[test]
    fn build_args_without_json_object_wrapper_returns_none_for_skip() {
        use crate::e2e::config::ArgMapping;
        let arg = ArgMapping {
            name: "config".to_string(),
            field: "config".to_string(),
            arg_type: "json_object".to_string(),
            optional: false,
            owned: false,
            element_type: None,
            go_type: None,
            trait_name: None,
        };
        let input = serde_json::json!({ "config": { "x": 1 } });
        // No recipe, no wrapper, no from_json — must return None for skip.
        let result = build_args_and_setup(
            &input,
            &[arg],
            "test_fixture",
            "../../test_documents",
            &[],
            None,
            "sample_crate",
            &[],
            None,
            "default",
        );
        assert!(
            result.is_none(),
            "json_object without recipe/wrapper/from_json must return None for skip; got: {result:?}"
        );
    }

    #[test]
    fn render_element_constructor_string_falls_back_to_default() {
        let recipe = GleamElementConstructor {
            element_type: "BatchBytesItem".to_string(),
            constructor: "k.BatchBytesItem".to_string(),
            fields: vec![GleamElementField {
                gleam_field: "mime_type".to_string(),
                kind: "string".to_string(),
                json_field: Some("mime_type".to_string()),
                default: Some("text/plain".to_string()),
                value: None,
            }],
        };
        let item = serde_json::json!({});
        let out = render_gleam_element_constructor(&item, &recipe, "../../test_documents");
        assert!(
            out.contains("mime_type: \"text/plain\""),
            "missing string field must fall back to default; got:\n{out}"
        );
    }
}
