//! Dart e2e test generator using package:test and package:http.
//!
//! Generates `e2e/dart/test/<category>_test.dart` files from JSON fixtures.
//! HTTP fixtures hit the mock server at `MOCK_SERVER_URL/fixtures/<id>`.
//! Non-HTTP fixtures without a dart-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::core::template_versions::pub_dev;
use crate::e2e::codegen::resolve_field;
use crate::e2e::config::E2eConfig;
use crate::e2e::escape::sanitize_filename;
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::{Assertion, Fixture, FixtureGroup, HttpFixture, ValidationErrorExpectation};
use anyhow::Result;
use heck::ToLowerCamelCase;
use std::cell::Cell;
use std::collections::HashMap;
use std::fmt::Write as FmtWrite;
use std::path::PathBuf;

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

/// Dart e2e code generator.
pub struct DartE2eCodegen;

impl E2eCodegen for DartE2eCodegen {
    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 package config.
        let dart_pkg = e2e_config.resolve_package("dart");
        let pkg_name = dart_pkg
            .as_ref()
            .and_then(|p| p.name.as_ref())
            .cloned()
            .unwrap_or_else(|| config.dart_pubspec_name());
        let pkg_path = dart_pkg
            .as_ref()
            .and_then(|p| p.path.as_ref())
            .cloned()
            .unwrap_or_else(|| "../../packages/dart".to_string());
        let pkg_version = dart_pkg
            .as_ref()
            .and_then(|p| p.version.as_ref())
            .cloned()
            .or_else(|| config.resolved_version())
            .unwrap_or_else(|| "0.1.0".to_string());

        // Generate pubspec.yaml with http dependency for HTTP client tests.
        files.push(GeneratedFile {
            path: output_base.join("pubspec.yaml"),
            content: render_pubspec(&pkg_name, &pkg_path, &pkg_version, e2e_config.dep_mode),
            generated_header: false,
        });

        // Generate dart_test.yaml to limit parallelism — the mock server uses keep-alive
        // connections and gets overwhelmed when test files run in parallel.
        files.push(GeneratedFile {
            path: output_base.join("dart_test.yaml"),
            content: concat!(
                "# Generated by alef — DO NOT EDIT.\n",
                "# Run test files sequentially to avoid overwhelming the mock server with\n",
                "# concurrent keep-alive connections.\n",
                "concurrency: 1\n",
            )
            .to_string(),
            generated_header: false,
        });

        let test_base = output_base.join("test");

        // One test file per fixture group.
        let bridge_class = config.dart_bridge_class_name();

        // FRB places its generated dart code under `lib/src/{module_name}_bridge_generated/`,
        // where `module_name` is the snake_cased crate name (e.g. `sample_markdown_rs`).
        // This is independent of the pubspec `name` (which may be a short alias like `sample-markdown`).
        let frb_module_name = config.name.replace('-', "_");

        // Methods declared as `stub_methods` in `[crates.dart]` cannot be bridged through
        // FRB and have `unimplemented!()` bodies on the Rust side. Emitting e2e tests for
        // these fixtures would result in `PanicException` at run-time. Filter them out
        // here so the dart e2e suite mirrors the actual runtime surface of the binding.
        let dart_stub_methods: std::collections::HashSet<String> = config
            .dart
            .as_ref()
            .map(|d| d.stub_methods.iter().cloned().collect())
            .unwrap_or_default();

        // Build the Dart stringy field classification map for aggregating text
        // accessors in Vec<T> contains assertions.
        let dart_first_class_map = build_dart_first_class_map(type_defs, enums, e2e_config);

        for group in groups {
            let active: Vec<&Fixture> = group
                .fixtures
                .iter()
                .filter(|f| super::should_include_fixture(f, lang, e2e_config))
                .filter(|f| {
                    let call_config = e2e_config.resolve_call_for_fixture(
                        f.call.as_deref(),
                        &f.id,
                        &f.resolved_category(),
                        &f.tags,
                        &f.input,
                    );
                    let resolved_function = call_config
                        .overrides
                        .get(lang)
                        .and_then(|o| o.function.as_ref())
                        .cloned()
                        .unwrap_or_else(|| call_config.function.clone());
                    !dart_stub_methods.contains(&resolved_function)
                })
                .collect();

            if active.is_empty() {
                continue;
            }

            let filename = format!("{}_test.dart", sanitize_filename(&group.category));
            let content = render_test_file(
                &group.category,
                &active,
                e2e_config,
                lang,
                &pkg_name,
                &frb_module_name,
                &bridge_class,
                &dart_first_class_map,
                &config.adapters,
                config,
                type_defs,
            );
            files.push(GeneratedFile {
                path: test_base.join(filename),
                content,
                generated_header: true,
            });
        }

        Ok(files)
    }

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

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

fn render_pubspec(
    pkg_name: &str,
    pkg_path: &str,
    pkg_version: &str,
    dep_mode: crate::e2e::config::DependencyMode,
) -> String {
    let test_ver = pub_dev::TEST_PACKAGE;
    let http_ver = pub_dev::HTTP_PACKAGE;

    let dep_block = match dep_mode {
        crate::e2e::config::DependencyMode::Registry => {
            // Only add ^ prefix if version doesn't already start with a constraint operator
            let constraint = if pkg_version.starts_with('^')
                || pkg_version.starts_with('~')
                || pkg_version.starts_with('>')
                || pkg_version.starts_with('<')
                || pkg_version.starts_with('=')
            {
                pkg_version.to_string()
            } else {
                format!("^{pkg_version}")
            };
            format!("  {pkg_name}: {constraint}")
        }
        crate::e2e::config::DependencyMode::Local => {
            format!("  {pkg_name}:\n    path: {pkg_path}")
        }
    };

    let sdk = crate::core::template_versions::toolchain::DART_SDK_CONSTRAINT;
    format!(
        r#"name: e2e_dart
version: 0.1.0
publish_to: none

environment:
  sdk: "{sdk}"

dependencies:
{dep_block}

dev_dependencies:
  test: {test_ver}
  http: {http_ver}
"#
    )
}

#[allow(clippy::too_many_arguments)]
fn render_test_file(
    category: &str,
    fixtures: &[&Fixture],
    e2e_config: &E2eConfig,
    lang: &str,
    pkg_name: &str,
    frb_module_name: &str,
    bridge_class: &str,
    dart_first_class_map: &crate::e2e::field_access::DartFirstClassMap,
    adapters: &[crate::core::config::extras::AdapterConfig],
    config: &ResolvedCrateConfig,
    type_defs: &[crate::core::ir::TypeDef],
) -> String {
    let mut out = String::new();
    out.push_str(&hash::header(CommentStyle::DoubleSlash));
    // Suppress unused_local_variable: `final result = await api.method(...)` is
    // emitted for every test case; tests that only check for absence of errors
    // do not consume `result`, triggering this dart-analyze warning.
    out.push_str("// ignore_for_file: unused_local_variable\n\n");

    // Check if any fixture needs the http package (HTTP server tests).
    let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());

    // Check if any fixture needs Uint8List.fromList (batch item byte arrays or trait_bridge args).
    let has_batch_byte_items = fixtures.iter().any(|f| {
        let call_config =
            e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
        f.resolved_args(call_config).iter().any(|a| {
            (a.element_type.as_deref() == Some("BatchBytesItem") && resolve_field(&f.input, &a.field).is_array())
                || a.arg_type == "test_backend" // trait_bridge stubs may use Uint8List in method params
        })
    });

    // Detect whether any fixture uses file_path or bytes args — if so, setUpAll must chdir
    // to the test_documents directory so that relative paths like "docx/fake.docx" resolve.
    // Mirrors the Ruby/Python conftest and Swift setUp patterns.
    let needs_chdir = fixtures.iter().any(|f| {
        if f.is_http_test() {
            return false;
        }
        let call_config =
            e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
        f.resolved_args(call_config)
            .iter()
            .any(|a| a.arg_type == "file_path" || a.arg_type == "bytes")
    });

    // Detect whether any non-HTTP fixture uses a json_object arg that resolves to a JSON array —
    // those are materialized via `jsonDecode` at test-run time and cast to `List<String>`.
    // Handle args themselves no longer require `jsonDecode` since they construct the config via
    // the FRB-generated `createCrawlConfigFromJson(json:)` helper which accepts the JSON string
    // directly. The variable name is kept as `has_handle_args` for downstream stability.
    let has_handle_args = fixtures.iter().any(|f| {
        if f.is_http_test() {
            return false;
        }
        let call_config =
            e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
        call_config
            .args
            .iter()
            .any(|a| a.arg_type == "json_object" && super::resolve_field(&f.input, &a.field).is_array())
    });

    // Detect whether any fixture uses a PageAction array argument (for interact calls).
    // PageAction and ScrollDirection types must be emitted in the test helper code only if used.
    let has_page_action = fixtures.iter().any(|f| {
        if f.is_http_test() {
            return false;
        }
        let call_config =
            e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
        f.resolved_args(call_config).iter().any(|a| {
            a.element_type.as_deref() == Some("PageAction") && super::resolve_field(&f.input, &a.field).is_array()
        })
    });

    // Non-HTTP fixtures that build a mock-server URL still reference `Platform.environment`
    // (from `dart:io`). This applies to `mock_url` and `mock_url_list` args and to fixtures
    // routed through a `client_factory` (per-call override or per-language override) that
    // derives `_mockUrl` inline. Without this, the generated tests fail to compile with
    // `Error: Undefined name 'Platform'`.
    let lang_client_factory = e2e_config
        .call
        .overrides
        .get(lang)
        .and_then(|o| o.client_factory.as_deref())
        .is_some();
    let has_mock_url_refs = lang_client_factory
        || fixtures.iter().any(|f| {
            if f.is_http_test() {
                return false;
            }
            let call_config = e2e_config.resolve_call_for_fixture(
                f.call.as_deref(),
                &f.id,
                &f.resolved_category(),
                &f.tags,
                &f.input,
            );
            if call_config
                .args
                .iter()
                .any(|a| a.arg_type == "mock_url" || a.arg_type == "mock_url_list")
            {
                return true;
            }
            call_config
                .overrides
                .get(lang)
                .and_then(|o| o.client_factory.as_deref())
                .is_some()
        });

    let _ = writeln!(out, "import 'package:test/test.dart';");
    // `dart:io` provides HttpClient/SocketException (HTTP fixtures), Platform/Directory
    // (file-path/bytes fixtures requiring chdir), and Platform.environment (mock-url
    // fixtures). Skip the import when none of these are in play — unconditional emission
    // triggers `unused_import` warnings.
    if has_http_fixtures || needs_chdir || has_mock_url_refs {
        let _ = writeln!(out, "import 'dart:io';");
    }
    if has_batch_byte_items {
        let _ = writeln!(out, "import 'dart:typed_data';");
    }
    let _ = writeln!(out, "import 'package:{pkg_name}/{pkg_name}.dart';");
    // RustLib is the flutter_rust_bridge entrypoint; must be initialized before any FRB call.
    // FRB places its generated dart sources under `lib/src/{module_name}_bridge_generated/`,
    // where `module_name` is the snake_cased crate name (independent of the pubspec `name`,
    // which may be a short alias like `sample-markdown`). `RustLib` lives in `frb_generated.dart` and
    // is not re-exported by the FRB barrel `lib.dart`, so we import it directly.
    let _ = writeln!(
        out,
        "import 'package:{pkg_name}/src/{frb_module_name}_bridge_generated/frb_generated.dart' show RustLib;"
    );
    // dart:async provides Completer (HTTP response handling + the mock-server
    // spawn harness, which awaits a Completer for the startup URL line).
    if has_http_fixtures || has_mock_url_refs {
        let _ = writeln!(out, "import 'dart:async';");
    }
    // dart:convert provides jsonDecode for handle-arg engine construction, HTTP response parsing,
    // and PageAction array deserialization, plus utf8/LineSplitter for decoding the mock-server's
    // startup stdout (MOCK_SERVER_URL= / MOCK_SERVERS=) in the spawn harness.
    if has_http_fixtures || has_handle_args || has_page_action || has_mock_url_refs {
        let _ = writeln!(out, "import 'dart:convert';");
    }
    let _ = writeln!(out);

    // Emit file-level HTTP client and serialization mutex.
    //
    // The shared HttpClient reuses keep-alive connections to minimize TCP overhead.
    // The mutex (_lock) ensures requests are serialized within the file so the
    // connection pool is not exercised concurrently by dart:test's async runner.
    //
    // _withRetry wraps the entire request closure with one automatic retry on
    // transient connection errors (keep-alive connections can be silently closed
    // by the server just as the client tries to reuse them).
    if has_http_fixtures {
        let _ = writeln!(out, "HttpClient _httpClient = HttpClient()..maxConnectionsPerHost = 1;");
        let _ = writeln!(out);
        let _ = writeln!(out, "var _lock = Future<void>.value();");
        let _ = writeln!(out);
        let _ = writeln!(out, "Future<T> _serialized<T>(Future<T> Function() fn) async {{");
        let _ = writeln!(out, "  final current = _lock;");
        let _ = writeln!(out, "  final next = Completer<void>();");
        let _ = writeln!(out, "  _lock = next.future;");
        let _ = writeln!(out, "  try {{");
        let _ = writeln!(out, "    await current;");
        let _ = writeln!(out, "    return await fn();");
        let _ = writeln!(out, "  }} finally {{");
        let _ = writeln!(out, "    next.complete();");
        let _ = writeln!(out, "  }}");
        let _ = writeln!(out, "}}");
        let _ = writeln!(out);
        // The `fn` here is the full request closure. Transient connection errors
        // (`SocketException` / `HttpException: Connection reset by peer`) happen rarely
        // but non-deterministically when the local mock server drops a connection mid-flight;
        // a single retry is not always enough, so retry several times with a short backoff,
        // recreating the HttpClient each time to drop any poisoned pooled connection. The
        // final attempt is outside the catch so a genuine, persistent failure still surfaces.
        let _ = writeln!(out, "Future<T> _withRetry<T>(Future<T> Function() fn) async {{");
        let _ = writeln!(out, "  for (var attempt = 0; attempt < 5; attempt++) {{");
        let _ = writeln!(out, "    try {{");
        let _ = writeln!(out, "      return await fn();");
        let _ = writeln!(out, "    }} on SocketException {{");
        let _ = writeln!(out, "      _httpClient.close(force: true);");
        let _ = writeln!(out, "      _httpClient = HttpClient()..maxConnectionsPerHost = 1;");
        let _ = writeln!(out, "    }} on HttpException {{");
        let _ = writeln!(out, "      _httpClient.close(force: true);");
        let _ = writeln!(out, "      _httpClient = HttpClient()..maxConnectionsPerHost = 1;");
        let _ = writeln!(out, "    }}");
        let _ = writeln!(
            out,
            "    await Future<void>.delayed(Duration(milliseconds: 25 * (attempt + 1)));"
        );
        let _ = writeln!(out, "  }}");
        let _ = writeln!(out, "  return await fn();");
        let _ = writeln!(out, "}}");
        let _ = writeln!(out);
    }

    let _ = writeln!(out, "// E2e tests for category: {category}");
    let _ = writeln!(out);

    // Emit a helper function to normalize enum values to their serde wire format.
    // Dart enums' .toString() returns "EnumName.variant" but fixtures use serde wire format
    // (e.g. "stop" for FinishReason.stop, "tool_calls" for FinishReason.toolCalls).
    // This helper handles enum-to-wire conversion by calling .name (which gives the Dart
    // variant name like "toolCalls") and converting back to snake_case for multi-word variants.
    let _ = writeln!(out, "String _alefE2eText(Object? value) {{");
    let _ = writeln!(out, "  if (value == null) return '';");
    let _ = writeln!(
        out,
        "  // Check if it's an enum by examining its toString representation."
    );
    let _ = writeln!(out, "  final str = value.toString();");
    let _ = writeln!(out, "  if (str.contains('.')) {{");
    let _ = writeln!(
        out,
        "    // Enum.toString() returns 'EnumName.variantName'. Extract the variant name."
    );
    let _ = writeln!(out, "    final parts = str.split('.');");
    let _ = writeln!(out, "    if (parts.length == 2) {{");
    let _ = writeln!(out, "      final variantName = parts[1];");
    let _ = writeln!(
        out,
        "      // Convert camelCase variant names to snake_case for serde compatibility."
    );
    let _ = writeln!(out, "      // E.g. 'toolCalls' -> 'tool_calls', 'stop' -> 'stop'.");
    let _ = writeln!(out, "      return _camelToSnake(variantName);");
    let _ = writeln!(out, "    }}");
    let _ = writeln!(out, "  }}");
    let _ = writeln!(out, "  return str;");
    let _ = writeln!(out, "}}");
    let _ = writeln!(out);

    // Helper to convert camelCase to snake_case.
    let _ = writeln!(out, "String _camelToSnake(String camel) {{");
    let _ = writeln!(out, "  final buffer = StringBuffer();");
    let _ = writeln!(out, "  for (int i = 0; i < camel.length; i++) {{");
    let _ = writeln!(out, "    final char = camel[i];");
    let _ = writeln!(out, "    if (char.contains(RegExp(r'[A-Z]'))) {{");
    let _ = writeln!(out, "      if (i > 0) buffer.write('_');");
    let _ = writeln!(out, "      buffer.write(char.toLowerCase());");
    let _ = writeln!(out, "    }} else {{");
    let _ = writeln!(out, "      buffer.write(char);");
    let _ = writeln!(out, "    }}");
    let _ = writeln!(out, "  }}");
    let _ = writeln!(out, "  return buffer.toString();");
    let _ = writeln!(out, "}}");
    let _ = writeln!(out);

    // Only emit _parsePageAction if any fixture uses PageAction arrays.
    if has_page_action {
        let _ = writeln!(out, "PageAction _parsePageAction(Map<String, dynamic> json) {{");
        let _ = writeln!(out, "  final actionType = json['type'] as String?;");
        let _ = writeln!(out, "  switch (actionType) {{");
        let _ = writeln!(out, "    case 'click':");
        let _ = writeln!(
            out,
            "      return PageAction.click(selector: json['selector'] as String);"
        );
        let _ = writeln!(out, "    case 'type':");
        let _ = writeln!(out, "      return PageAction.typeText(");
        let _ = writeln!(out, "        selector: json['selector'] as String,");
        let _ = writeln!(out, "        text: json['text'] as String,");
        let _ = writeln!(out, "      );");
        let _ = writeln!(out, "    case 'press':");
        let _ = writeln!(out, "      return PageAction.press(");
        let _ = writeln!(out, "        key: json['key'] as String,");
        let _ = writeln!(out, "      );");
        let _ = writeln!(out, "    case 'scroll':");
        let _ = writeln!(out, "      return PageAction.scroll(");
        let _ = writeln!(out, "        direction: ScrollDirection.down,");
        let _ = writeln!(out, "        selector: json['selector'] as String? ?? '',");
        let _ = writeln!(out, "        amount: json['amount'] as int? ?? 0,");
        let _ = writeln!(out, "      );");
        let _ = writeln!(out, "    case 'wait':");
        let _ = writeln!(out, "      return PageAction.wait(");
        let _ = writeln!(out, "        milliseconds: json['timeout_ms'] as int? ?? 0,");
        let _ = writeln!(out, "        selector: json['selector'] as String,");
        let _ = writeln!(out, "      );");
        let _ = writeln!(out, "    case 'screenshot':");
        let _ = writeln!(
            out,
            "      return PageAction.screenshot(fullPage: json['full_page'] as bool? ?? false);"
        );
        let _ = writeln!(out, "    case 'executeJs':");
        let _ = writeln!(
            out,
            "      return PageAction.executeJs(script: json['script'] as String);"
        );
        let _ = writeln!(out, "    case 'scrape':");
        let _ = writeln!(out, "      return const PageAction.scrape();");
        let _ = writeln!(out, "    default:");
        let _ = writeln!(
            out,
            "      throw UnsupportedError('Unknown PageAction type: $actionType');"
        );
        let _ = writeln!(out, "  }}");
        let _ = writeln!(out, "}}");
        let _ = writeln!(out);
    }

    // Whether this test file must spawn the mock-server. True for direct HTTP
    // fixtures and for any fixture that derives a URL from `MOCK_SERVER_URL`
    // (mock_url args / client_factory). `package:test` has no cross-file global
    // setup, so each file spawns its own server in `setUpAll` and tears it down
    // in `tearDownAll`; `dart_test.yaml` pins `concurrency: 1` so at most one
    // server runs at a time. A pre-set `MOCK_SERVER_URL` (external CI
    // orchestration) short-circuits the spawn. Mirrors the Python conftest /
    // Ruby spec_helper / Java MockServerListener pattern.
    let needs_mock_server_spawn = has_http_fixtures || has_mock_url_refs;

    // The `_mockServerFor` helper (per-fixture URL lookup) is only referenced by
    // host-root fixtures and by `mock_url`/`mock_url_list` args. Emitting it for
    // pure-HTTP files would trip the `unused_element` analyzer lint, so gate it
    // on actual usage. The `_spawnedMockServers` map it reads is gated likewise.
    let needs_mock_server_for_helper = fixtures.iter().any(|f| {
        if f.is_http_test() {
            return f.has_host_root_route();
        }
        let call_config =
            e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
        f.has_host_root_route()
            || f.resolved_args(call_config)
                .iter()
                .any(|a| a.arg_type == "mock_url" || a.arg_type == "mock_url_list")
    });

    // Top-level mock-server state. `Platform.environment` is read-only in Dart,
    // so the spawned server's URL is held in mutable globals and read through
    // helper functions (rather than re-reading the environment) by the test
    // bodies below.
    if needs_mock_server_spawn {
        let _ = writeln!(out, "Process? _mockServer;");
        let _ = writeln!(out, "String? _spawnedMockServerUrl;");
        if needs_mock_server_for_helper {
            let _ = writeln!(out, "final Map<String, String> _spawnedMockServers = {{}};");
        }
        let _ = writeln!(out);
        let _ = writeln!(
            out,
            "String _mockServerUrl() => _spawnedMockServerUrl ?? Platform.environment['MOCK_SERVER_URL'] ?? 'http://localhost:8080';"
        );
        let _ = writeln!(out);
        if needs_mock_server_for_helper {
            let _ = writeln!(
                out,
                "String _mockServerFor(String fixtureId, String envKey) => _spawnedMockServers[fixtureId] ?? Platform.environment[envKey] ?? '${{_mockServerUrl()}}/fixtures/$fixtureId';"
            );
            let _ = writeln!(out);
        }
    }

    // First pass: collect module-level test stub class definitions BEFORE void main().
    // Dart does not allow class definitions inside functions, so we must emit them
    // at the module level before void main().
    let mut test_stub_classes = String::new();
    for fixture in fixtures {
        collect_dart_test_stub_classes(&mut test_stub_classes, fixture, e2e_config, config, type_defs);
    }
    if !test_stub_classes.is_empty() {
        out.push_str(&test_stub_classes);
        let _ = writeln!(out);
    }

    let _ = writeln!(out, "void main() {{");

    // Emit setUpAll to initialize the flutter_rust_bridge before any test runs and,
    // when fixtures load files by path, chdir to test_documents so that relative
    // paths like "docx/fake.docx" resolve correctly.
    //
    // The test_documents directory lives two levels above e2e/dart/ (at the repo root).
    // The FIXTURES_DIR environment variable can override this for CI environments.
    let _ = writeln!(out, "  setUpAll(() async {{");
    let _ = writeln!(out, "    await RustLib.init();");
    if needs_chdir {
        let test_docs_path = e2e_config.test_documents_relative_from(0);
        let _ = writeln!(
            out,
            "    final _testDocs = Platform.environment['FIXTURES_DIR'] ?? '{test_docs_path}';"
        );
        let _ = writeln!(out, "    final _dir = Directory(_testDocs);");
        let _ = writeln!(out, "    if (_dir.existsSync()) Directory.current = _dir;");
    }
    if needs_mock_server_spawn {
        render_dart_mock_server_spawn(&mut out, needs_mock_server_for_helper);
    }
    let _ = writeln!(out, "  }});");
    let _ = writeln!(out);

    // Close the shared client after all tests in this file complete, and tear
    // down the mock-server we spawned.
    if has_http_fixtures || needs_mock_server_spawn {
        let _ = writeln!(out, "  tearDownAll(() async {{");
        if has_http_fixtures {
            let _ = writeln!(out, "    _httpClient.close(force: true);");
        }
        if needs_mock_server_spawn {
            let _ = writeln!(out, "    final server = _mockServer;");
            let _ = writeln!(out, "    if (server != null) {{");
            let _ = writeln!(out, "      server.kill();");
            let _ = writeln!(out, "      await server.exitCode;");
            let _ = writeln!(out, "    }}");
        }
        let _ = writeln!(out, "  }});");
        let _ = writeln!(out);
    }

    for fixture in fixtures {
        render_test_case(
            &mut out,
            fixture,
            DartTestCaseContext {
                e2e_config,
                lang,
                bridge_class,
                dart_first_class_map,
                adapters,
                config,
                type_defs,
            },
        );
    }

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

/// Emit the `setUpAll` body that spawns the standalone mock-server binary and
/// captures its URL into the top-level `_spawnedMockServerUrl` /
/// `_spawnedMockServers` globals.
///
/// The mock-server binds an ephemeral `127.0.0.1` port and prints
/// `MOCK_SERVER_URL=http://127.0.0.1:<port>` (plus an optional
/// `MOCK_SERVERS={...}` JSON line for host-root fixtures) on stdout once it is
/// listening. A pre-set `MOCK_SERVER_URL` environment variable (external CI
/// orchestration) short-circuits the spawn. Mirrors the Python conftest / Ruby
/// spec_helper / Java MockServerListener spawn pattern.
///
/// Emitted inside an `async` `setUpAll`; the binary lives at
/// `../rust/target/release/mock-server` relative to the `e2e/dart/test/`
/// directory and is passed the repo-root `fixtures/` directory as its argument.
fn render_dart_mock_server_spawn(out: &mut String, parse_mock_servers: bool) {
    let _ = writeln!(out, "    if (Platform.environment['MOCK_SERVER_URL'] == null) {{");
    let _ = writeln!(
        out,
        "      final _bin = Platform.script.resolve('../rust/target/release/mock-server').toFilePath();"
    );
    let _ = writeln!(
        out,
        "      final _fixturesDir = Platform.script.resolve('../../fixtures').toFilePath();"
    );
    let _ = writeln!(out, "      if (File(_bin).existsSync()) {{");
    let _ = writeln!(
        out,
        "        _mockServer = await Process.start(_bin, [_fixturesDir], mode: ProcessStartMode.normal);"
    );
    // A single `listen` keeps draining stdout after the startup lines are seen
    // (so a full pipe never blocks the child); the Completer resolves once the
    // URL has been captured. `Process.stdout` is a single-subscription stream,
    // so it must be consumed exactly once — re-reading `.stdout` would throw.
    let _ = writeln!(out, "        final _ready = Completer<void>();");
    let _ = writeln!(out, "        _mockServer!.stdout");
    let _ = writeln!(out, "            .transform(utf8.decoder)");
    let _ = writeln!(out, "            .transform(const LineSplitter())");
    let _ = writeln!(out, "            .listen((_line) {{");
    let _ = writeln!(out, "          final _trimmed = _line.trim();");
    let _ = writeln!(out, "          if (_trimmed.startsWith('MOCK_SERVER_URL=')) {{");
    let _ = writeln!(
        out,
        "            _spawnedMockServerUrl = _trimmed.substring('MOCK_SERVER_URL='.length);"
    );
    let _ = writeln!(out, "          }} else if (_trimmed.startsWith('MOCK_SERVERS=')) {{");
    if parse_mock_servers {
        // Parse the per-fixture host-root URL map (MOCK_SERVERS={...}) into the
        // `_spawnedMockServers` lookup consumed by `_mockServerFor`.
        let _ = writeln!(
            out,
            "            final _json = _trimmed.substring('MOCK_SERVERS='.length);"
        );
        let _ = writeln!(out, "            try {{");
        let _ = writeln!(
            out,
            "              final _parsed = jsonDecode(_json) as Map<String, dynamic>;"
        );
        let _ = writeln!(out, "              _parsed.forEach((k, v) {{");
        let _ = writeln!(out, "                _spawnedMockServers[k] = v as String;");
        let _ = writeln!(out, "              }});");
        let _ = writeln!(out, "            }} catch (_) {{}}");
    }
    let _ = writeln!(out, "            if (!_ready.isCompleted) _ready.complete();");
    let _ = writeln!(out, "          }} else if (_spawnedMockServerUrl != null) {{");
    let _ = writeln!(out, "            if (!_ready.isCompleted) _ready.complete();");
    let _ = writeln!(out, "          }}");
    let _ = writeln!(out, "        }}, onDone: () {{");
    let _ = writeln!(out, "          if (!_ready.isCompleted) _ready.complete();");
    let _ = writeln!(out, "        }});");
    let _ = writeln!(
        out,
        "        await _ready.future.timeout(const Duration(seconds: 30), onTimeout: () {{}});"
    );
    let _ = writeln!(out, "      }}");
    let _ = writeln!(out, "    }}");
}

/// Collect module-level test stub class definitions for Dart.
/// Dart does not allow class definitions inside functions, so we must emit them
/// at the module level before void main(). This function checks if the fixture
/// uses a test_backend argument and if so, emits the class definition.
fn collect_dart_test_stub_classes(
    out: &mut String,
    fixture: &Fixture,
    _e2e_config: &E2eConfig,
    config: &ResolvedCrateConfig,
    type_defs: &[crate::core::ir::TypeDef],
) {
    // HTTP fixtures do not use test_backend.
    if fixture.is_http_test() {
        return;
    }

    // Check fixture.args directly (not call_config.args, which is empty for trait-bridge calls).
    // The fixture JSON defines the actual arguments including test_backend definitions.
    for arg_def in &fixture.args {
        if arg_def.arg_type != "test_backend" {
            continue;
        }
        if let Some(trait_name) = &arg_def.trait_name {
            if let Some(trait_bridge) = config.trait_bridges.iter().find(|tb| tb.trait_name == *trait_name) {
                let methods: Vec<&crate::core::ir::MethodDef> = type_defs
                    .iter()
                    .find(|t| t.name == *trait_name)
                    .map(|t| t.methods.iter().collect())
                    .unwrap_or_default();
                let emission = emit_test_backend(trait_bridge, &methods, fixture);
                // Emit only the class definition at module-level.
                let _ = writeln!(out, "{}", emission.setup_block);
                let _ = writeln!(out);
            }
        }
    }
}

struct DartTestCaseContext<'a> {
    e2e_config: &'a E2eConfig,
    lang: &'a str,
    bridge_class: &'a str,
    dart_first_class_map: &'a crate::e2e::field_access::DartFirstClassMap,
    adapters: &'a [crate::core::config::extras::AdapterConfig],
    config: &'a ResolvedCrateConfig,
    type_defs: &'a [crate::core::ir::TypeDef],
}

fn render_test_case(out: &mut String, fixture: &Fixture, context: DartTestCaseContext<'_>) {
    let DartTestCaseContext {
        e2e_config,
        lang,
        bridge_class,
        dart_first_class_map,
        adapters,
        config,
        type_defs,
    } = context;
    // HTTP fixtures: hit the mock server.
    if let Some(http) = &fixture.http {
        render_http_test_case(out, fixture, http);
        return;
    }

    // Non-HTTP fixtures: render a call-based test using the resolved 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_with_dart_first_class(
        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),
        &HashMap::new(),
        dart_first_class_map.clone(),
    )
    .with_dart_root_type(dart_call_result_type(call_config).or_else(|| dart_first_class_map.root_type.clone()));
    let field_resolver = &call_field_resolver;
    let enum_fields_base = e2e_config.effective_fields_enum(call_config);

    // Merge per-language enum_fields from the Dart override into the effective enum set so that
    // fields like "status" (BatchStatus on BatchObject) are treated as enum-typed
    // even when they are not globally listed in fields_enum (they are context-
    // dependent — BatchStatus on BatchObject but plain String on ResponseObject).
    let effective_enum_fields: std::collections::HashSet<String> = {
        let dart_overrides = call_config.overrides.get("dart");
        if let Some(overrides) = dart_overrides {
            let mut merged = enum_fields_base.clone();
            merged.extend(overrides.enum_fields.keys().cloned());
            merged
        } else {
            enum_fields_base.clone()
        }
    };
    let enum_fields = &effective_enum_fields;
    let call_overrides = call_config.overrides.get(lang);
    let mut function_name = call_overrides
        .and_then(|o| o.function.as_ref())
        .cloned()
        .unwrap_or_else(|| call_config.function.clone());
    // Convert snake_case function names to camelCase for Dart conventions.
    function_name = function_name
        .split('_')
        .enumerate()
        .map(|(i, part)| {
            if i == 0 {
                part.to_string()
            } else {
                let mut chars = part.chars();
                match chars.next() {
                    None => String::new(),
                    Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
                }
            }
        })
        .collect::<Vec<_>>()
        .join("");
    let result_var = &call_config.result_var;
    let description = escape_dart(&fixture.description);
    let fixture_id = &fixture.id;
    // `is_async` retained for future use (e.g. non-FRB backends); unused with FRB since
    // all wrappers return Future<T>.
    let _is_async = call_overrides.and_then(|o| o.r#async).unwrap_or(call_config.r#async);

    let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
    let is_streaming = crate::e2e::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming);
    // `result_is_simple = true` means the dart return is a scalar/bytes value
    // (e.g. `Uint8List` for speech/file_content), not a struct. Field-based
    // assertions like `audio.not_empty` collapse to whole-result checks so we
    // don't emit `result.audio` against a `Uint8List` receiver.
    let result_is_simple = call_overrides.is_some_and(|o| o.result_is_simple) || call_config.result_is_simple;

    // Resolve options_type and options_via from per-fixture → per-call → default.
    // These drive how `json_object` args are constructed:
    //   options_via = "from_json" — call `createTypeNameFromJson(json: r'...')` bridge
    //                               helper and pass the result as a named parameter `req:`.
    //   All other values (or absent) — existing behaviour (batch arrays, config objects,
    //   generic JSON arrays, or nothing).
    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("kwargs");

    // Build argument list from fixture.input and resolved args (fixture.args or call_config.args).
    // Use `resolve_field` (respects the `field` path like "input.data") rather than
    // looking up by `arg_def.name` directly — the name and the field key may differ.
    //
    // For `extract_file_sync` / `extract_file` fixtures that omit `mime_type`,
    // derive the MIME from the path extension so `extractBytesSync`/`extractBytes`
    // can be called (both require an explicit MIME type).
    let file_path_for_mime: Option<&str> = fixture
        .resolved_args(call_config)
        .iter()
        .find(|a| a.arg_type == "file_path")
        .and_then(|a| resolve_field(&fixture.input, &a.field).as_str());

    // Detect whether this call converts a file_path arg to bytes at test-run time.
    // Dart cannot pass OS-level file paths through the FRB bridge — the idiomatic API
    // is always bytes. When a file_path arg is present (and no caller-supplied dart
    // function override has already been applied), remap the function name:
    //   extractFile      → extractBytes
    //   extractFileSync  → extractBytesSync
    let has_file_path_arg = fixture
        .resolved_args(call_config)
        .iter()
        .any(|a| a.arg_type == "file_path");
    // Apply the remap only when no per-fixture dart override has already specified the
    // function — if the fixture author set a dart-specific function name we trust it.
    let caller_supplied_override = call_overrides.and_then(|o| o.function.as_ref()).is_some();
    if has_file_path_arg && !caller_supplied_override {
        function_name = match function_name.as_str() {
            "extractFile" => "extractBytes".to_string(),
            "extractFileSync" => "extractBytesSync".to_string(),
            other => other.to_string(),
        };
    }

    // Resolve client_factory early so the per-arg builders below can pick the
    // calling convention. When `client_factory` is set the test calls methods on
    // an FRB-generated client instance (e.g. sample-llm's `retrieveFile`), and FRB
    // emits every non-`config` parameter as a Dart named-required parameter. When
    // unset the call routes through a hand-written facade whose required args are
    // positional. See the `"string"` arg handler below.
    let client_factory_for_args: Option<&str> =
        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())
        });

    // Resolve adapter request type for streaming methods.
    let adapter = adapters.iter().find(|a| a.name == call_config.function.as_str());
    let adapter_request_type: Option<String> = adapter
        .and_then(|a| a.request_type.as_deref())
        .map(|rt| rt.rsplit("::").next().unwrap_or(rt).to_string());

    // setup_lines holds per-test statements that must precede the main call:
    // engine construction (handle args) and URL building (mock_url args).
    let mut setup_lines: Vec<String> = Vec::new();
    let mut args = Vec::new();

    for arg_def in fixture.resolved_args(call_config) {
        match arg_def.arg_type.as_str() {
            "mock_url" => {
                let name = arg_def.name.clone();
                if fixture.has_host_root_route() {
                    let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
                    setup_lines.push(format!(
                        r#"final {name} = _mockServerFor("{fixture_id}", "{env_key}");"#
                    ));
                } else {
                    setup_lines.push(format!(
                        r#"final {name} = "${{_mockServerUrl()}}/fixtures/{fixture_id}";"#
                    ));
                }
                // For streaming adapters with a request_type, wrap the URL in the request constructor.
                if let Some(ref req_type) = adapter_request_type {
                    let req_var = format!("{}Req", name);
                    // Extract just the type name (last segment after ::).
                    let req_type_name = req_type.rsplit("::").next().unwrap_or(req_type.as_str());
                    setup_lines.push(format!("final {req_var} = {req_type_name}(url: {name});"));
                    args.push(req_var);
                } else {
                    args.push(name);
                }
                continue;
            }
            "handle" => {
                let name = arg_def.name.clone();
                let field = arg_def.field.strip_prefix("input.").unwrap_or(&arg_def.field);
                let config_value = fixture.input.get(field).cloned().unwrap_or(serde_json::Value::Null);
                // Derive the create-function name: "engine" → "createEngine".
                let create_fn = {
                    let mut chars = name.chars();
                    let pascal = match chars.next() {
                        None => String::new(),
                        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
                    };
                    format!("create{pascal}")
                };
                if config_value.is_null()
                    || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
                {
                    setup_lines.push(format!("final {name} = await {bridge_class}.{create_fn}();"));
                } else {
                    let json_str = serde_json::to_string(&config_value).unwrap_or_default();
                    let config_var = format!("{name}Config");
                    // FRB-generated free function: `createCrawlConfigFromJson(json: '...')` — async,
                    // deserializes the JSON into the mirror struct via the Rust `create_<type>_from_json`
                    // helper emitted by the dart backend. This avoids relying on a Dart-side `fromJson`
                    // constructor (FRB classes don't expose one).
                    setup_lines.push(format!(
                        "final {config_var} = await createCrawlConfigFromJson(json: r'{json_str}');"
                    ));
                    // Facade exposes `createEngine` with a positional-optional `[ConfigType? config]`
                    // parameter (see dart `gen_bindings/functions.rs::format_param` for handles),
                    // so the call site uses positional-arg syntax — `createEngine(cfg)`, not
                    // `createEngine(config: cfg)` (the latter raises "No named parameter 'config'").
                    setup_lines.push(format!(
                        "final {name} = await {bridge_class}.{create_fn}({config_var});"
                    ));
                }
                args.push(name);
                continue;
            }
            "mock_url_list" => {
                // List<String> of URLs: each element is either a bare path (`/seed1`) — prefixed
                // with the per-fixture mock-server URL at runtime — or an absolute URL kept as-is.
                // Mirrors the `mock_url` resolution: `MOCK_SERVER_<FIXTURE_ID>` first, then
                // `MOCK_SERVER_URL/fixtures/<id>`.
                let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
                let field = arg_def.field.strip_prefix("input.").unwrap_or(&arg_def.field);
                let val = fixture.input.get(field).unwrap_or(&serde_json::Value::Null);

                let paths: Vec<String> = if let Some(arr) = val.as_array() {
                    arr.iter()
                        .filter_map(|v| v.as_str())
                        .map(|s| format!("'{}'", escape_dart(s)))
                        .collect()
                } else {
                    Vec::new()
                };

                let var_name = &arg_def.name;
                let paths_literal = paths.join(", ");

                setup_lines.push(format!(
                    r#"final {var_name}Base = _mockServerFor("{fixture_id}", "{env_key}");"#
                ));
                setup_lines.push(format!(
                    r#"final {var_name} = <String>[{paths_literal}].map((p) => p.startsWith('http') ? p : {var_name}Base + p).toList();"#
                ));

                // For streaming adapters with a request_type, wrap the URL list in the request constructor.
                if let Some(ref req_type) = adapter_request_type {
                    let req_var = format!("{}Req", var_name);
                    // Extract just the type name (last segment after ::).
                    let req_type_name = req_type.rsplit("::").next().unwrap_or(req_type.as_str());
                    setup_lines.push(format!("final {req_var} = {req_type_name}(urls: {var_name});"));
                    args.push(req_var);
                } else {
                    args.push(var_name.to_string());
                }
                continue;
            }
            "test_backend" => {
                if let Some(trait_name) = &arg_def.trait_name {
                    if let Some(trait_bridge) = config.trait_bridges.iter().find(|tb| tb.trait_name == *trait_name) {
                        let methods: Vec<&crate::core::ir::MethodDef> = type_defs
                            .iter()
                            .find(|t| t.name == *trait_name)
                            .map(|t| t.methods.iter().collect())
                            .unwrap_or_default();
                        let emission = crate::e2e::codegen::emit_test_backend("dart", trait_bridge, &methods, fixture);
                        // Dart class definitions are emitted at module-level (before void main)
                        // in collect_dart_test_stub_classes, so we only push the instantiation here.
                        args.push(emission.arg_expr);
                        continue;
                    }
                }
                let emission = crate::e2e::codegen::TestBackendEmission::unimplemented("dart");
                setup_lines.push(format!("// {}", emission.arg_expr));
                args.push("null".to_string());
                continue;
            }
            _ => {}
        }

        let arg_value = resolve_field(&fixture.input, &arg_def.field);
        match arg_def.arg_type.as_str() {
            "bytes" | "file_path" => {
                // `bytes`: value is a file path string; load file contents at test-run time.
                // `file_path`: also loaded as bytes for dart — extractBytes/extractBytesSync is
                // the idiomatic Dart API since the Dart runtime cannot pass OS-level file paths
                // through the FFI bridge.
                if let serde_json::Value::String(file_path) = arg_value {
                    args.push(format!("File('{}').readAsBytesSync()", file_path));
                }
            }
            "int" | "integer" | "i64" => {
                // Scalar integer argument: emit the numeric value as-is (positional required).
                match arg_value {
                    serde_json::Value::Number(n) => {
                        args.push(n.to_string());
                    }
                    serde_json::Value::Null if arg_def.optional => {
                        // Optional int absent: omit it.
                    }
                    _ => {
                        // Required int with no fixture value: emit 0 as default.
                        args.push("0".to_string());
                    }
                }
            }
            "float" | "number" => {
                // Scalar float/number argument: emit the numeric value as-is (positional required).
                match arg_value {
                    serde_json::Value::Number(n) => {
                        args.push(n.to_string());
                    }
                    serde_json::Value::Null if arg_def.optional => {
                        // Optional float absent: omit it.
                    }
                    _ => {
                        // Required float with no fixture value: emit 0.0 as default.
                        args.push("0.0".to_string());
                    }
                }
            }
            "bool" | "boolean" => {
                // Scalar boolean argument: emit the boolean value as-is (positional required).
                match arg_value {
                    serde_json::Value::Bool(b) => {
                        args.push(if *b { "true" } else { "false" }.to_string());
                    }
                    serde_json::Value::Null if arg_def.optional => {
                        // Optional bool absent: omit it.
                    }
                    _ => {
                        // Required bool with no fixture value: emit false as default.
                        args.push("false".to_string());
                    }
                }
            }
            "string" => {
                // Polyglot repos expose their Dart surface through a hand-written facade
                // (e.g. `SampleMarkupBridge.convert(String html, {ConversionOptions? options})`,
                // `SampleLanguagePackBridge.process(String source, ProcessConfig config)`,
                // `SampleCrateBridge.extractBytes(Uint8List content, String mimeType, [ExtractionConfig? config])`)
                // that wraps the FRB-generated bridge methods. Those facades follow the
                // Rust idiom: required args are positional, optional args are named with
                // defaults. The "always emit named" heuristic targets the raw FRB bridge
                // call site but breaks every hand-written facade.
                //
                // Mirror the policy used by the `json_object` handler below: required →
                // positional, optional → named. Sample-llm's `chat`/`embed` calls are
                // unaffected because they route through the `from_json` path (which
                // always emits `req:` named) and the `client_factory` path (which
                // hardcodes its own arg shape).
                let dart_param_name = snake_to_camel(&arg_def.name);
                // The `mime_type` parameter is a *positional* parameter in every facade
                // extract method — both `extractBytes`/`extractBytesSync` (where it is a
                // required `String mimeType`) and `extractFile`/`extractFileSync` (where
                // it is a required-but-nullable `String? mimeType`). It always precedes
                // the trailing optional `[ExtractionConfig? config]`, so it can never be
                // a Dart named parameter. The `optional` flag on the call config marks it
                // optional only because `extract_file`'s Rust signature takes
                // `Option<&str>`; it does not change the Dart facade's positional shape.
                // Only the `client_factory` FRB path declares non-`config` params named.
                let mime_type_is_positional = arg_def.name == "mime_type" && client_factory_for_args.is_none();
                match arg_value {
                    serde_json::Value::String(s) => {
                        let literal = format!("'{}'", escape_dart(s));
                        // FRB-generated client methods (the `client_factory` path, e.g.
                        // sample-llm's `retrieveFile({required String fileId})`) declare
                        // every non-`config` parameter as named-required, so required
                        // string args must be passed with a `name:` label too. Facade
                        // methods (no `client_factory`) keep required args positional.
                        if (arg_def.optional || client_factory_for_args.is_some()) && !mime_type_is_positional {
                            args.push(format!("{dart_param_name}: {literal}"));
                        } else {
                            args.push(literal);
                        }
                    }
                    serde_json::Value::Null
                        if arg_def.optional
                        // Optional string absent from fixture — try to infer MIME from path
                        // when the arg name looks like a MIME-type parameter.
                        && arg_def.name == "mime_type" =>
                    {
                        let inferred = file_path_for_mime
                            .and_then(mime_from_extension)
                            .unwrap_or("application/octet-stream");
                        // Emit positionally for the facade path (see above); only the
                        // `client_factory` FRB path uses the `mimeType:` named label.
                        if mime_type_is_positional {
                            args.push(format!("'{inferred}'"));
                        } else {
                            args.push(format!("{dart_param_name}: '{inferred}'"));
                        }
                    }
                    // Other optional strings with null value are omitted.
                    _ => {}
                }
            }
            "json_object" => {
                // Handle batch item arrays (BatchBytesItem / BatchFileItem).
                if let Some(elem_type) = &arg_def.element_type {
                    if (elem_type == "BatchBytesItem" || elem_type == "BatchFileItem") && arg_value.is_array() {
                        let dart_items = emit_dart_batch_item_array(arg_value, elem_type);
                        args.push(dart_items);
                    } else if elem_type == "String" && arg_value.is_array() {
                        // Scalar string array (e.g. `texts: ["a", "b"]` for embed_texts).
                        // The `SampleCrateBridge` facade declares these parameters as required
                        // positional (e.g. `embedTexts(List<String> texts, EmbeddingConfig config)`),
                        // so the list literal must be passed positionally — matching the
                        // facade contract rather than the underlying FRB bridge's named-arg
                        // convention.
                        let items: Vec<String> = arg_value
                            .as_array()
                            .unwrap()
                            .iter()
                            .filter_map(|v| v.as_str())
                            .map(|s| format!("'{}'", escape_dart(s)))
                            .collect();
                        args.push(format!("<String>[{}]", items.join(", ")));
                    } else if arg_value.is_array() {
                        // Generic typed array (e.g. `actions: [PageAction]` for interact).
                        // Decode via jsonDecode at test-run time and convert to typed instances.
                        let json_str = serde_json::to_string(&arg_value).unwrap_or_default();
                        let var_name = arg_def.name.clone();
                        if elem_type == "PageAction" {
                            setup_lines.push(format!(
                                "final {var_name} = (jsonDecode(r'{json_str}') as List<dynamic>).map((e) => _parsePageAction(e as Map<String, dynamic>)).toList();"
                            ));
                        } else {
                            setup_lines.push(format!(
                                "final {var_name} = (jsonDecode(r'{json_str}') as List<dynamic>).cast<Map<String, dynamic>>();"
                            ));
                        }
                        args.push(var_name);
                    }
                } else if options_via == "from_json" {
                    // `from_json` path: construct a typed mirror-struct via the generated
                    // `create<TypeName>FromJson(json: '...')` bridge helper, then pass it
                    // as the named FRB parameter `req: _var`.
                    //
                    // The helper is generated by `emit_from_json_fn` in the dart bridge-crate
                    // generator and made available as a top-level function via the exported
                    // `sample_llm_bridge_generated/lib.dart`. The parameter name used in the
                    // bridge method call is always `req:` for single-request-object methods
                    // (derived from the Rust IR param name).
                    if let Some(opts_type) = options_type {
                        if !arg_value.is_null() {
                            let json_str = serde_json::to_string(&arg_value).unwrap_or_default();
                            // Escape for Dart single-quoted string literal (handles embedded quotes,
                            // backslashes, and interpolation markers).
                            let escaped_json = escape_dart(&json_str);
                            let var_name = format!("_{}", arg_def.name);
                            let dart_fn = type_name_to_create_from_json_dart(opts_type);
                            setup_lines.push(format!("final {var_name} = await {dart_fn}(json: '{escaped_json}');"));
                            // FRB bridge method param name is `req` for all single-request methods.
                            // Use `req:` as the named argument label.
                            args.push(format!("req: {var_name}"));
                        }
                    }
                } else if arg_def.name == "config" {
                    if let serde_json::Value::Object(map) = &arg_value {
                        if !map.is_empty() {
                            // When the call override specifies a non-default `options_type`
                            // (e.g. `EmbeddingConfig` for `embed_texts`), or the override map
                            // contains a non-scalar field that the literal `ExtractionConfig`
                            // constructor cannot express (e.g. `output_format: "markdown"` is
                            // a tagged enum, not a plain string), fall back to the
                            // FRB-generated `create<Type>FromJson(json: '...')` helper which
                            // round-trips the JSON through serde and so preserves enum tags,
                            // nested configs, and string-valued enum variants verbatim.
                            let explicit_options =
                                options_type.is_some_and(|t| t != "ExtractionConfig" && t != "FileExtractionConfig");
                            let has_non_scalar = map.values().any(|v| {
                                matches!(
                                    v,
                                    serde_json::Value::String(_)
                                        | serde_json::Value::Object(_)
                                        | serde_json::Value::Array(_)
                                )
                            });
                            if explicit_options || has_non_scalar {
                                let opts_type = options_type.unwrap_or("ExtractionConfig");
                                let json_str = serde_json::to_string(&arg_value).unwrap_or_default();
                                let escaped_json = escape_dart(&json_str);
                                let var_name = format!("_{}", arg_def.name);
                                let dart_fn = type_name_to_create_from_json_dart(opts_type);
                                setup_lines
                                    .push(format!("final {var_name} = await {dart_fn}(json: '{escaped_json}');"));
                                args.push(var_name);
                            } else {
                                // Fixture provides scalar-only overrides — build an
                                // `ExtractionConfig` constructor literal with defaults,
                                // overriding only the bool/int fields present in the
                                // fixture JSON. Handles configs such as
                                // {force_ocr:true, disable_ocr:true} that toggle error paths.
                                args.push(emit_extraction_config_dart(map));
                            }
                        } else {
                            // Empty config object: construct a default instance via FRB's
                            // `create<Type>FromJson(json: '{}')` helper (supports all
                            // config types, not just ExtractionConfig). This ensures the
                            // call signature matches the binding, which expects a required
                            // config parameter even when all fields use their defaults.
                            if let Some(opts_type) = options_type {
                                let var_name = format!("_{}", arg_def.name);
                                let dart_fn = type_name_to_create_from_json_dart(opts_type);
                                setup_lines.push(format!("final {var_name} = await {dart_fn}(json: '{{}}');"));
                                args.push(var_name);
                            }
                        }
                    } else if arg_def.optional {
                        // Fixture has no config block (null/absent) but the Dart facade
                        // declares the arg as a required-positional non-nullable type
                        // (e.g. `embed_texts_async(texts, config)` with `EmbeddingConfig`).
                        // Construct a default instance via FRB's
                        // `create<Type>FromJson(json: '{}')` helper. Skip when no
                        // `options_type` is configured — those calls go through the
                        // `ExtractionConfig?` nullable facade path where omission is allowed.
                        if let Some(opts_type) = options_type {
                            let var_name = format!("_{}", arg_def.name);
                            let dart_fn = type_name_to_create_from_json_dart(opts_type);
                            setup_lines.push(format!("final {var_name} = await {dart_fn}(json: '{{}}');"));
                            args.push(var_name);
                        }
                    }
                } else if arg_value.is_array() {
                    // Generic JSON array (e.g. batch_urls: ["/page1", "/page2"]).
                    // Decode via jsonDecode and cast to List<String> at test-run time.
                    let json_str = serde_json::to_string(&arg_value).unwrap_or_default();
                    let var_name = arg_def.name.clone();
                    setup_lines.push(format!(
                        "final {var_name} = (jsonDecode(r'{json_str}') as List<dynamic>).cast<String>();"
                    ));
                    args.push(var_name);
                } else if let serde_json::Value::Object(map) = &arg_value {
                    // Generic options-style json_object arg (e.g. sample-markdown's
                    // `options: ConversionOptions` on `convert(html, options)`). When the
                    // fixture provides input.options and the call config declares an
                    // `options_type`, build the mirror struct via the FRB-generated
                    // `create<OptionsType>FromJson(json: '...')` helper. Use the arg's
                    // original name (e.g. `options`) as the named parameter label.
                    //
                    // When the fixture also carries a visitor spec, swap to the
                    // `create<OptionsType>FromJsonWithVisitor(json, visitor)` helper
                    // (emitted by `alef-backend-dart` for trait bridges with `type_alias`
                    // + `options_field` binding). The `_visitor` variable is materialised
                    // in the visitor block below — its setup line is inserted ahead of
                    // this options call by `build_dart_visitor`.
                    if !map.is_empty() {
                        if let Some(opts_type) = options_type {
                            let json_str = serde_json::to_string(&arg_value).unwrap_or_default();
                            let escaped_json = escape_dart(&json_str);
                            let dart_param_name = snake_to_camel(&arg_def.name);
                            let var_name = format!("_{}", arg_def.name);
                            let dart_fn = type_name_to_create_from_json_dart(opts_type);
                            if fixture.visitor.is_some() {
                                setup_lines.push(format!(
                                    "final {var_name} = await {dart_fn}WithVisitor(json: '{escaped_json}', visitor: _visitor);"
                                ));
                            } else {
                                setup_lines
                                    .push(format!("final {var_name} = await {dart_fn}(json: '{escaped_json}');"));
                            }
                            if arg_def.optional {
                                args.push(format!("{dart_param_name}: {var_name}"));
                            } else {
                                args.push(var_name);
                            }
                        }
                    }
                }
            }
            _ => {}
        }
    }

    // Fixture-driven visitor handle. When `fixture.visitor` is set we build a
    // `_visitor` via the `createHtmlVisitor(...)` factory (emitted by
    // `alef-backend-dart`'s trait-bridge generator in the `type_alias` mode)
    // and thread it into the options blob via the
    // `create<OptionsType>FromJsonWithVisitor(json, visitor)` helper (handled
    // a few lines above in the json_object arg branch).
    //
    // The visitor setup line is INSERTED at the front of `setup_lines` so
    // `_visitor` is defined before any `_options` line that references it.
    // Fixtures without an `options` json_object in input still need an options
    // blob to carry the visitor through to convert — we synthesise an empty-
    // options call to `createConversionOptionsFromJsonWithVisitor(json: '{}',
    // visitor: _visitor)` here when no `options` arg was emitted in the loop
    // above.
    if let Some(visitor_spec) = &fixture.visitor {
        let mut visitor_setup: Vec<String> = Vec::new();
        let _ = super::dart_visitors::build_dart_visitor(&mut visitor_setup, visitor_spec);
        // Prepend the visitor block so `_visitor` is in scope by the time the
        // options call (which may reference it) runs.
        for line in visitor_setup.into_iter().rev() {
            setup_lines.insert(0, line);
        }

        // If no `options` arg was emitted by the loop above (the fixture has no
        // input.options block), build an empty options-with-visitor and add it as
        // an `options:` named arg so the visitor reaches the convert call.
        let already_has_options = args.iter().any(|a| a.starts_with("options:") || a == "_options");
        if !already_has_options {
            if let Some(opts_type) = options_type {
                let dart_fn = type_name_to_create_from_json_dart(opts_type);
                setup_lines.push(format!(
                    "final _options = await {dart_fn}WithVisitor(json: '{{}}', visitor: _visitor);"
                ));
                args.push("options: _options".to_string());
            }
        }
    }

    // Resolve client_factory: when set, tests create a client instance and call
    // methods on it rather than using static bridge-class calls. This mirrors the
    // go/python/zig pattern for stateful clients (e.g. sample-llm).
    let client_factory: Option<&str> = 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())
    });

    // Convert factory name to camelCase (same rule as function_name above).
    let client_factory_camel: Option<String> = client_factory.map(|f| {
        f.split('_')
            .enumerate()
            .map(|(i, part)| {
                if i == 0 {
                    part.to_string()
                } else {
                    let mut chars = part.chars();
                    match chars.next() {
                        None => String::new(),
                        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
                    }
                }
            })
            .collect::<Vec<_>>()
            .join("")
    });

    // All bridge methods return Future<T> because FRB v2 wraps every Rust
    // function as async in Dart — even "sync" Rust functions. Always emit an async
    // test body and await the call so the test framework waits for the future.
    let _ = writeln!(out, "  test('{description}', () async {{");

    let args_str = args.join(", ");
    let receiver_class = call_overrides
        .and_then(|o| o.class.as_ref())
        .cloned()
        .unwrap_or_else(|| bridge_class.to_string());

    // When client_factory is set, determine the mock URL and emit client instantiation.
    // The mock URL derivation follows the same has_host_root_route / plain-fixture split
    // used by the mock_url arg handler above.
    let (receiver, extra_setup): (String, Option<String>) = if let Some(factory) = &client_factory_camel {
        let has_mock_url = fixture
            .resolved_args(call_config)
            .iter()
            .any(|a| a.arg_type == "mock_url");
        let mock_url_setup = if !has_mock_url {
            // No explicit mock_url arg — derive the URL inline.
            if fixture.has_host_root_route() {
                let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
                Some(format!(
                    "final _mockUrl = _mockServerFor(\"{fixture_id}\", \"{env_key}\");"
                ))
            } else {
                Some(format!(
                    r#"final _mockUrl = "${{_mockServerUrl()}}/fixtures/{fixture_id}";"#
                ))
            }
        } else {
            None
        };
        let url_expr = if has_mock_url {
            // A mock_url arg was emitted into setup_lines already — reuse the variable name
            // from the first mock_url arg definition so we don't duplicate the URL.
            call_config
                .args
                .iter()
                .find(|a| a.arg_type == "mock_url")
                .map(|a| a.name.clone())
                .unwrap_or_else(|| "_mockUrl".to_string())
        } else {
            "_mockUrl".to_string()
        };
        let create_line = format!("final _client = await {receiver_class}.{factory}('test-key', baseUrl: {url_expr});");
        let full_setup = if let Some(url_line) = mock_url_setup {
            Some(format!("{url_line}\n    {create_line}"))
        } else {
            Some(create_line)
        };
        ("_client".to_string(), full_setup)
    } else {
        (receiver_class.clone(), None)
    };

    if expects_error && (!setup_lines.is_empty() || extra_setup.is_some()) {
        // Wrap setup + call in an async lambda so any exception at any step is caught.
        // flutter_rust_bridge 2.x decodes Rust errors as raw String values (not Exception
        // subtypes), so throwsException will not match. Use throwsA(anything) instead.
        let _ = writeln!(out, "    await expectLater(() async {{");
        for line in &setup_lines {
            // Handle multi-line setup blocks (e.g., class definitions from emit_test_backend).
            // Each embedded newline in `line` needs proper indentation.
            for inner_line in line.lines() {
                let _ = writeln!(out, "      {inner_line}");
            }
        }
        if let Some(extra) = &extra_setup {
            for line in extra.lines() {
                let _ = writeln!(out, "      {line}");
            }
        }
        if is_streaming {
            let _ = writeln!(out, "      return {receiver}.{function_name}({args_str}).toList();");
        } else {
            let _ = writeln!(out, "      return {receiver}.{function_name}({args_str});");
        }
        let _ = writeln!(out, "    }}(), throwsA(anything));");
    } else if expects_error {
        // No setup lines, direct call — same throwsA(anything) rationale as above.
        if let Some(extra) = &extra_setup {
            for line in extra.lines() {
                let _ = writeln!(out, "    {line}");
            }
        }
        if is_streaming {
            let _ = writeln!(
                out,
                "    await expectLater({receiver}.{function_name}({args_str}).toList(), throwsA(anything));"
            );
        } else {
            let _ = writeln!(
                out,
                "    await expectLater({receiver}.{function_name}({args_str}), throwsA(anything));"
            );
        }
    } else {
        for line in &setup_lines {
            // Handle multi-line setup blocks (e.g., class definitions from emit_test_backend).
            // Each embedded newline in `line` needs proper indentation.
            for inner_line in line.lines() {
                let _ = writeln!(out, "    {inner_line}");
            }
        }
        if let Some(extra) = &extra_setup {
            for line in extra.lines() {
                let _ = writeln!(out, "    {line}");
            }
        }
        if is_streaming {
            let _ = writeln!(
                out,
                "    final {result_var} = await {receiver}.{function_name}({args_str}).toList();"
            );
        } else {
            let _ = writeln!(
                out,
                "    final {result_var} = await {receiver}.{function_name}({args_str});"
            );
        }
        for assertion in &fixture.assertions {
            if is_streaming {
                render_streaming_assertion_dart(out, assertion, result_var);
            } else {
                render_assertion_dart(
                    out,
                    assertion,
                    result_var,
                    result_is_simple,
                    field_resolver,
                    enum_fields,
                );
            }
        }
    }

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

/// Render `.length` / `?.length ?? 0` against a Dart field accessor.
///
/// Count-style assertions (`count_equals`, `count_min`, `min_length`, `max_length`)
/// operate on collection-typed fields. FRB v2 maps `Option<Vec<T>>` to `List<T>?`
/// (nullable) but `Vec<T>` to `List<T>` (non-null). Emitting `?.length ?? 0`
/// against a non-null receiver triggers `invalid_null_aware_operator`. Inspect
/// the IR via `FieldResolver::is_optional` and choose the safe form per field.
fn dart_length_expr(field_accessor: &str, field: Option<&str>, field_resolver: &FieldResolver) -> String {
    let is_optional = field
        .map(|f| {
            let resolved = field_resolver.resolve(f);
            field_resolver.is_optional(f) || field_resolver.is_optional(resolved)
        })
        .unwrap_or(false);
    if is_optional {
        format!("{field_accessor}?.length ?? 0")
    } else {
        format!("{field_accessor}.length")
    }
}

fn dart_format_value(val: &serde_json::Value) -> String {
    match val {
        serde_json::Value::String(s) => format!("'{}'", escape_dart(s)),
        serde_json::Value::Bool(b) => b.to_string(),
        serde_json::Value::Number(n) => n.to_string(),
        serde_json::Value::Null => "null".to_string(),
        other => format!("'{}'", escape_dart(&other.to_string())),
    }
}

/// Render a single fixture assertion as a Dart `package:test` `expect(...)` call.
///
/// Field paths are converted per-segment to camelCase (FRB v2 convention) using
/// [`field_to_dart_accessor`].  All 24 fixture assertion types are handled.
///
/// Assertions on fixture fields that are not in the configured `result_fields` set
/// are emitted as a `// skipped:` comment instead — the Dart binding may model a
/// different result shape than the fixture asserts on (e.g. flat `ScrapeResult` vs.
/// nested `result.browser.*`), and emitting unresolvable getters would break the
/// whole file at compile time.
fn render_assertion_dart(
    out: &mut String,
    assertion: &Assertion,
    result_var: &str,
    result_is_simple: bool,
    field_resolver: &FieldResolver,
    enum_fields: &std::collections::HashSet<String>,
) {
    // Skip assertions on fields that don't exist on the dart result type. This must run
    // BEFORE the array-traversal and standard accessor paths since both emit code that
    // references the field — an unknown field path produces an `isn't defined` error.
    if !result_is_simple {
        if let Some(f) = assertion.field.as_deref() {
            // Use the head segment (before any `[].`) for validation since `is_valid_for_result`
            // only checks the first path component.
            let head = f.split("[].").next().unwrap_or(f);
            if !head.is_empty() && !field_resolver.is_valid_for_result(head) {
                let _ = writeln!(out, "    // skipped: field '{f}' not available on dart result type");
                return;
            }
        }
    }

    // Skip assertions that traverse a tagged-union variant boundary. FRB exposes
    // tagged unions like `FormatMetadata` as sealed classes whose variants are
    // accessed via pattern matching (`switch (m) { case FormatMetadata_Excel ... }`)
    // — there is no `.excel?` getter, so the fixture path cannot be expressed as
    // a simple chained accessor without language-specific pattern-matching codegen.
    if let Some(f) = assertion.field.as_deref() {
        if !f.is_empty() && field_resolver.tagged_union_split(f).is_some() {
            let _ = writeln!(
                out,
                "    // skipped: field '{f}' crosses a tagged-union variant boundary (not expressible in Dart)"
            );
            return;
        }
    }

    // Handle array traversal (e.g. "links[].link_type" → any() expression).
    if let Some(f) = assertion.field.as_deref() {
        if let Some(dot) = f.find("[].") {
            // Apply the alias mapping to the full `xxx[].yyy` path first so renamed
            // sub-fields (e.g. `assets[].category` → `assets[].asset_category`) resolve
            // correctly. Split *after* resolving so both the array head and the element
            // path reflect any alias rewrites.
            let resolved_full = field_resolver.resolve(f);
            let (array_part, elem_part) = match resolved_full.find("[].") {
                Some(rdot) => (&resolved_full[..rdot], &resolved_full[rdot + 3..]),
                // Resolver mapped the path away from `[].` form — fall back to the original
                // split, since downstream code expects the array/elem structure.
                None => (&f[..dot], &f[dot + 3..]),
            };
            let array_accessor = if array_part.is_empty() {
                result_var.to_string()
            } else {
                field_resolver.accessor(array_part, "dart", result_var)
            };
            let elem_accessor = field_to_dart_accessor(elem_part);
            match assertion.assertion_type.as_str() {
                "contains" => {
                    if let Some(expected) = &assertion.value {
                        let dart_val = dart_format_value(expected);
                        let _ = writeln!(
                            out,
                            "    expect({array_accessor}.any((e) => e.{elem_accessor}.toString().contains({dart_val})), isTrue);"
                        );
                    }
                }
                "contains_all" => {
                    if let Some(values) = &assertion.values {
                        for val in values {
                            let dart_val = dart_format_value(val);
                            let _ = writeln!(
                                out,
                                "    expect({array_accessor}.any((e) => e.{elem_accessor}.toString().contains({dart_val})), isTrue);"
                            );
                        }
                    }
                }
                "not_contains" => {
                    if let Some(expected) = &assertion.value {
                        let dart_val = dart_format_value(expected);
                        let _ = writeln!(
                            out,
                            "    expect({array_accessor}.any((e) => e.{elem_accessor}.toString().contains({dart_val})), isFalse);"
                        );
                    } else if let Some(values) = &assertion.values {
                        for val in values {
                            let dart_val = dart_format_value(val);
                            let _ = writeln!(
                                out,
                                "    expect({array_accessor}.any((e) => e.{elem_accessor}.toString().contains({dart_val})), isFalse);"
                            );
                        }
                    }
                }
                "not_empty" => {
                    let _ = writeln!(
                        out,
                        "    expect({array_accessor}.any((e) => e.{elem_accessor}.toString().isNotEmpty), isTrue);"
                    );
                }
                other => {
                    let _ = writeln!(
                        out,
                        "    // skipped: unsupported traversal assertion '{other}' on '{f}'"
                    );
                }
            }
            return;
        }
    }

    let field_accessor = if result_is_simple {
        // Whole-result assertion path: the dart return is a scalar (e.g. a
        // `Uint8List` for speech/file_content), so any `field` on the
        // assertion resolves to the whole value rather than a sub-accessor.
        result_var.to_string()
    } else {
        match assertion.field.as_deref() {
            // Use the shared accessor builder (`FieldResolver::accessor`) — it applies the
            // alias mapping (e.g. `robots.is_allowed` → `is_allowed`), expands array
            // segments to `[0]` lookups, and injects `!` after optional intermediates so
            // chained access compiles under sound null safety.
            Some(f) if !f.is_empty() => field_resolver.accessor(f, "dart", result_var),
            _ => result_var.to_string(),
        }
    };

    let format_value = |val: &serde_json::Value| -> String { dart_format_value(val) };

    match assertion.assertion_type.as_str() {
        "equals" | "field_equals" => {
            if let Some(expected) = &assertion.value {
                let dart_val = format_value(expected);
                // Check if this field is an enum field. Enum fields need _alefE2eText for serde
                // wire format conversion (e.g. FinishReason.toolCalls → "tool_calls").
                let is_enum_field = assertion
                    .field
                    .as_deref()
                    .map(|f| {
                        let resolved = field_resolver.resolve(f);
                        enum_fields.contains(f) || enum_fields.contains(resolved)
                    })
                    .unwrap_or(false);

                // Match the rust codegen's behaviour: trim both sides for string equality
                // so trailing-newline differences between sample-markdown's emitted markdown and the
                // fixture's expected value don't produce false positives.
                if expected.is_string() {
                    if is_enum_field {
                        // For enum fields, use _alefE2eText to normalize the enum value to its
                        // serde wire format before comparison.
                        let _ = writeln!(
                            out,
                            "    expect(_alefE2eText({field_accessor}).trim(), equals({dart_val}.toString().trim()));"
                        );
                    } else {
                        // When result_is_simple is true and the field_accessor is nullable (e.g. String?),
                        // use null-coalescing operator (?? '') to handle null gracefully.
                        let safe_accessor = if result_is_simple && assertion.field.is_none() {
                            format!("({field_accessor} ?? '').toString().trim()")
                        } else {
                            format!("{field_accessor}.toString().trim()")
                        };
                        let _ = writeln!(
                            out,
                            "    expect({safe_accessor}, equals({dart_val}.toString().trim()));"
                        );
                    }
                } else {
                    let _ = writeln!(out, "    expect({field_accessor}, equals({dart_val}));");
                }
            } else {
                let _ = writeln!(
                    out,
                    "    // skipped: '{}' assertion missing value",
                    assertion.assertion_type
                );
            }
        }
        "not_equals" => {
            if let Some(expected) = &assertion.value {
                let dart_val = format_value(expected);
                // Check if this field is an enum field.
                let is_enum_field = assertion
                    .field
                    .as_deref()
                    .map(|f| {
                        let resolved = field_resolver.resolve(f);
                        enum_fields.contains(f) || enum_fields.contains(resolved)
                    })
                    .unwrap_or(false);

                if expected.is_string() {
                    if is_enum_field {
                        let _ = writeln!(
                            out,
                            "    expect(_alefE2eText({field_accessor}).trim(), isNot(equals({dart_val}.toString().trim())));"
                        );
                    } else {
                        // When result_is_simple is true and the field_accessor is nullable (e.g. String?),
                        // use null-coalescing operator (?? '') to handle null gracefully.
                        let safe_accessor = if result_is_simple && assertion.field.is_none() {
                            format!("({field_accessor} ?? '').toString().trim()")
                        } else {
                            format!("{field_accessor}.toString().trim()")
                        };
                        let _ = writeln!(
                            out,
                            "    expect({safe_accessor}, isNot(equals({dart_val}.toString().trim())));"
                        );
                    }
                } else {
                    let _ = writeln!(out, "    expect({field_accessor}, isNot(equals({dart_val})));");
                }
            }
        }
        "contains" => {
            if let Some(expected) = &assertion.value {
                let dart_val = format_value(expected);
                // Try the "stringy aggregator" path first: when the field is a list of DTOs
                // with multiple text-bearing accessors (e.g. List<ImportInfo> with
                // source/items/alias), emit code that walks every accessor and does
                // substring matching. This avoids the brittle "primary accessor" guess.
                let aggregator = dart_stringy_aggregator_contains_assert(
                    assertion.field.as_deref(),
                    result_var,
                    field_resolver,
                    &dart_val,
                );
                if let Some(line) = aggregator {
                    let _ = writeln!(out, "{line}");
                } else {
                    let _ = writeln!(out, "    expect({field_accessor}, contains({dart_val}));");
                }
            } else {
                let _ = writeln!(out, "    // skipped: 'contains' assertion missing value");
            }
        }
        "contains_all" => {
            if let Some(values) = &assertion.values {
                for val in values {
                    let dart_val = format_value(val);
                    let _ = writeln!(out, "    expect({field_accessor}, contains({dart_val}));");
                }
            }
        }
        "contains_any" => {
            if let Some(values) = &assertion.values {
                let checks: Vec<String> = values
                    .iter()
                    .map(|v| {
                        let dart_val = format_value(v);
                        format!("{field_accessor}.contains({dart_val})")
                    })
                    .collect();
                let joined = checks.join(" || ");
                let _ = writeln!(out, "    expect({joined}, isTrue);");
            }
        }
        "not_contains" => {
            if let Some(expected) = &assertion.value {
                let dart_val = format_value(expected);
                let _ = writeln!(out, "    expect({field_accessor}, isNot(contains({dart_val})));");
            } else if let Some(values) = &assertion.values {
                for val in values {
                    let dart_val = format_value(val);
                    let _ = writeln!(out, "    expect({field_accessor}, isNot(contains({dart_val})));");
                }
            }
        }
        "not_empty" => {
            // `isNotEmpty` only applies to types with a `.isEmpty` getter (collections,
            // strings, maps). For struct-shaped fields (e.g. `document: DocumentStructure`)
            // we instead assert the value is non-null — those types have no notion of
            // "empty" and the fixture intent is "the field is present".
            let is_collection = assertion.field.as_deref().is_some_and(|f| {
                let resolved = field_resolver.resolve(f);
                field_resolver.is_array(f) || field_resolver.is_array(resolved)
            });
            if is_collection {
                let _ = writeln!(out, "    expect({field_accessor}, isNotEmpty);");
            } else {
                let _ = writeln!(out, "    expect({field_accessor}, isNotNull);");
            }
        }
        "is_empty" => {
            // FRB models `Option<String>` / `Option<Vec<T>>` as nullable in Dart. The `isEmpty`
            // matcher throws `NoSuchMethodError` on `null`. Accept `null` as semantically
            // empty by combining `isNull` with `isEmpty` via `anyOf`.
            let _ = writeln!(out, "    expect({field_accessor}, anyOf(isNull, isEmpty));");
        }
        "starts_with" => {
            if let Some(expected) = &assertion.value {
                let dart_val = format_value(expected);
                let _ = writeln!(out, "    expect({field_accessor}, startsWith({dart_val}));");
            }
        }
        "ends_with" => {
            if let Some(expected) = &assertion.value {
                let dart_val = format_value(expected);
                let _ = writeln!(out, "    expect({field_accessor}, endsWith({dart_val}));");
            }
        }
        "min_length" => {
            if let Some(val) = &assertion.value {
                if let Some(n) = val.as_u64() {
                    let length_expr = dart_length_expr(&field_accessor, assertion.field.as_deref(), field_resolver);
                    let _ = writeln!(out, "    expect({length_expr}, greaterThanOrEqualTo({n}));");
                }
            }
        }
        "max_length" => {
            if let Some(val) = &assertion.value {
                if let Some(n) = val.as_u64() {
                    let length_expr = dart_length_expr(&field_accessor, assertion.field.as_deref(), field_resolver);
                    let _ = writeln!(out, "    expect({length_expr}, lessThanOrEqualTo({n}));");
                }
            }
        }
        "count_equals" => {
            if let Some(val) = &assertion.value {
                if let Some(n) = val.as_u64() {
                    let length_expr = dart_length_expr(&field_accessor, assertion.field.as_deref(), field_resolver);
                    let _ = writeln!(out, "    expect({length_expr}, equals({n}));");
                }
            }
        }
        "count_min" => {
            if let Some(val) = &assertion.value {
                if let Some(n) = val.as_u64() {
                    let length_expr = dart_length_expr(&field_accessor, assertion.field.as_deref(), field_resolver);
                    let _ = writeln!(out, "    expect({length_expr}, greaterThanOrEqualTo({n}));");
                }
            }
        }
        "matches_regex" => {
            if let Some(expected) = &assertion.value {
                let dart_val = format_value(expected);
                let _ = writeln!(out, "    expect({field_accessor}, matches(RegExp({dart_val})));");
            }
        }
        "is_true" => {
            let _ = writeln!(out, "    expect({field_accessor}, isTrue);");
        }
        "is_false" => {
            let _ = writeln!(out, "    expect({field_accessor}, isFalse);");
        }
        "greater_than" => {
            if let Some(val) = &assertion.value {
                let dart_val = format_value(val);
                let _ = writeln!(out, "    expect({field_accessor}, greaterThan({dart_val}));");
            }
        }
        "less_than" => {
            if let Some(val) = &assertion.value {
                let dart_val = format_value(val);
                let _ = writeln!(out, "    expect({field_accessor}, lessThan({dart_val}));");
            }
        }
        "greater_than_or_equal" => {
            if let Some(val) = &assertion.value {
                let dart_val = format_value(val);
                let _ = writeln!(out, "    expect({field_accessor}, greaterThanOrEqualTo({dart_val}));");
            }
        }
        "less_than_or_equal" => {
            if let Some(val) = &assertion.value {
                let dart_val = format_value(val);
                let _ = writeln!(out, "    expect({field_accessor}, lessThanOrEqualTo({dart_val}));");
            }
        }
        "not_null" => {
            let _ = writeln!(out, "    expect({field_accessor}, isNotNull);");
        }
        "not_error" => {
            // The `await` already guarantees no thrown error reaches this point — if
            // the call throws, the test fails before reaching here. Don't emit
            // `expect(result, isNotNull)`: for void-returning trait-bridge fns
            // (clear_*) Dart rejects `expect(<void>, ...)` with "expression has type
            // 'void' and can't be used". The implicit exception handling proves
            // success.
        }
        "error" => {
            // Handled at the test method level via throwsA(anything).
        }
        "method_result" => {
            if let Some(method) = &assertion.method {
                let dart_method = method.to_lower_camel_case();
                let check = assertion.check.as_deref().unwrap_or("not_null");
                let method_call = format!("{field_accessor}.{dart_method}()");
                match check {
                    "equals" => {
                        if let Some(expected) = &assertion.value {
                            let dart_val = format_value(expected);
                            let _ = writeln!(out, "    expect({method_call}, equals({dart_val}));");
                        }
                    }
                    "is_true" => {
                        let _ = writeln!(out, "    expect({method_call}, isTrue);");
                    }
                    "is_false" => {
                        let _ = writeln!(out, "    expect({method_call}, isFalse);");
                    }
                    "greater_than_or_equal" => {
                        if let Some(val) = &assertion.value {
                            let dart_val = format_value(val);
                            let _ = writeln!(out, "    expect({method_call}, greaterThanOrEqualTo({dart_val}));");
                        }
                    }
                    "count_min" => {
                        if let Some(val) = &assertion.value {
                            if let Some(n) = val.as_u64() {
                                let _ = writeln!(out, "    expect({method_call}.length, greaterThanOrEqualTo({n}));");
                            }
                        }
                    }
                    _ => {
                        let _ = writeln!(out, "    expect({method_call}, isNotNull);");
                    }
                }
            }
        }
        other => {
            let _ = writeln!(out, "    // skipped: unknown assertion type '{other}'");
        }
    }
}

/// Render a single fixture assertion for a streaming result.
///
/// `result_var` is the `List<T>` collected via `.toList()` on the stream.
/// Supports:
/// - `not_error`: `expect(result, isNotNull)` (a thrown error would already fail
///   the test; the explicit expect keeps the test body non-empty).
/// - `count_min` with `field = "chunks"`: assert `result_var.length >= value`.
/// - `equals` with `field = "stream_content"`: concatenate `delta.content` and compare.
///
/// Other assertion types are emitted as comments.
fn render_streaming_assertion_dart(out: &mut String, assertion: &Assertion, result_var: &str) {
    match assertion.assertion_type.as_str() {
        "not_error" => {
            // `.toList()` would have thrown to fail the test on error; emit an
            // explicit `expect` so the test body isn't empty and the collected
            // stream variable is consumed.
            let _ = writeln!(out, "    expect({result_var}, isNotNull);");
        }
        "count_min" if assertion.field.as_deref() == Some("chunks") => {
            if let Some(serde_json::Value::Number(n)) = &assertion.value {
                let _ = writeln!(out, "    expect({result_var}.length, greaterThanOrEqualTo({n}));");
            }
        }
        "equals" if assertion.field.as_deref() == Some("stream_content") => {
            if let Some(serde_json::Value::String(expected)) = &assertion.value {
                let escaped = escape_dart(expected);
                let _ = writeln!(
                    out,
                    "    final _content = {result_var}.map((c) => c.choices.firstOrNull?.delta.content ?? '').join();"
                );
                let _ = writeln!(out, "    expect(_content, equals('{escaped}'));");
            }
        }
        other => {
            let _ = writeln!(out, "    // skipped streaming assertion: '{other}'");
        }
    }
}

/// Converts a snake_case JSON key to Dart camelCase.
fn snake_to_camel(s: &str) -> String {
    let mut result = String::with_capacity(s.len());
    let mut next_upper = false;
    for ch in s.chars() {
        if ch == '_' {
            next_upper = true;
        } else if next_upper {
            result.extend(ch.to_uppercase());
            next_upper = false;
        } else {
            result.push(ch);
        }
    }
    result
}

/// Convert a dot-separated fixture field path to a Dart accessor expression.
///
/// Each segment is converted to camelCase (FRB v2 convention); array-index brackets
/// (e.g. `choices[0]`) and map-key brackets (e.g. `tags[name]`) are preserved.
/// This replaces the former single-pass `snake_to_camel` call which incorrectly
/// treated the entire path string as one identifier.
///
/// Examples:
/// - `"choices"` → `"choices"`
/// - `"choices[0].message.content"` → `"choices[0].message.content"`
/// - `"metadata.document_title"` → `"metadata.documentTitle"`
/// - `"model_id"` → `"modelId"`
fn field_to_dart_accessor(path: &str) -> String {
    let mut result = String::with_capacity(path.len());
    for (i, segment) in path.split('.').enumerate() {
        if i > 0 {
            result.push('.');
        }
        // Separate a trailing `[...]` bracket from the field name so we only
        // camelCase the identifier part, not the bracket content. The owning
        // collection may be `List<T>?` when the underlying Rust field is
        // `Option<Vec<T>>`; force-unwrap with `!` so the `[N]` lookup and any
        // subsequent member access compile under sound null safety.
        if let Some(bracket_pos) = segment.find('[') {
            let name = &segment[..bracket_pos];
            let bracket = &segment[bracket_pos..];
            result.push_str(&name.to_lower_camel_case());
            result.push('!');
            result.push_str(bracket);
        } else {
            result.push_str(&segment.to_lower_camel_case());
        }
    }
    result
}

/// Emits a Dart `ExtractionConfig(...)` constructor with default values, overriding
/// fields present in `overrides` (from fixture JSON, snake_case keys).
///
/// Only simple scalar overrides (bool, int) are supported. Complex nested types
/// (ocr, chunking, etc.) are left at their defaults (null).
fn emit_extraction_config_dart(overrides: &serde_json::Map<String, serde_json::Value>) -> String {
    // Collect scalar overrides; convert keys to camelCase.
    let mut field_overrides: std::collections::HashMap<String, String> = std::collections::HashMap::new();
    for (key, val) in overrides {
        let camel = snake_to_camel(key);
        let dart_val = match val {
            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::String(s) => format!("'{s}'"),
            _ => continue, // skip complex nested objects
        };
        field_overrides.insert(camel, dart_val);
    }

    let use_cache = field_overrides.remove("useCache").unwrap_or_else(|| "true".to_string());
    let enable_quality_processing = field_overrides
        .remove("enableQualityProcessing")
        .unwrap_or_else(|| "true".to_string());
    let force_ocr = field_overrides
        .remove("forceOcr")
        .unwrap_or_else(|| "false".to_string());
    let disable_ocr = field_overrides
        .remove("disableOcr")
        .unwrap_or_else(|| "false".to_string());
    let include_document_structure = field_overrides
        .remove("includeDocumentStructure")
        .unwrap_or_else(|| "false".to_string());
    let use_layout_for_markdown = field_overrides
        .remove("useLayoutForMarkdown")
        .unwrap_or_else(|| "false".to_string());
    let max_archive_depth = field_overrides
        .remove("maxArchiveDepth")
        .unwrap_or_else(|| "3".to_string());

    format!(
        "ExtractionConfig(useCache: {use_cache}, enableQualityProcessing: {enable_quality_processing}, forceOcr: {force_ocr}, disableOcr: {disable_ocr}, resultFormat: ResultFormat.unified, outputFormat: OutputFormat.plain(), includeDocumentStructure: {include_document_structure}, useLayoutForMarkdown: {use_layout_for_markdown}, maxArchiveDepth: {max_archive_depth})"
    )
}

// ---------------------------------------------------------------------------
// HTTP server test rendering — DartTestClientRenderer impl + thin driver wrapper
// ---------------------------------------------------------------------------

/// Renderer that emits `package:test` `test(...)` blocks using `dart:io HttpClient`
/// against the mock server (`Platform.environment['MOCK_SERVER_URL']`).
///
/// Skipped tests are emitted as self-contained stubs (complete test block with
/// `markTestSkipped`) entirely inside `render_test_open`. `render_test_close` uses
/// `in_skip` to emit the right closing token: nothing extra for skip stubs (already
/// closed) vs. `})));` for regular tests.
///
/// `is_redirect` must be set to `true` before invoking the shared driver for 3xx
/// fixtures so that `render_call` can inject `ioReq.followRedirects = false` after
/// the `openUrl` call.
struct DartTestClientRenderer {
    /// Set to `true` when `render_test_open` is called with a skip reason so that
    /// `render_test_close` can match the opening shape.
    in_skip: Cell<bool>,
    /// Pre-set to `true` by the thin wrapper when the fixture expects a 3xx response.
    /// `render_call` injects `ioReq.followRedirects = false` when this is `true`.
    is_redirect: Cell<bool>,
}

impl DartTestClientRenderer {
    fn new(is_redirect: bool) -> Self {
        Self {
            in_skip: Cell::new(false),
            is_redirect: Cell::new(is_redirect),
        }
    }
}

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

    /// Emit the test opening.
    ///
    /// For skipped fixtures: emit the entire self-contained stub (open + body +
    /// close + blank line) and set `in_skip = true` so `render_test_close` is a
    /// no-op.
    ///
    /// For active fixtures: emit `test('desc', () => _serialized(() => _withRetry(() async {`
    /// leaving the block open for the assertion primitives.
    fn render_test_open(&self, out: &mut String, _fn_name: &str, description: &str, skip_reason: Option<&str>) {
        let escaped_desc = escape_dart(description);
        if let Some(reason) = skip_reason {
            let escaped_reason = escape_dart(reason);
            let _ = writeln!(out, "  test('{escaped_desc}', () {{");
            let _ = writeln!(out, "    markTestSkipped('{escaped_reason}');");
            let _ = writeln!(out, "  }});");
            let _ = writeln!(out);
            self.in_skip.set(true);
        } else {
            let _ = writeln!(
                out,
                "  test('{escaped_desc}', () => _serialized(() => _withRetry(() async {{"
            );
            self.in_skip.set(false);
        }
    }

    /// Emit the test closing token.
    ///
    /// No-op for skip stubs (the stub was fully closed in `render_test_open`).
    /// Emits `})));` followed by a blank line for regular tests.
    fn render_test_close(&self, out: &mut String) {
        if self.in_skip.get() {
            // Stub was already closed in render_test_open.
            return;
        }
        let _ = writeln!(out, "  }})));");
        let _ = writeln!(out);
    }

    /// Emit the full `dart:io HttpClient` request scaffolding.
    ///
    /// Emits:
    /// - URL construction from `MOCK_SERVER_URL`.
    /// - `_httpClient.openUrl(method, uri)`.
    /// - `followRedirects = false` when `is_redirect` is pre-set on the renderer.
    /// - Content-Type header, request headers, cookies, optional body bytes.
    /// - `ioReq.contentLength` when a body is present (avoids chunked encoding).
    /// - `ioReq.close()` → `ioResp`.
    /// - Response-body drain into `bodyStr` (always emitted, including for 3xx).
    fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
        // dart:io restricted headers (handled automatically by the HTTP stack).
        const DART_RESTRICTED_HEADERS: &[&str] = &["content-length", "host", "transfer-encoding"];

        let method = ctx.method.to_uppercase();
        let escaped_method = escape_dart(&method);

        // Fixture path is `/fixtures/<id>` — extract the id portion for URL construction.
        let fixture_path = escape_dart(ctx.path);

        // Determine effective content-type.
        let has_explicit_content_type = ctx.headers.keys().any(|k| k.to_lowercase() == "content-type");
        let effective_content_type = if has_explicit_content_type {
            ctx.headers
                .iter()
                .find(|(k, _)| k.to_lowercase() == "content-type")
                .map(|(_, v)| v.as_str())
                .unwrap_or("application/json")
        } else if ctx.body.is_some() {
            ctx.content_type.unwrap_or("application/json")
        } else {
            ""
        };

        let _ = writeln!(out, "    final baseUrl = _mockServerUrl();");
        let _ = writeln!(out, "    final uri = Uri.parse('$baseUrl{fixture_path}');");
        let _ = writeln!(
            out,
            "    final ioReq = await _httpClient.openUrl('{escaped_method}', uri);"
        );

        // Use a fresh (non-persistent) connection per request. Dart's HttpClient keeps
        // connections alive and reuses them; when the mock server closes an idle keep-alive
        // socket, the next reused request races into a "Connection reset by peer". Disabling
        // persistence trades a little speed for deterministic, reset-free runs.
        let _ = writeln!(out, "    ioReq.persistentConnection = false;");

        // Disable automatic redirect following for 3xx fixtures so the test can
        // assert on the redirect status code itself.
        if self.is_redirect.get() {
            let _ = writeln!(out, "    ioReq.followRedirects = false;");
        }

        // Set content-type header.
        if !effective_content_type.is_empty() {
            let escaped_ct = escape_dart(effective_content_type);
            let _ = writeln!(out, "    ioReq.headers.set('content-type', '{escaped_ct}');");
        }

        // Set request headers (skip dart:io restricted headers and content-type, already handled).
        let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
        header_pairs.sort_by_key(|(k, _)| k.as_str());
        for (name, value) in &header_pairs {
            if DART_RESTRICTED_HEADERS.contains(&name.to_lowercase().as_str()) {
                continue;
            }
            if name.to_lowercase() == "content-type" {
                continue; // Already handled above.
            }
            let escaped_name = escape_dart(&name.to_lowercase());
            let escaped_value = escape_dart(value);
            let _ = writeln!(out, "    ioReq.headers.set('{escaped_name}', '{escaped_value}');");
        }

        // Add cookies.
        if !ctx.cookies.is_empty() {
            let mut cookie_pairs: Vec<(&String, &String)> = ctx.cookies.iter().collect();
            cookie_pairs.sort_by_key(|(k, _)| k.as_str());
            let cookie_str: Vec<String> = cookie_pairs.iter().map(|(k, v)| format!("{k}={v}")).collect();
            let cookie_header = escape_dart(&cookie_str.join("; "));
            let _ = writeln!(out, "    ioReq.headers.set('cookie', '{cookie_header}');");
        }

        // Write body bytes if present (bypass charset-based encoding issues).
        // Set contentLength explicitly so Dart sends Content-Length rather than
        // chunked Transfer-Encoding — consistent with Python (urllib) and Go (http)
        // which both set Content-Length automatically. Chunked encoding is valid
        // HTTP/1.1 but some server configurations respond with a connection reset.
        if let Some(body) = ctx.body {
            let json_str = serde_json::to_string(body).unwrap_or_default();
            let escaped = escape_dart(&json_str);
            let _ = writeln!(out, "    final bodyBytes = utf8.encode('{escaped}');");
            let _ = writeln!(out, "    ioReq.contentLength = bodyBytes.length;");
            let _ = writeln!(out, "    ioReq.add(bodyBytes);");
        }

        let _ = writeln!(out, "    final ioResp = await ioReq.close();");
        // Always drain the response body into `bodyStr` so assertion primitives
        // (render_assert_json_body, render_assert_partial_body, etc.) can reference
        // it unconditionally. For 3xx redirect responses with followRedirects=false,
        // the mock server still sends a response body (e.g. `{}`) — draining it is
        // safe and necessary when the fixture has a body assertion.
        let _ = writeln!(out, "    final bodyStr = await ioResp.transform(utf8.decoder).join();");
    }

    fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
        let _ = writeln!(
            out,
            "    expect(ioResp.statusCode, equals({status}), reason: 'status code mismatch');"
        );
    }

    /// Emit a single header assertion, handling special tokens `<<present>>`,
    /// `<<absent>>`, and `<<uuid>>`.
    fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
        let escaped_name = escape_dart(&name.to_lowercase());
        match expected {
            "<<present>>" => {
                let _ = writeln!(
                    out,
                    "    expect(ioResp.headers.value('{escaped_name}'), isNotNull, reason: 'header {escaped_name} should be present');"
                );
            }
            "<<absent>>" => {
                let _ = writeln!(
                    out,
                    "    expect(ioResp.headers.value('{escaped_name}'), isNull, reason: 'header {escaped_name} should be absent');"
                );
            }
            "<<uuid>>" => {
                let _ = writeln!(
                    out,
                    "    expect(ioResp.headers.value('{escaped_name}'), matches(RegExp(r'^[0-9a-f]{{8}}-[0-9a-f]{{4}}-[0-9a-f]{{4}}-[0-9a-f]{{4}}-[0-9a-f]{{12}}$')), reason: 'header {escaped_name} should be a UUID');"
                );
            }
            exact => {
                let escaped_value = escape_dart(exact);
                let _ = writeln!(
                    out,
                    "    expect(ioResp.headers.value('{escaped_name}'), contains('{escaped_value}'), reason: 'header {escaped_name} mismatch');"
                );
            }
        }
    }

    /// Emit an exact-equality body assertion.
    ///
    /// String bodies are compared as decoded text; structured JSON bodies are
    /// compared via `jsonDecode`.
    fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
        match expected {
            serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
                let json_str = serde_json::to_string(expected).unwrap_or_default();
                let escaped = escape_dart(&json_str);
                let _ = writeln!(out, "    final bodyJson = jsonDecode(bodyStr);");
                let _ = writeln!(out, "    final expectedJson = jsonDecode('{escaped}');");
                let _ = writeln!(
                    out,
                    "    expect(bodyJson, equals(expectedJson), reason: 'body mismatch');"
                );
            }
            serde_json::Value::String(s) => {
                let escaped = escape_dart(s);
                let _ = writeln!(
                    out,
                    "    expect(bodyStr.trim(), equals('{escaped}'), reason: 'body mismatch');"
                );
            }
            other => {
                let escaped = escape_dart(&other.to_string());
                let _ = writeln!(
                    out,
                    "    expect(bodyStr.trim(), equals('{escaped}'), reason: 'body mismatch');"
                );
            }
        }
    }

    /// Emit partial-body assertions — every key in `expected` must match the
    /// corresponding field in the parsed JSON response.
    fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
        let _ = writeln!(
            out,
            "    final partialJson = jsonDecode(bodyStr) as Map<String, dynamic>;"
        );
        if let Some(obj) = expected.as_object() {
            for (idx, (key, val)) in obj.iter().enumerate() {
                let escaped_key = escape_dart(key);
                let json_val = serde_json::to_string(val).unwrap_or_default();
                let escaped_val = escape_dart(&json_val);
                // Use an index-based variable name so keys with special characters
                // don't produce invalid Dart identifiers.
                let _ = writeln!(out, "    final _expectedField{idx} = jsonDecode('{escaped_val}');");
                let _ = writeln!(
                    out,
                    "    expect(partialJson['{escaped_key}'], equals(_expectedField{idx}), reason: 'partial body field \\'{escaped_key}\\' mismatch');"
                );
            }
        }
    }

    /// Emit validation-error assertions for 422 responses.
    fn render_assert_validation_errors(
        &self,
        out: &mut String,
        _response_var: &str,
        errors: &[ValidationErrorExpectation],
    ) {
        let _ = writeln!(out, "    final errBody = jsonDecode(bodyStr) as Map<String, dynamic>;");
        let _ = writeln!(out, "    final errList = (errBody['errors'] ?? []) as List<dynamic>;");
        for ve in errors {
            let loc_dart: Vec<String> = ve.loc.iter().map(|s| format!("'{}'", escape_dart(s))).collect();
            let loc_str = loc_dart.join(", ");
            let escaped_msg = escape_dart(&ve.msg);
            let _ = writeln!(
                out,
                "    expect(errList.any((e) => e is Map && (e['loc'] as List?)?.join(',') == [{loc_str}].join(',') && (e['msg'] as String? ?? '').contains('{escaped_msg}')), isTrue, reason: 'validation error not found: {escaped_msg}');"
            );
        }
    }
}

/// Render a `package:test` `test(...)` block for an HTTP server fixture.
///
/// Delegates to the shared [`client::http_call::render_http_test`] driver via
/// [`DartTestClientRenderer`]. HTTP 101 (WebSocket upgrade) fixtures are emitted
/// as skip stubs before reaching the driver because `dart:io HttpClient` cannot
/// handle protocol-switch responses.
fn render_http_test_case(out: &mut String, fixture: &Fixture, http: &HttpFixture) {
    // HTTP 101 (WebSocket upgrade) — dart:io HttpClient cannot handle upgrade responses.
    if http.expected_response.status_code == 101 {
        let description = escape_dart(&fixture.description);
        let _ = writeln!(out, "  test('{description}', () {{");
        let _ = writeln!(
            out,
            "    markTestSkipped('Skipped: Dart HttpClient cannot handle 101 Switching Protocols responses');"
        );
        let _ = writeln!(out, "  }});");
        let _ = writeln!(out);
        return;
    }

    // Pre-set `is_redirect` on the renderer so `render_call` can inject
    // `ioReq.followRedirects = false` for 3xx fixtures. The shared driver has no
    // concept of expected status code so we thread it through renderer state.
    let is_redirect = http.expected_response.status_code / 100 == 3;
    client::http_call::render_http_test(out, &DartTestClientRenderer::new(is_redirect), fixture);
}

/// Infer a MIME type from a file path extension.
///
/// Returns `None` when the extension is unknown so the caller can supply a fallback.
/// Used in dart e2e tests when a fixture omits `mime_type` but uses a `file_path` arg.
fn mime_from_extension(path: &str) -> Option<&'static str> {
    let ext = path.rsplit('.').next()?;
    match ext.to_lowercase().as_str() {
        "docx" => Some("application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
        "xlsx" => Some("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
        "pptx" => Some("application/vnd.openxmlformats-officedocument.presentationml.presentation"),
        "pdf" => Some("application/pdf"),
        "txt" | "text" => Some("text/plain"),
        "html" | "htm" => Some("text/html"),
        "json" => Some("application/json"),
        "xml" => Some("application/xml"),
        "csv" => Some("text/csv"),
        "md" | "markdown" => Some("text/markdown"),
        "png" => Some("image/png"),
        "jpg" | "jpeg" => Some("image/jpeg"),
        "gif" => Some("image/gif"),
        "zip" => Some("application/zip"),
        "odt" => Some("application/vnd.oasis.opendocument.text"),
        "ods" => Some("application/vnd.oasis.opendocument.spreadsheet"),
        "odp" => Some("application/vnd.oasis.opendocument.presentation"),
        "rtf" => Some("application/rtf"),
        "epub" => Some("application/epub+zip"),
        "msg" => Some("application/vnd.ms-outlook"),
        "eml" => Some("message/rfc822"),
        _ => None,
    }
}

/// Emit Dart constructors for a batch item array (`BatchBytesItem` or `BatchFileItem`).
///
/// Returns a Dart list literal like:
/// ```dart
/// [BatchBytesItem(content: Uint8List.fromList([72, 101, ...]), mimeType: 'text/plain')]
/// ```
fn emit_dart_batch_item_array(arr: &serde_json::Value, elem_type: &str) -> String {
    let items: Vec<String> = arr
        .as_array()
        .map(|a| a.as_slice())
        .unwrap_or_default()
        .iter()
        .filter_map(|item| {
            let obj = item.as_object()?;
            match elem_type {
                "BatchBytesItem" => {
                    let content_bytes = obj
                        .get("content")
                        .and_then(|v| v.as_array())
                        .map(|arr| {
                            let nums: Vec<String> =
                                arr.iter().filter_map(|v| v.as_u64().map(|n| n.to_string())).collect();
                            format!("Uint8List.fromList([{}])", nums.join(", "))
                        })
                        .unwrap_or_else(|| "Uint8List(0)".to_string());
                    let mime_type = obj
                        .get("mime_type")
                        .and_then(|v| v.as_str())
                        .unwrap_or("application/octet-stream");
                    Some(format!(
                        "BatchBytesItem(content: {content_bytes}, mimeType: '{}')",
                        escape_dart(mime_type)
                    ))
                }
                "BatchFileItem" => {
                    let path = obj.get("path").and_then(|v| v.as_str()).unwrap_or("");
                    Some(format!("BatchFileItem(path: '{}')", escape_dart(path)))
                }
                _ => None,
            }
        })
        .collect();
    format!("[{}]", items.join(", "))
}

/// Emit a `expect(array.where(...).any(...), isTrue)` line that aggregates
/// every accessor on the element type of a `List<T>` field, mirroring
/// python's `_alef_e2e_item_texts` helper.
///
/// Since Dart e2e codegen doesn't currently carry type information for per-type
/// field classification, this fallback always tries to aggregate common text-bearing
/// accessors (kind, name, source, items, alias, and similar snake_case names) on any
/// element type. This is lenient and works well with opaque DTOs from FRB binding
/// generation where we can't statically determine the exact field structure.
///
/// Returns `None` when:
///   - `field` is missing or the field doesn't look like an array field
///
/// When matched, emits code that tries to gather text from a set of known
/// accessor names into a `[String]` and substring-matches the expected value
/// against every entry. The matcher is lenient so that fixtures asserting `"os"`
/// against the `imports` field succeed regardless of which accessor surfaces
/// the value (`ImportInfo.source`, `ImportInfo.items`, etc.).
///
/// First tries the "stringy aggregator" path: when the array element is an
/// opaque DTO with several text-bearing accessors, emit a `where(...)`
/// closure that walks every accessor and does substring matching. Falls back
/// to the catch-all path if no stringy fields are recorded for the element type.
fn dart_stringy_aggregator_contains_assert(
    field: Option<&str>,
    result_var: &str,
    field_resolver: &crate::e2e::field_access::FieldResolver,
    dart_val: &str,
) -> Option<String> {
    use crate::e2e::field_access::StringyFieldKind;
    let field = field?;
    let resolved = field_resolver.resolve(field);

    // Only handle simple top-level array fields (no nested chains).
    if resolved.contains('.') || resolved.contains('[') {
        return None;
    }

    // Check if this is a known array field. If not, we can't tell if it's a
    // list of DTOs so bail out and let the scalar list path handle it.
    if !field_resolver.is_array(field) && !field_resolver.is_array(resolved) {
        return None;
    }

    let array_accessor = field_resolver.accessor(field, "dart", result_var);

    // Try the stringy aggregator path: if the element type has multiple
    // text-bearing accessors, emit a proper aggregator instead of a catch-all.
    let root_type = field_resolver.dart_root_type().cloned();
    if let Some(elem_type) = field_resolver.dart_advance(root_type.as_deref(), resolved) {
        if let Some(stringy) = field_resolver.dart_stringy_fields(&elem_type) {
            // Only emit the aggregator if the element type has 2+ stringy fields.
            // Single-field types are better served by the simpler single-accessor path.
            if stringy.len() >= 2 {
                // flutter_rust_bridge renders struct DTOs as plain Dart classes
                // with `final` fields, so accessors are property reads (no
                // parens). Dart is statically typed — calling `item.field()` on
                // a non-callable field, or naming a field the type lacks, is a
                // compile error, not a runtime miss.
                let mut texts_lines: Vec<String> = Vec::new();
                for sf in stringy {
                    let call = sf.name.to_lower_camel_case();
                    match sf.kind {
                        StringyFieldKind::Plain => {
                            texts_lines.push(format!("            texts.add(item.{call}.toString());"));
                        }
                        StringyFieldKind::Optional => {
                            texts_lines.push(format!(
                                "            final v_{call} = item.{call};\n            if (v_{call} != null) texts.add(v_{call}.toString());"
                            ));
                        }
                        StringyFieldKind::Vec => {
                            texts_lines.push(format!(
                                "            texts.addAll(item.{call}.map((e) => e.toString()));"
                            ));
                        }
                    }
                }
                let texts_block = texts_lines.join("\n");
                // Case-insensitive substring match: enum/sealed-class fields
                // stringify to `EnumName.variant()` (lowerCamelCase variant),
                // while fixture node-type values are PascalCase (`Function`).
                return Some(format!(
                    "    expect({array_accessor}.where((item) {{\n            final texts = <String>[];\n{texts_block}\n            return texts.any((t) => t.toLowerCase().contains(({dart_val}).toString().toLowerCase()));\n          }}).isEmpty, isFalse);"
                ));
            }
        }
    }

    // Fallback: the element type's fields could not be resolved from the IR
    // (unknown root type, or fewer than two recorded stringy fields). Dart is
    // statically typed, so probing arbitrary accessor names cannot compile —
    // emit a lenient whole-object stringification match that always compiles.
    Some(format!(
        "    expect({array_accessor}.where((item) => item.toString().toLowerCase().contains(({dart_val}).toString().toLowerCase())).isEmpty, isFalse);"
    ))
}

/// Resolve the IR type name backing this call's result, mirroring
/// `swift_call_result_type`. Any of `c, csharp, java, kotlin, go, php`
/// overrides may carry a `result_type` field; the first non-empty value wins.
/// These are language-agnostic IR type names shared across every binding.
///
/// Returns `None` when no override sets `result_type`; the renderer then falls
/// back to the workspace-default root-type heuristic in `DartFirstClassMap`.
fn dart_call_result_type(call_config: &crate::core::config::e2e::CallConfig) -> Option<String> {
    const LOOKUP_LANGS: &[&str] = &["c", "csharp", "java", "kotlin", "go", "php"];
    for lang in LOOKUP_LANGS {
        if let Some(o) = call_config.overrides.get(*lang)
            && let Some(rt) = o.result_type.as_deref()
            && !rt.is_empty()
        {
            return Some(rt.to_string());
        }
    }
    None
}

/// Escape a string for embedding in a Dart single-quoted string literal.
pub(super) fn escape_dart(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('\'', "\\'")
        .replace('\n', "\\n")
        .replace('\r', "\\r")
        .replace('\t', "\\t")
        .replace('$', "\\$")
}

/// Derive the Dart top-level helper function name for constructing a mirror type from JSON.
///
/// The alef dart bridge-crate generator emits a Rust free function
/// `create_<snake_type>_from_json(json: String)` for each non-opaque mirror struct.
/// FRB generates the corresponding Dart function as `createTypeNameFromJson` (camelCase).
///
/// Example: `"ChatCompletionRequest"` → `"createChatCompletionRequestFromJson"`.
fn type_name_to_create_from_json_dart(type_name: &str) -> String {
    // Convert PascalCase type name to snake_case.
    let mut snake = String::with_capacity(type_name.len() + 8);
    for (i, ch) in type_name.char_indices() {
        if ch.is_uppercase() {
            if i > 0 {
                snake.push('_');
            }
            snake.extend(ch.to_lowercase());
        } else {
            snake.push(ch);
        }
    }
    // snake is now e.g. "chat_completion_request"
    // Full Rust function name: "create_chat_completion_request_from_json"
    let rust_fn = format!("create_{snake}_from_json");
    // Convert to Dart camelCase: "createChatCompletionRequestFromJson"
    rust_fn
        .split('_')
        .enumerate()
        .map(|(i, part)| {
            if i == 0 {
                part.to_string()
            } else {
                let mut chars = part.chars();
                match chars.next() {
                    None => String::new(),
                    Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
                }
            }
        })
        .collect::<Vec<_>>()
        .join("")
}

/// Build the Dart stringy field classification map for aggregating text accessors
/// in `Vec<T>` contains assertions. Similar to Swift's `build_swift_first_class_map`,
/// but Dart doesn't distinguish first-class vs opaque types — we just track stringy
/// fields per type for the `contains(where:)` closure aggregator.
fn build_dart_first_class_map(
    type_defs: &[crate::core::ir::TypeDef],
    enum_defs: &[crate::core::ir::EnumDef],
    e2e_config: &crate::e2e::config::E2eConfig,
) -> crate::e2e::field_access::DartFirstClassMap {
    use crate::core::ir::TypeRef;
    use crate::e2e::field_access::{StringyField, StringyFieldKind};

    let mut field_types: std::collections::HashMap<String, std::collections::HashMap<String, String>> =
        std::collections::HashMap::new();

    fn inner_named(ty: &TypeRef) -> Option<String> {
        match ty {
            TypeRef::Named(n) => Some(n.clone()),
            TypeRef::Optional(inner) | TypeRef::Vec(inner) => inner_named(inner),
            _ => None,
        }
    }

    let enum_names: std::collections::HashSet<&str> = enum_defs.iter().map(|e| e.name.as_str()).collect();
    let classify_stringy = |ty: &TypeRef, field_optional: bool| -> Option<StringyFieldKind> {
        match ty {
            TypeRef::String => Some(if field_optional {
                StringyFieldKind::Optional
            } else {
                StringyFieldKind::Plain
            }),
            TypeRef::Named(name) if enum_names.contains(name.as_str()) => Some(if field_optional {
                StringyFieldKind::Optional
            } else {
                StringyFieldKind::Plain
            }),
            TypeRef::Optional(inner) => match inner.as_ref() {
                TypeRef::String => Some(StringyFieldKind::Optional),
                TypeRef::Named(name) if enum_names.contains(name.as_str()) => Some(StringyFieldKind::Optional),
                _ => None,
            },
            TypeRef::Vec(inner) => match inner.as_ref() {
                TypeRef::String => Some(StringyFieldKind::Vec),
                TypeRef::Named(name) if enum_names.contains(name.as_str()) => Some(StringyFieldKind::Vec),
                _ => None,
            },
            _ => None,
        }
    };

    let mut stringy_fields_by_type: std::collections::HashMap<String, Vec<StringyField>> =
        std::collections::HashMap::new();
    for td in type_defs {
        let mut td_field_types: std::collections::HashMap<String, String> = std::collections::HashMap::new();
        let mut td_stringy: Vec<StringyField> = Vec::new();
        for f in &td.fields {
            if let Some(named) = inner_named(&f.ty) {
                td_field_types.insert(f.name.clone(), named);
            }
            if f.binding_excluded {
                continue;
            }
            if let Some(kind) = classify_stringy(&f.ty, f.optional) {
                td_stringy.push(StringyField {
                    name: f.name.clone(),
                    kind,
                });
            }
        }
        if !td_field_types.is_empty() {
            field_types.insert(td.name.clone(), td_field_types);
        }
        if !td_stringy.is_empty() {
            stringy_fields_by_type.insert(td.name.clone(), td_stringy);
        }
    }

    // Best-effort root-type detection: pick a unique TypeDef that contains all
    // `result_fields`.
    let root_type = if e2e_config.result_fields.is_empty() {
        None
    } else {
        let matches: Vec<&crate::core::ir::TypeDef> = type_defs
            .iter()
            .filter(|td| {
                let names: std::collections::HashSet<&str> = td.fields.iter().map(|f| f.name.as_str()).collect();
                e2e_config.result_fields.iter().all(|rf| names.contains(rf.as_str()))
            })
            .collect();
        if matches.len() == 1 {
            Some(matches[0].name.clone())
        } else {
            None
        }
    };

    crate::e2e::field_access::DartFirstClassMap {
        field_types,
        root_type,
        stringy_fields_by_type,
    }
}

/// Emit a Dart test backend stub class for a trait bridge.
///
/// Generates a concrete subclass of the trait's abstract base class. Required
/// methods are overridden with `Future.value(default)` (async) or the direct
/// default (sync). The `name` getter is emitted when a Plugin super-trait is
/// configured.
#[allow(unused_imports)]
pub fn emit_test_backend(
    trait_bridge: &crate::core::config::TraitBridgeConfig,
    methods: &[&crate::core::ir::MethodDef],
    fixture: &crate::e2e::fixture::Fixture,
) -> super::TestBackendEmission {
    use crate::backends::dart::type_map::DartMapper;
    use crate::codegen::defaults::language_defaults;
    use crate::codegen::type_mapper::TypeMapper as _;
    use heck::{ToLowerCamelCase, ToUpperCamelCase};
    use std::fmt::Write as _;

    let pascal_id = fixture.id.to_upper_camel_case();
    let class_name = format!("TestStub{pascal_id}");
    let trait_class = &trait_bridge.trait_name;

    // Prefer the fixture's input "name" field (e.g. "test-extractor") over the
    // fixture id, which is a snake_case internal identifier not a backend name.
    let plugin_name = fixture
        .input
        .get("name")
        .and_then(|v| v.as_str())
        .unwrap_or(&fixture.id)
        .to_string();

    let defaults = language_defaults("dart");
    let mapper = DartMapper;

    // Collect all types used in method signatures to determine needed imports.
    let mut needs_uint8list = false;
    for method in methods {
        for param in &method.params {
            if param.ty == crate::core::ir::TypeRef::Bytes {
                needs_uint8list = true;
            }
        }
        if method.return_type == crate::core::ir::TypeRef::Bytes {
            needs_uint8list = true;
        }
    }

    let mut setup = String::new();
    let _ = writeln!(setup, "class {class_name} extends {trait_class} {{");

    // Plugin super-trait `name` getter — no @override on local class members.
    if trait_bridge.super_trait.is_some() {
        let _ = writeln!(setup, "  String get name => '{plugin_name}';");
    }

    // Required methods — use concrete Dart types from the type mapper.
    for method in methods {
        if method.has_default_impl {
            continue;
        }
        let method_name = method.name.to_lower_camel_case();

        // Build typed parameter list using DartMapper for concrete type names.
        let params: Vec<String> = method
            .params
            .iter()
            .map(|p| {
                let param_type = map_dart_type_with_fallback(&mapper, &p.ty);
                format!("{} {}", param_type, p.name.to_lower_camel_case())
            })
            .collect();
        let params_str = params.join(", ");

        let return_type = map_dart_type_with_fallback(&mapper, &method.return_type);
        let default_val = emit_dart_default_for_type(defaults.as_ref(), &method.return_type);

        if method.is_async {
            let _ = writeln!(
                setup,
                "  Future<{return_type}> {method_name}({params_str}) async => {default_val};"
            );
        } else {
            let _ = writeln!(setup, "  {return_type} {method_name}({params_str}) => {default_val};");
        }
    }

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

    // Dart trait bridges require wrapping the implementation in a `create<Trait>DartImpl()` call.
    // The wrapper requires pluginName, pluginVersion, and callbacks for all trait methods.
    let create_fn = format!("create{}DartImpl", trait_bridge.trait_name);
    let plugin_name = fixture
        .input
        .get("name")
        .and_then(|v| v.as_str())
        .unwrap_or(&fixture.id);

    let instance_name = format!("_{class_name}_instance");
    let wrapped_var = format!("_{class_name}_wrapped");

    // Emit the instance creation and wrapper call in the setup block.
    let _ = writeln!(setup, "final {instance_name} = {class_name}();");
    // The factory wrapper is sync (not awaited); trait factory callbacks return T directly.
    let _ = writeln!(setup, "final {wrapped_var} = {create_fn}(");
    let _ = writeln!(setup, "  pluginName: '{plugin_name}',");
    let _ = writeln!(setup, "  pluginVersion: '0.0.1',");

    // Emit method callbacks - required methods with implementations
    for (i, method) in methods.iter().enumerate() {
        let method_name = method.name.to_lower_camel_case();
        let param_names: Vec<String> = method.params.iter().map(|p| p.name.to_lower_camel_case()).collect();
        let params_str = param_names.join(", ");
        let binding = if params_str.is_empty() {
            format!("{method_name}: () => {instance_name}.{method_name}()")
        } else {
            format!("{method_name}: ({params_str}) => {instance_name}.{method_name}({params_str})")
        };
        let comma = if i < methods.len() - 1 { "," } else { "" };
        let _ = writeln!(setup, "  {binding}{comma}");
    }
    let _ = writeln!(setup, ");");

    let mut type_imports = Vec::new();
    if needs_uint8list {
        type_imports.push("dart:typed_data".to_string());
    }

    super::TestBackendEmission {
        setup_block: setup,
        arg_expr: wrapped_var,
        type_imports,
    }
}

/// Map a Dart type, with fallback to public public types for internal-only types.
/// For example, InternalDocument (not a public export) maps to ExtractionResult.
fn map_dart_type_with_fallback(
    mapper: &crate::backends::dart::type_map::DartMapper,
    ty: &crate::core::ir::TypeRef,
) -> String {
    use crate::codegen::type_mapper::TypeMapper as _;
    let mapped = mapper.map_type(ty);
    // If the type is an unpublished internal type (contains "Internal"), use ExtractionResult as fallback.
    if mapped.contains("Internal") {
        return "ExtractionResult".to_string();
    }
    mapped.to_string()
}

/// Emit a Dart default value for a type, with special handling for enums.
fn emit_dart_default_for_type(
    defaults: &dyn crate::codegen::defaults::LanguageDefaults,
    ty: &crate::core::ir::TypeRef,
) -> String {
    use crate::core::ir::TypeRef;
    // For named types that are enums (OcrBackendType, ProcessingStage, etc.), emit a default enum variant.
    if let TypeRef::Named(name) = ty {
        if name.ends_with("Type") || name.ends_with("Stage") {
            // Assume the enum has a default variant like .standard or .pending
            // Fallback to a safe generic form if not recognized.
            if name == "OcrBackendType" {
                return "OcrBackendType.tesseract".to_string();
            } else if name == "ProcessingStage" {
                return "ProcessingStage.preProcessing".to_string();
            }
        }
    }
    defaults.emit_default(ty).to_string()
}

#[cfg(test)]
mod test_backend_tests {
    use super::emit_test_backend;
    use crate::core::config::TraitBridgeConfig;
    use crate::core::ir::{MethodDef, PrimitiveType, TypeRef};
    use crate::e2e::fixture::Fixture;

    fn make_trait_bridge(trait_name: &str) -> TraitBridgeConfig {
        TraitBridgeConfig {
            trait_name: trait_name.to_string(),
            super_trait: Some("Plugin".to_string()),
            register_fn: Some(format!("register_{}", trait_name.to_lowercase())),
            ..Default::default()
        }
    }

    fn make_method(name: &str, required: bool) -> MethodDef {
        MethodDef {
            name: name.to_string(),
            params: vec![],
            return_type: TypeRef::Primitive(PrimitiveType::Bool),
            is_async: true,
            is_static: false,
            error_type: None,
            doc: String::new(),
            receiver: Some(crate::core::ir::ReceiverKind::Ref),
            sanitized: false,
            trait_source: None,
            returns_ref: false,
            returns_cow: false,
            return_newtype_wrapper: None,
            has_default_impl: !required,
            binding_excluded: false,
            binding_exclusion_reason: None,
        }
    }

    fn make_fixture(id: &str) -> Fixture {
        Fixture {
            id: id.to_string(),
            category: None,
            description: "test".to_string(),
            tags: vec![],
            skip: None,
            env: None,
            call: None,
            input: serde_json::Value::Null,
            mock_response: None,
            source: String::new(),
            http: None,
            assertions: vec![],
            visitor: None,
            args: vec![],
        }
    }

    /// Verify that no sample_core-domain names leak into the generated output when
    /// the trait bridge is configured for a synthetic `TestTrait` in `testlib`.
    #[test]
    fn dart_stub_contains_no_sample_crate_domain_names() {
        let bridge = make_trait_bridge("TestTrait");
        let required_method = make_method("doWork", true);
        let methods = [&required_method];
        let fixture = make_fixture("my_test_fixture");

        let emission = emit_test_backend(&bridge, &methods, &fixture);

        let output = format!("{}\n{}", emission.setup_block, emission.arg_expr);

        assert!(
            !output.contains("SampleCrate"),
            "must not contain literal 'SampleCrate', got:\n{output}"
        );
        assert!(
            !output.contains("sample_crate::"),
            "must not contain 'sample_crate::', got:\n{output}"
        );
        assert!(
            !output.contains("SampleCrateBridge"),
            "must not contain 'SampleCrateBridge', got:\n{output}"
        );
        assert!(
            output.contains("TestStubMyTestFixture"),
            "class name must be derived from fixture id, got:\n{output}"
        );
        assert!(
            output.contains("extends TestTrait"),
            "class must extend the configured trait class, got:\n{output}"
        );
        assert!(
            output.contains("doWork"),
            "required method must be emitted, got:\n{output}"
        );
    }

    fn make_param(name: &str, ty: TypeRef) -> crate::core::ir::ParamDef {
        crate::core::ir::ParamDef {
            name: name.to_string(),
            ty,
            optional: false,
            default: None,
            sanitized: false,
            typed_default: None,
            is_ref: false,
            is_mut: false,
            newtype_wrapper: None,
            original_type: None,
            map_is_ahash: false,
            map_key_is_cow: false,
        }
    }

    fn make_method_with_params(name: &str, required: bool) -> MethodDef {
        MethodDef {
            name: name.to_string(),
            params: vec![
                make_param("content", TypeRef::Bytes),
                make_param("mime_type", TypeRef::String),
            ],
            return_type: TypeRef::Named("ExtractionResult".to_string()),
            is_async: true,
            is_static: false,
            error_type: Some("anyhow::Error".to_string()),
            doc: String::new(),
            receiver: Some(crate::core::ir::ReceiverKind::Ref),
            sanitized: false,
            trait_source: None,
            returns_ref: false,
            returns_cow: false,
            return_newtype_wrapper: None,
            has_default_impl: !required,
            binding_excluded: false,
            binding_exclusion_reason: None,
        }
    }

    /// Verify params use concrete Dart types (not `dynamic`) and no @override annotation.
    #[test]
    fn dart_stub_uses_typed_params_not_dynamic() {
        let bridge = make_trait_bridge("TestTrait");
        let required_method = make_method_with_params("extract", true);
        let methods = [&required_method];
        let fixture = make_fixture("my_test_fixture");

        let emission = emit_test_backend(&bridge, &methods, &fixture);
        let output = format!("{}\n{}", emission.setup_block, emission.arg_expr);

        assert!(
            !output.contains("dynamic content"),
            "param must not use `dynamic`, got:\n{output}"
        );
        assert!(
            output.contains("Uint8List content"),
            "bytes param must map to Uint8List, got:\n{output}"
        );
        assert!(
            output.contains("String mimeType"),
            "string param must map to String, got:\n{output}"
        );
        assert!(
            output.contains("Future<ExtractionResult>"),
            "return type must be concrete not dynamic, got:\n{output}"
        );
        assert!(
            !output.contains("@override"),
            "local class members must not use @override annotation, got:\n{output}"
        );
    }

    /// Verify that `fixture.input["name"]` is used as the plugin name when present.
    #[test]
    fn dart_stub_uses_fixture_input_name_for_plugin_name() {
        let bridge = make_trait_bridge("TestTrait");
        let required_method = make_method("doWork", true);
        let methods = [&required_method];
        let mut fixture = make_fixture("my_fixture_id");
        fixture.input = serde_json::json!({ "name": "my-backend-name" });

        let emission = emit_test_backend(&bridge, &methods, &fixture);
        let output = format!("{}\n{}", emission.setup_block, emission.arg_expr);

        assert!(
            output.contains("'my-backend-name'"),
            "plugin name must come from fixture.input.name, got:\n{output}"
        );
        assert!(
            !output.contains("my_fixture_id"),
            "fixture id must not appear as plugin name when input.name is set, got:\n{output}"
        );
    }
}
