use alef::backends::dart::DartBackend;
use alef::core::backend::Backend;
use alef::core::config::{ResolvedCrateConfig, TraitBridgeConfig, new_config::NewAlefConfig};
use alef::core::ir::{
    ApiSurface, CoreWrapper, DefaultValue, EnumDef, EnumVariant, ErrorDef, ErrorVariant, FieldDef, FunctionDef,
    MethodDef, ParamDef, PrimitiveType, ReceiverKind, TypeDef, TypeRef,
};

fn make_field(name: &str, ty: TypeRef, optional: bool) -> FieldDef {
    FieldDef {
        name: name.to_string(),
        ty,
        optional,
        default: None,
        doc: String::new(),
        sanitized: false,
        is_boxed: false,
        type_rust_path: None,
        cfg: None,
        typed_default: None,
        core_wrapper: CoreWrapper::None,
        vec_inner_core_wrapper: CoreWrapper::None,
        newtype_wrapper: None,
        serde_rename: None,
        serde_flatten: false,
        binding_excluded: false,
        binding_exclusion_reason: None,
        original_type: None,
    }
}

fn make_param(name: &str, ty: TypeRef) -> ParamDef {
    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_type(name: &str, fields: Vec<FieldDef>) -> TypeDef {
    TypeDef {
        name: name.to_string(),
        rust_path: format!("demo::{name}"),
        original_rust_path: String::new(),
        fields,
        methods: vec![],
        is_opaque: false,
        is_clone: true,

        is_copy: false,
        doc: String::new(),
        cfg: None,
        is_trait: false,
        has_default: false,
        has_stripped_cfg_fields: false,
        is_return_type: false,
        serde_rename_all: None,
        has_serde: false,
        super_traits: vec![],
        binding_excluded: false,
        binding_exclusion_reason: None,
    }
}

fn make_config() -> ResolvedCrateConfig {
    let toml = r#"
[workspace]
languages = ["dart"]

[[crates]]
name = "demo-crate"
sources = ["src/lib.rs"]
"#;
    let cfg: NewAlefConfig = toml::from_str(toml).expect("test config must parse");
    cfg.resolve().expect("test config must resolve").remove(0)
}

#[test]
#[ignore = "FRB style: types/enums/errors are emitted by flutter_rust_bridge, not alef"]
fn struct_with_primitive_fields_emits_class() {
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![make_type(
            "Point",
            vec![
                make_field("x_coord", TypeRef::Primitive(PrimitiveType::I32), false),
                make_field("y_coord", TypeRef::Primitive(PrimitiveType::I32), false),
            ],
        )],
        functions: vec![],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };

    let files = DartBackend.generate_bindings(&api, &make_config()).unwrap();
    // generate_bindings now returns the Dart wrapper plus 5 Rust crate files
    assert_eq!(files.len(), 6);
    let content = files
        .iter()
        .find(|f| f.path.to_string_lossy().ends_with(".dart"))
        .map(|f| f.content.as_str())
        .expect("no .dart file emitted");
    assert!(
        content.contains("// Generated by alef. Do not edit by hand."),
        "missing header: {content}"
    );
    assert!(content.contains("class Point {"), "missing class: {content}");
    assert!(content.contains("final int xCoord;"), "missing field xCoord: {content}");
    assert!(content.contains("final int yCoord;"), "missing field yCoord: {content}");
    // Multi-field constructor uses named required params
    assert!(
        content.contains("required this.xCoord"),
        "missing named ctor param: {content}"
    );
    assert!(
        content.contains("required this.yCoord"),
        "missing named ctor param: {content}"
    );
}

#[test]
#[ignore = "FRB style: types/enums/errors are emitted by flutter_rust_bridge, not alef"]
fn struct_with_optional_vec_map_fields() {
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![make_type(
            "Container",
            vec![
                make_field("maybe_name", TypeRef::Optional(Box::new(TypeRef::String)), false),
                make_field("tags", TypeRef::Vec(Box::new(TypeRef::String)), false),
                make_field(
                    "scores",
                    TypeRef::Map(
                        Box::new(TypeRef::String),
                        Box::new(TypeRef::Primitive(PrimitiveType::F64)),
                    ),
                    false,
                ),
            ],
        )],
        functions: vec![],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };

    let files = DartBackend.generate_bindings(&api, &make_config()).unwrap();
    let content = &files[0].content;
    assert!(
        content.contains("final String? maybeName;"),
        "missing optional field: {content}"
    );
    assert!(
        content.contains("final List<String> tags;"),
        "missing list field: {content}"
    );
    assert!(
        content.contains("final Map<String, double> scores;"),
        "missing map field: {content}"
    );
}

#[test]
#[ignore = "FRB style: types/enums/errors are emitted by flutter_rust_bridge, not alef"]
fn empty_struct_emits_empty_class() {
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![make_type("Empty", vec![])],
        functions: vec![],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };

    let files = DartBackend.generate_bindings(&api, &make_config()).unwrap();
    let content = &files[0].content;
    assert!(content.contains("class Empty {}"), "missing empty class: {content}");
}

#[test]
#[ignore = "FRB style: types/enums/errors are emitted by flutter_rust_bridge, not alef"]
fn unit_enum_emits_dart_enum() {
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![],
        enums: vec![EnumDef {
            name: "Status".into(),
            rust_path: "demo::Status".into(),
            original_rust_path: String::new(),
            variants: vec![
                EnumVariant {
                    name: "Active".into(),
                    fields: vec![],
                    doc: String::new(),
                    is_default: false,
                    serde_rename: None,
                    is_tuple: false,
                },
                EnumVariant {
                    name: "Inactive".into(),
                    fields: vec![],
                    doc: String::new(),
                    is_default: false,
                    serde_rename: None,
                    is_tuple: false,
                },
            ],
            doc: String::new(),
            cfg: None,
            serde_tag: None,
            serde_untagged: false,
            serde_rename_all: None,

            is_copy: false,
            has_serde: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
        }],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };

    let files = DartBackend.generate_bindings(&api, &make_config()).unwrap();
    let content = &files[0].content;
    assert!(content.contains("enum Status {"), "missing enum: {content}");
    // Dart uses lowerCamelCase for enum variants
    assert!(content.contains("active"), "missing active variant: {content}");
    assert!(content.contains("inactive"), "missing inactive variant: {content}");
    // Last variant has semicolon
    assert!(
        content.contains("inactive;"),
        "missing semicolon on last variant: {content}"
    );
}

#[test]
#[ignore = "FRB style: types/enums/errors are emitted by flutter_rust_bridge, not alef"]
fn data_bearing_enum_emits_sealed_class() {
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![],
        enums: vec![EnumDef {
            name: "Shape".into(),
            rust_path: "demo::Shape".into(),
            original_rust_path: String::new(),
            variants: vec![
                EnumVariant {
                    name: "Circle".into(),
                    fields: vec![make_field("radius", TypeRef::Primitive(PrimitiveType::F64), false)],
                    doc: String::new(),
                    is_default: false,
                    serde_rename: None,
                    is_tuple: false,
                },
                EnumVariant {
                    name: "Rectangle".into(),
                    fields: vec![],
                    doc: String::new(),
                    is_default: false,
                    serde_rename: None,
                    is_tuple: false,
                },
            ],
            doc: String::new(),
            cfg: None,
            serde_tag: None,
            serde_untagged: false,
            serde_rename_all: None,

            is_copy: false,
            has_serde: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
        }],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };

    let files = DartBackend.generate_bindings(&api, &make_config()).unwrap();
    let content = &files[0].content;
    assert!(
        content.contains("sealed class Shape {}"),
        "missing sealed class: {content}"
    );
    assert!(
        content.contains("final class Circle extends Shape {"),
        "missing Circle variant: {content}"
    );
    assert!(
        content.contains("final double radius;"),
        "missing radius field: {content}"
    );
    assert!(
        content.contains("final class Rectangle extends Shape {}"),
        "missing unit Rectangle: {content}"
    );
}

#[test]
fn simple_sync_function_emits_static_method() {
    let api = ApiSurface {
        crate_name: "demo-crate".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![FunctionDef {
            name: "greet_user".into(),
            rust_path: "demo::greet_user".into(),
            original_rust_path: String::new(),
            params: vec![make_param("user_name", TypeRef::String)],
            return_type: TypeRef::Primitive(PrimitiveType::I32),
            is_async: false,
            error_type: None,
            doc: String::new(),
            cfg: None,
            sanitized: false,
            return_sanitized: false,
            returns_ref: false,
            returns_cow: false,
            return_newtype_wrapper: None,
            binding_excluded: false,
            binding_exclusion_reason: None,
        }],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };

    let files = DartBackend.generate_bindings(&api, &make_config()).unwrap();
    let content = files
        .iter()
        .find(|f| {
            f.path
                .to_string_lossy()
                .replace('\\', "/")
                .ends_with("/src/demo_crate.dart")
        })
        .map(|f| f.content.as_str())
        .expect("missing src/demo_crate.dart");
    // FRB style: wrappers are always-async (Future<...>) and call into the
    // bridge-generated lib.dart with named parameters.
    assert!(
        content.contains("class DemoCrateBridge {"),
        "missing bridge class: {content}"
    );
    assert!(
        content.contains("static Future<int> greetUser(String userName) async {"),
        "missing async wrapper signature: {content}"
    );
    assert!(
        content.contains("return await rust_bridge.greetUser(userName: userName);"),
        "missing awaited bridge call with named param: {content}"
    );
    assert!(
        content.contains("import 'demo_crate_bridge_generated/lib.dart' as rust_bridge;"),
        "missing rust_bridge lib.dart import: {content}"
    );
}

#[test]
fn default_config_param_uses_default_constructor_for_empty_default_type() {
    let mut pack_config = make_type("PackConfig", vec![]);
    pack_config.has_default = true;
    let api = ApiSurface {
        crate_name: "demo-crate".into(),
        version: "0.1.0".into(),
        types: vec![pack_config],
        functions: vec![FunctionDef {
            name: "pack".into(),
            rust_path: "demo::pack".into(),
            original_rust_path: String::new(),
            params: vec![make_param("config", TypeRef::Named("PackConfig".to_string()))],
            return_type: TypeRef::Unit,
            is_async: false,
            error_type: None,
            doc: String::new(),
            cfg: None,
            sanitized: false,
            return_sanitized: false,
            returns_ref: false,
            returns_cow: false,
            return_newtype_wrapper: None,
            binding_excluded: false,
            binding_exclusion_reason: None,
        }],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };

    let files = DartBackend.generate_bindings(&api, &make_config()).unwrap();
    let content = find_dart_src(&files).expect("src dart file should be emitted");

    assert!(
        content.contains("static Future<void> pack([PackConfig? config]) async {"),
        "config should be optional: {content}"
    );
    assert!(
        content.contains("return await rust_bridge.pack(config: config ?? PackConfig());"),
        "config should fall back to PackConfig(): {content}"
    );
}

#[test]
fn default_config_param_synthesizes_expression_from_type_metadata() {
    let mut enabled = make_field("enabled", TypeRef::Primitive(PrimitiveType::Bool), false);
    enabled.typed_default = Some(DefaultValue::BoolLiteral(true));
    let mut retry_count = make_field("retry_count", TypeRef::Primitive(PrimitiveType::U32), false);
    retry_count.typed_default = Some(DefaultValue::IntLiteral(2));
    let mut strategy = make_field("strategy", TypeRef::Named("Strategy".to_string()), false);
    strategy.typed_default = Some(DefaultValue::EnumVariant("Balanced".to_string()));
    let mut example_config = make_type("ExampleConfig", vec![enabled, retry_count, strategy]);
    example_config.has_default = true;

    let api = ApiSurface {
        crate_name: "demo-crate".into(),
        version: "0.1.0".into(),
        types: vec![example_config],
        functions: vec![FunctionDef {
            name: "run_example".into(),
            rust_path: "demo::run_example".into(),
            original_rust_path: String::new(),
            params: vec![make_param("config", TypeRef::Named("ExampleConfig".to_string()))],
            return_type: TypeRef::String,
            is_async: false,
            error_type: None,
            doc: String::new(),
            cfg: None,
            sanitized: false,
            return_sanitized: false,
            returns_ref: false,
            returns_cow: false,
            return_newtype_wrapper: None,
            binding_excluded: false,
            binding_exclusion_reason: None,
        }],
        enums: vec![EnumDef {
            name: "Strategy".into(),
            rust_path: "demo::Strategy".into(),
            original_rust_path: String::new(),
            variants: vec![EnumVariant {
                name: "Balanced".into(),
                fields: vec![],
                doc: String::new(),
                is_default: true,
                serde_rename: None,
                is_tuple: false,
            }],
            doc: String::new(),
            cfg: None,
            serde_tag: None,
            serde_untagged: false,
            serde_rename_all: None,
            is_copy: false,
            has_serde: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
        }],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };

    let files = DartBackend.generate_bindings(&api, &make_config()).unwrap();
    let content = find_dart_src(&files).expect("src dart file should be emitted");

    assert!(
        content.contains("static Future<String> runExample([ExampleConfig? config]) async {"),
        "config should be optional: {content}"
    );
    assert!(
        content.contains("config: config ?? ExampleConfig(enabled: true, retryCount: 2, strategy: Strategy.balanced)"),
        "config should use metadata-derived defaults: {content}"
    );
    assert!(
        !content.contains("DEFAULT_EXTRACTION_CONFIG") && !content.contains("ExtractionConfig("),
        "Dart defaults should not use the removed ExtractionConfig special case: {content}"
    );
}

#[test]
fn async_function_emits_future_return_and_async_keyword() {
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![FunctionDef {
            name: "fetch_data".into(),
            rust_path: "demo::fetch_data".into(),
            original_rust_path: String::new(),
            params: vec![],
            return_type: TypeRef::String,
            is_async: true,
            error_type: None,
            doc: String::new(),
            cfg: None,
            sanitized: false,
            return_sanitized: false,
            returns_ref: false,
            returns_cow: false,
            return_newtype_wrapper: None,
            binding_excluded: false,
            binding_exclusion_reason: None,
        }],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };

    let files = DartBackend.generate_bindings(&api, &make_config()).unwrap();
    let content = files
        .iter()
        .find(|f| {
            f.path
                .to_string_lossy()
                .replace('\\', "/")
                .ends_with("/src/demo_crate.dart")
        })
        .map(|f| f.content.as_str())
        .expect("missing src/demo_crate.dart");
    // dart:async is no longer needed: Dart's core Future is in dart:core (auto-imported).
    assert!(
        content.contains("static Future<String> fetchData() async {"),
        "missing async method: {content}"
    );
    assert!(
        content.contains("return await rust_bridge.fetchData();"),
        "missing await bridge call: {content}"
    );
}

#[test]
fn error_returning_function_emits_doc_comment() {
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![FunctionDef {
            name: "parse_input".into(),
            rust_path: "demo::parse_input".into(),
            original_rust_path: String::new(),
            params: vec![make_param("raw", TypeRef::String)],
            return_type: TypeRef::String,
            is_async: false,
            error_type: Some("ParseError".into()),
            doc: String::new(),
            cfg: None,
            sanitized: false,
            return_sanitized: false,
            returns_ref: false,
            returns_cow: false,
            return_newtype_wrapper: None,
            binding_excluded: false,
            binding_exclusion_reason: None,
        }],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };

    let files = DartBackend.generate_bindings(&api, &make_config()).unwrap();
    let content = files
        .iter()
        .find(|f| {
            f.path
                .to_string_lossy()
                .replace('\\', "/")
                .ends_with("/src/demo_crate.dart")
        })
        .map(|f| f.content.as_str())
        .expect("missing src/demo_crate.dart");
    assert!(
        content.contains("/// throws ParseError on failure"),
        "missing error doc comment: {content}"
    );
    // FRB style emits always-async wrappers — Future<String> rather than bare String.
    assert!(
        content.contains("static Future<String> parseInput(String raw) async {"),
        "missing async parseInput method: {content}"
    );
}

#[test]
#[ignore = "FRB style: types/enums/errors are emitted by flutter_rust_bridge, not alef"]
fn error_type_emits_sealed_class_hierarchy() {
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![],
        enums: vec![],
        errors: vec![ErrorDef {
            name: "ApiError".into(),
            rust_path: "demo::ApiError".into(),
            original_rust_path: String::new(),
            variants: vec![
                ErrorVariant {
                    name: "NotFound".into(),
                    message_template: Some("Resource not found".into()),
                    fields: vec![],
                    has_source: false,
                    has_from: false,
                    is_unit: true,
                    doc: String::new(),
                },
                ErrorVariant {
                    name: "InvalidFormat".into(),
                    message_template: Some("Invalid format at line {0}".into()),
                    fields: vec![make_field("line_number", TypeRef::Primitive(PrimitiveType::I32), false)],
                    has_source: false,
                    has_from: false,
                    is_unit: false,
                    doc: String::new(),
                },
            ],
            doc: String::new(),
            methods: vec![],
            binding_excluded: false,
            binding_exclusion_reason: None,
        }],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };

    let files = DartBackend.generate_bindings(&api, &make_config()).unwrap();
    let content = &files[0].content;
    assert!(
        content.contains("sealed class ApiError implements Exception {"),
        "missing sealed error class: {content}"
    );
    assert!(
        content.contains("final class NotFound implements ApiError {"),
        "missing NotFound: {content}"
    );
    assert!(
        content.contains("String get message => 'Resource not found'"),
        "missing message getter: {content}"
    );
    assert!(
        content.contains("final class InvalidFormat implements ApiError {"),
        "missing InvalidFormat: {content}"
    );
    assert!(content.contains("final int lineNumber;"), "missing field: {content}");
}

#[test]
#[ignore = "FRB style: types/enums/errors are emitted by flutter_rust_bridge, not alef"]
fn bytes_field_adds_typed_data_import() {
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![make_type("Blob", vec![make_field("data", TypeRef::Bytes, false)])],
        functions: vec![],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };

    let files = DartBackend.generate_bindings(&api, &make_config()).unwrap();
    let content = &files[0].content;
    assert!(
        content.contains("import 'dart:typed_data';"),
        "missing typed_data import: {content}"
    );
    assert!(
        content.contains("final Uint8List data;"),
        "missing Uint8List field: {content}"
    );
}

#[test]
fn output_path_uses_module_name() {
    let api = ApiSurface {
        crate_name: "my-lib".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };
    let mut config = make_config();
    config.name = "my-lib".to_string();

    let files = DartBackend.generate_bindings(&api, &config).unwrap();
    // Even empty API generates one file (or zero if we short-circuit)
    // With no content, we still return the file — check the path ends with my_lib.dart
    if !files.is_empty() {
        let path_str = files[0].path.to_string_lossy();
        assert!(path_str.ends_with("my_lib.dart"), "wrong output path: {path_str}");
    }
}

// ── Dart trait abstract class emission ──────────────────────────────────────

fn make_method(name: &str, params: Vec<ParamDef>, return_type: TypeRef, is_async: bool) -> MethodDef {
    MethodDef {
        name: name.to_string(),
        params,
        return_type,
        is_async,
        is_static: false,
        error_type: None,
        doc: String::new(),
        receiver: Some(ReceiverKind::Ref),
        sanitized: false,
        trait_source: None,
        returns_ref: false,
        returns_cow: false,
        return_newtype_wrapper: None,
        has_default_impl: false,
        binding_excluded: false,
        binding_exclusion_reason: None,
    }
}

fn make_trait(name: &str, rust_path: &str, methods: Vec<MethodDef>) -> TypeDef {
    TypeDef {
        name: name.to_string(),
        rust_path: rust_path.to_string(),
        original_rust_path: String::new(),
        fields: vec![],
        methods,
        is_opaque: true,
        is_clone: false,
        is_copy: false,
        doc: String::new(),
        cfg: None,
        is_trait: true,
        has_default: false,
        has_stripped_cfg_fields: false,
        is_return_type: false,
        serde_rename_all: None,
        has_serde: false,
        super_traits: vec![],
        binding_excluded: false,
        binding_exclusion_reason: None,
    }
}

fn make_config_with_bridge(bridge_trait_name: &str) -> ResolvedCrateConfig {
    let mut config = make_config();
    config.trait_bridges = vec![TraitBridgeConfig {
        trait_name: bridge_trait_name.to_string(),
        super_trait: None,
        registry_getter: None,
        register_fn: None,

        unregister_fn: None,

        clear_fn: None,
        type_alias: None,
        param_name: None,
        register_extra_args: None,
        exclude_languages: vec![],
        ffi_skip_methods: Vec::new(),
        bind_via: alef::core::config::BridgeBinding::FunctionParam,
        options_type: None,
        options_field: None,
        context_type: None,
        result_type: None,
    }];
    config
}

fn find_traits_dart(files: &[alef::core::backend::GeneratedFile]) -> Option<&str> {
    files
        .iter()
        .find(|f| f.path.to_string_lossy().ends_with("traits.dart"))
        .map(|f| f.content.as_str())
}

#[test]
fn no_trait_bridges_does_not_emit_traits_dart() {
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };

    let files = DartBackend.generate_bindings(&api, &make_config()).unwrap();
    // Without trait_bridges config, traits.dart should NOT appear.
    assert!(
        find_traits_dart(&files).is_none(),
        "traits.dart should not be emitted without trait_bridges config"
    );
}

#[test]
fn single_trait_bridge_emits_traits_dart_with_abstract_class() {
    let trait_def = make_trait(
        "OcrBackend",
        "demo_crate::OcrBackend",
        vec![make_method(
            "process_image",
            vec![make_param("bytes", TypeRef::Bytes)],
            TypeRef::String,
            true,
        )],
    );
    let api = ApiSurface {
        crate_name: "demo-crate".into(),
        version: "0.1.0".into(),
        types: vec![trait_def],
        functions: vec![],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };

    let files = DartBackend
        .generate_bindings(&api, &make_config_with_bridge("OcrBackend"))
        .unwrap();
    let content = find_traits_dart(&files).expect("traits.dart should be emitted");

    assert!(
        content.contains("// Generated by alef. Do not edit by hand."),
        "missing header: {content}"
    );
    assert!(
        content.contains("abstract class OcrBackend {"),
        "missing abstract class: {content}"
    );
    assert!(
        content.contains("Future<String> processImage(Uint8List bytes);"),
        "missing abstract method: {content}"
    );
}

#[test]
fn traits_dart_path_is_under_lib_src() {
    let trait_def = make_trait(
        "Validator",
        "demo_crate::Validator",
        vec![make_method(
            "validate",
            vec![make_param("input", TypeRef::String)],
            TypeRef::Primitive(PrimitiveType::Bool),
            false,
        )],
    );
    let api = ApiSurface {
        crate_name: "demo-crate".into(),
        version: "0.1.0".into(),
        types: vec![trait_def],
        functions: vec![],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };

    let files = DartBackend
        .generate_bindings(&api, &make_config_with_bridge("Validator"))
        .unwrap();
    let traits_file = files
        .iter()
        .find(|f| f.path.to_string_lossy().ends_with("traits.dart"))
        .expect("traits.dart should be emitted");

    let path_str = traits_file.path.to_string_lossy();
    assert!(
        path_str.contains("packages/dart/lib/src/traits.dart"),
        "wrong path for traits.dart: {path_str}"
    );
}

#[test]
fn multiple_trait_bridges_emit_multiple_abstract_classes() {
    let ocr_trait = make_trait(
        "OcrBackend",
        "demo_crate::OcrBackend",
        vec![make_method(
            "extract_text",
            vec![make_param("data", TypeRef::Bytes)],
            TypeRef::String,
            true,
        )],
    );
    let validator_trait = make_trait(
        "Validator",
        "demo_crate::Validator",
        vec![make_method(
            "validate",
            vec![make_param("input", TypeRef::String)],
            TypeRef::Primitive(PrimitiveType::Bool),
            false,
        )],
    );
    let api = ApiSurface {
        crate_name: "demo-crate".into(),
        version: "0.1.0".into(),
        types: vec![ocr_trait, validator_trait],
        functions: vec![],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };

    let mut config = make_config();
    config.trait_bridges = vec![
        TraitBridgeConfig {
            trait_name: "OcrBackend".to_string(),
            super_trait: None,
            registry_getter: None,
            register_fn: None,

            unregister_fn: None,

            clear_fn: None,
            type_alias: None,
            param_name: None,
            register_extra_args: None,
            exclude_languages: vec![],
            ffi_skip_methods: Vec::new(),
            bind_via: alef::core::config::BridgeBinding::FunctionParam,
            options_type: None,
            options_field: None,
            context_type: None,
            result_type: None,
        },
        TraitBridgeConfig {
            trait_name: "Validator".to_string(),
            super_trait: None,
            registry_getter: None,
            register_fn: None,

            unregister_fn: None,

            clear_fn: None,
            type_alias: None,
            param_name: None,
            register_extra_args: None,
            exclude_languages: vec![],
            ffi_skip_methods: Vec::new(),
            bind_via: alef::core::config::BridgeBinding::FunctionParam,
            options_type: None,
            options_field: None,
            context_type: None,
            result_type: None,
        },
    ];

    let files = DartBackend.generate_bindings(&api, &config).unwrap();
    let content = find_traits_dart(&files).expect("traits.dart should be emitted");

    assert!(
        content.contains("abstract class OcrBackend {"),
        "missing OcrBackend: {content}"
    );
    assert!(
        content.contains("abstract class Validator {"),
        "missing Validator: {content}"
    );
    assert!(
        content.contains("Future<String> extractText(Uint8List data);"),
        "missing extractText: {content}"
    );
    assert!(
        content.contains("Future<bool> validate(String input);"),
        "missing validate: {content}"
    );
}

#[test]
fn excluded_trait_bridge_does_not_appear_in_traits_dart() {
    let trait_def = make_trait(
        "OcrBackend",
        "demo_crate::OcrBackend",
        vec![make_method(
            "extract_text",
            vec![make_param("data", TypeRef::Bytes)],
            TypeRef::String,
            true,
        )],
    );
    let api = ApiSurface {
        crate_name: "demo-crate".into(),
        version: "0.1.0".into(),
        types: vec![trait_def],
        functions: vec![],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };

    let mut config = make_config();
    config.trait_bridges = vec![TraitBridgeConfig {
        trait_name: "OcrBackend".to_string(),
        super_trait: None,
        registry_getter: None,
        register_fn: None,

        unregister_fn: None,

        clear_fn: None,
        type_alias: None,
        param_name: None,
        register_extra_args: None,
        exclude_languages: vec!["dart".to_string()],
        ffi_skip_methods: Vec::new(),
        bind_via: alef::core::config::BridgeBinding::FunctionParam,
        options_type: None,
        options_field: None,
        context_type: None,
        result_type: None,
    }];

    let files = DartBackend.generate_bindings(&api, &config).unwrap();
    // The trait is excluded for dart, so traits.dart should not appear.
    assert!(
        find_traits_dart(&files).is_none(),
        "traits.dart should not be emitted when trait bridge excludes dart"
    );
}

#[test]
fn traits_dart_includes_typed_data_import_for_bytes_param() {
    let trait_def = make_trait(
        "ImageProcessor",
        "demo_crate::ImageProcessor",
        vec![make_method(
            "process",
            vec![make_param("raw", TypeRef::Bytes)],
            TypeRef::Bytes,
            true,
        )],
    );
    let api = ApiSurface {
        crate_name: "demo-crate".into(),
        version: "0.1.0".into(),
        types: vec![trait_def],
        functions: vec![],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };

    let files = DartBackend
        .generate_bindings(&api, &make_config_with_bridge("ImageProcessor"))
        .unwrap();
    let content = find_traits_dart(&files).expect("traits.dart should be emitted");

    assert!(
        content.contains("import 'dart:typed_data';"),
        "missing dart:typed_data import for Bytes type: {content}"
    );
    assert!(
        content.contains("Future<Uint8List> process(Uint8List raw);"),
        "missing method: {content}"
    );
}

#[test]
fn traits_dart_doc_comment_shows_registration_pattern() {
    let trait_def = make_trait(
        "OcrBackend",
        "demo_crate::OcrBackend",
        vec![make_method(
            "process_image",
            vec![make_param("bytes", TypeRef::Bytes)],
            TypeRef::String,
            true,
        )],
    );
    let api = ApiSurface {
        crate_name: "demo-crate".into(),
        version: "0.1.0".into(),
        types: vec![trait_def],
        functions: vec![],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };

    let files = DartBackend
        .generate_bindings(&api, &make_config_with_bridge("OcrBackend"))
        .unwrap();
    let content = find_traits_dart(&files).expect("traits.dart should be emitted");

    assert!(
        content.contains("createOcrBackendDartImpl("),
        "doc comment should show createOcrBackendDartImpl factory: {content}"
    );
    assert!(
        content.contains("class MyOcrBackend implements OcrBackend"),
        "doc comment should show example class: {content}"
    );
}

// ── Dart-side trait bridge wrapper methods ──────────────────────────────────

fn make_config_with_full_bridge(bridge_trait_name: &str) -> ResolvedCrateConfig {
    let mut config = make_config();
    config.trait_bridges = vec![TraitBridgeConfig {
        trait_name: bridge_trait_name.to_string(),
        super_trait: None,
        registry_getter: Some("demo_crate::plugins::registry::get_ocr_backend_registry".to_string()),
        register_fn: Some("register_ocr_backend".to_string()),
        unregister_fn: Some("unregister_ocr_backend".to_string()),
        clear_fn: Some("clear_ocr_backends".to_string()),
        type_alias: None,
        param_name: None,
        register_extra_args: None,
        exclude_languages: vec![],
        ffi_skip_methods: Vec::new(),
        bind_via: alef::core::config::BridgeBinding::FunctionParam,
        options_type: None,
        options_field: None,
        context_type: None,
        result_type: None,
    }];
    config
}

fn find_dart_src(files: &[alef::core::backend::GeneratedFile]) -> Option<&str> {
    files
        .iter()
        .find(|f| {
            let p = f.path.to_string_lossy();
            p.ends_with(".dart") && p.contains("/lib/src/") && !p.ends_with("traits.dart")
        })
        .map(|f| f.content.as_str())
}

/// When `register_fn`, `unregister_fn`, and `clear_fn` are all set, the generated Dart
/// wrapper class must contain matching static methods delegating to the FRB free functions.
#[test]
fn dart_bridge_class_emits_register_unregister_clear_wrappers_when_all_configured() {
    let trait_def = make_trait(
        "OcrBackend",
        "demo_crate::OcrBackend",
        vec![make_method(
            "extract_text",
            vec![make_param("data", TypeRef::Bytes)],
            TypeRef::String,
            true,
        )],
    );
    let api = ApiSurface {
        crate_name: "demo-crate".into(),
        version: "0.1.0".into(),
        types: vec![trait_def],
        functions: vec![],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };
    let config = make_config_with_full_bridge("OcrBackend");
    let files = DartBackend.generate_bindings(&api, &config).unwrap();
    let content = find_dart_src(&files).expect("src dart file should be emitted");

    // register wrapper
    assert!(
        content.contains("static Future<void> registerOcrBackend(OcrBackendDartImpl impl) async {"),
        "missing registerOcrBackend wrapper: {content}"
    );
    assert!(
        content.contains("await rust_bridge.registerOcrBackend(impl: impl);"),
        "registerOcrBackend must delegate to rust_bridge: {content}"
    );

    // unregister wrapper
    assert!(
        content.contains("static Future<void> unregisterOcrBackend(String name) async {"),
        "missing unregisterOcrBackend wrapper: {content}"
    );
    assert!(
        content.contains("await rust_bridge.unregisterOcrBackend(name: name);"),
        "unregisterOcrBackend must delegate to rust_bridge: {content}"
    );

    // clear wrapper
    assert!(
        content.contains("static Future<void> clearOcrBackends() async {"),
        "missing clearOcrBackends wrapper: {content}"
    );
    assert!(
        content.contains("await rust_bridge.clearOcrBackends();"),
        "clearOcrBackends must delegate to rust_bridge: {content}"
    );
}

/// When `unregister_fn` and `clear_fn` are both None, neither wrapper should be emitted.
#[test]
fn dart_bridge_class_does_not_emit_unregister_or_clear_when_not_configured() {
    let trait_def = make_trait(
        "Validator",
        "demo_crate::Validator",
        vec![make_method(
            "validate",
            vec![make_param("input", TypeRef::String)],
            TypeRef::Primitive(PrimitiveType::Bool),
            false,
        )],
    );
    let api = ApiSurface {
        crate_name: "demo-crate".into(),
        version: "0.1.0".into(),
        types: vec![trait_def],
        functions: vec![],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };
    // make_config_with_bridge leaves register_fn / unregister_fn / clear_fn all None
    let config = make_config_with_bridge("Validator");
    let files = DartBackend.generate_bindings(&api, &config).unwrap();

    // No Dart bridge class should be emitted at all when there are no functions
    // and no bridge methods to emit.
    let dart_src = find_dart_src(&files).expect("src dart file should still be emitted");
    assert!(
        !dart_src.contains("unregisterValidator"),
        "no unregister wrapper should be emitted: {dart_src}"
    );
    assert!(
        !dart_src.contains("clearValidators"),
        "no clear wrapper should be emitted: {dart_src}"
    );
}

// ── Bug regression: reserved Dart keyword `default` as enum variant name ────

#[test]
#[ignore = "FRB style: types/enums/errors are emitted by flutter_rust_bridge, not alef"]
fn enum_variant_named_default_is_escaped() {
    // Regression for: HtmlTheme::Default emitting bare `default` which is a
    // Dart reserved keyword, causing a parse error.
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![],
        enums: vec![EnumDef {
            name: "HtmlTheme".into(),
            rust_path: "demo::HtmlTheme".into(),
            original_rust_path: String::new(),
            variants: vec![
                EnumVariant {
                    name: "Default".into(),
                    fields: vec![],
                    doc: String::new(),
                    is_default: true,
                    serde_rename: None,
                    is_tuple: false,
                },
                EnumVariant {
                    name: "Dark".into(),
                    fields: vec![],
                    doc: String::new(),
                    is_default: false,
                    serde_rename: None,
                    is_tuple: false,
                },
            ],
            doc: String::new(),
            cfg: None,
            serde_tag: None,
            serde_untagged: false,
            serde_rename_all: None,
            is_copy: false,
            has_serde: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
        }],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };

    let files = DartBackend.generate_bindings(&api, &make_config()).unwrap();
    let content = &files[0].content;
    // `default` must be escaped to `default_`; bare `default` is a Dart reserved word.
    assert!(
        content.contains("default_"),
        "reserved keyword `default` must be escaped to `default_`: {content}"
    );
    assert!(
        !content.contains("  default,") && !content.contains("  default;"),
        "bare `default` identifier must not appear as enum variant: {content}"
    );
}

// ── Bug regression: numeric field name `0` from tuple-variant index ─────────

#[test]
#[ignore = "FRB style: types/enums/errors are emitted by flutter_rust_bridge, not alef"]
fn tuple_variant_with_numeric_field_name_is_escaped() {
    // Regression for: FormatMetadata::Pdf(PdfMetadata) emitting `final String 0;`
    // when the IR uses the string "0" as the tuple-field name.
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![],
        enums: vec![EnumDef {
            name: "FormatMetadata".into(),
            rust_path: "demo::FormatMetadata".into(),
            original_rust_path: String::new(),
            variants: vec![EnumVariant {
                name: "Pdf".into(),
                // Tuple-variant: the IR represents the unnamed field with name "0".
                fields: vec![make_field("0", TypeRef::String, false)],
                doc: String::new(),
                is_default: false,
                serde_rename: None,
                is_tuple: true,
            }],
            doc: String::new(),
            cfg: None,
            serde_tag: None,
            serde_untagged: false,
            serde_rename_all: None,
            is_copy: false,
            has_serde: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
        }],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };

    let files = DartBackend.generate_bindings(&api, &make_config()).unwrap();
    let content = &files[0].content;
    // The field must be renamed to `field0`, not bare `0`.
    assert!(
        content.contains("final String field0;"),
        "numeric field name `0` must be renamed to `field0`: {content}"
    );
    assert!(
        content.contains("Pdf(this.field0);"),
        "constructor must use `field0`: {content}"
    );
    assert!(
        !content.contains("final String 0;"),
        "bare numeric identifier `0` must not appear: {content}"
    );
}

#[test]
#[ignore = "FRB style: types/enums/errors are emitted by flutter_rust_bridge, not alef"]
fn error_message_template_strips_placeholders_and_escapes_special_chars() {
    // Templates carry `thiserror`-style `{name}` placeholders. The Dart Display
    // string must not leak those placeholders to runtime users — the resolved
    // string only ever contains the prose with the holes removed. Escaping
    // (single-quote, backslash, dollar) still applies to the surviving prose.
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![],
        enums: vec![],
        errors: vec![ErrorDef {
            name: "PluginError".into(),
            rust_path: "demo::PluginError".into(),
            original_rust_path: String::new(),
            variants: vec![
                ErrorVariant {
                    name: "Plugin".into(),
                    message_template: Some("Plugin error in '{plugin_name}': {message}".into()),
                    fields: vec![
                        make_field("plugin_name", TypeRef::String, false),
                        make_field("message", TypeRef::String, false),
                    ],
                    has_source: false,
                    has_from: false,
                    is_unit: false,
                    doc: String::new(),
                },
                ErrorVariant {
                    name: "WithBackslash".into(),
                    message_template: Some("Path C:\\Users\\{name}".into()),
                    fields: vec![make_field("name", TypeRef::String, false)],
                    has_source: false,
                    has_from: false,
                    is_unit: false,
                    doc: String::new(),
                },
                ErrorVariant {
                    name: "WithDollar".into(),
                    message_template: Some("Cost: ${amount}".into()),
                    fields: vec![make_field("amount", TypeRef::Primitive(PrimitiveType::I32), false)],
                    has_source: false,
                    has_from: false,
                    is_unit: false,
                    doc: String::new(),
                },
            ],
            doc: String::new(),
            methods: vec![],
            binding_excluded: false,
            binding_exclusion_reason: None,
        }],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };

    let files = DartBackend.generate_bindings(&api, &make_config()).unwrap();
    let content = &files[0].content;

    // No `{name}` substitution markers anywhere in emitted message strings.
    assert!(
        !content.contains("{plugin_name}") && !content.contains("{message}") && !content.contains("{amount}"),
        "thiserror placeholders must be stripped from runtime messages: {content}"
    );
    // The Plugin variant's prose was `Plugin error in '...': ...` — after
    // placeholder removal and trailing-punctuation cleanup it collapses to
    // `Plugin error in`. (No surviving stray quote, no leak.)
    assert!(
        content.contains("String get message => 'Plugin error in';"),
        "expected stripped Plugin message: {content}"
    );
    // Backslash in surviving prose is doubled so Dart doesn't treat it as escape.
    assert!(
        content.contains(r"String get message => 'Path C:\\Users\\';"),
        "backslashes in surviving prose must be escaped as \\\\: {content}"
    );
    // Dollar sign survives as prose; must be `\$` so Dart doesn't interpolate.
    assert!(
        content.contains(r"String get message => 'Cost: \$';"),
        "dollar sign in surviving prose must be escaped as \\$: {content}"
    );
}

#[test]
fn build_config_for_frb_emits_post_process_file_step() {
    use alef::core::backend::{PostBuildStep, PostProcessor};
    use std::path::PathBuf;

    let config = make_config(); // crate name = "demo-crate" → module = "demo_crate"
    let bc = DartBackend
        .build_config_for(&config)
        .expect("FRB style must yield a BuildConfig");

    let post_process_steps: Vec<&PostBuildStep> = bc
        .post_build
        .iter()
        .filter(|s| matches!(s, PostBuildStep::PostProcessFile { .. }))
        .collect();

    assert_eq!(
        post_process_steps.len(),
        5,
        "FRB config must have five PostProcessFile steps: (1) exclude_functions on lib.dart, \
         (2) sealed_variants on lib.dart, (3) optional_fields_with_defaults on lib.dart, \
         (4) exclude_functions on frb_generated.dart, (5) sealed_variants on frb_generated.dart \
         for the published-package native-lib loader"
    );

    let lib_dart_path = PathBuf::from("packages")
        .join("dart")
        .join("lib")
        .join("src")
        .join("demo_crate_bridge_generated")
        .join("lib.dart");
    let frb_generated_path = PathBuf::from("packages")
        .join("dart")
        .join("lib")
        .join("src")
        .join("demo_crate_bridge_generated")
        .join("frb_generated.dart");

    // (1) exclude_functions on lib.dart
    if let PostBuildStep::PostProcessFile { path, processor } = post_process_steps[0] {
        assert!(
            matches!(processor, PostProcessor::FrbDartExcludeFunctions(..)),
            "First PostProcessFile must use FrbDartExcludeFunctions processor"
        );
        assert_eq!(path, &lib_dart_path, "First PostProcessFile must target lib.dart");
    }

    // (2) sealed_variants on lib.dart
    if let PostBuildStep::PostProcessFile { path, processor } = post_process_steps[1] {
        assert_eq!(
            *processor,
            PostProcessor::FrbDartSealedVariants,
            "Second PostProcessFile must use FrbDartSealedVariants processor"
        );
        assert_eq!(path, &lib_dart_path, "Second PostProcessFile must target lib.dart");
    }

    // (3) optional_fields_with_defaults on lib.dart — makes Dart constructor fields with
    // Rust serde defaults optional.
    if let PostBuildStep::PostProcessFile { path, processor } = post_process_steps[2] {
        assert_eq!(
            *processor,
            PostProcessor::FrbDartOptionalFieldsWithDefaults,
            "Third PostProcessFile must use FrbDartOptionalFieldsWithDefaults processor"
        );
        assert_eq!(path, &lib_dart_path, "Third PostProcessFile must target lib.dart");
    }

    // (4) exclude_functions on frb_generated.dart — filters Rust FFI bridge wrappers like
    // `crateCalculateQualityScore` that are also emitted in frb_generated.dart.
    if let PostBuildStep::PostProcessFile { path, processor } = post_process_steps[3] {
        assert!(
            matches!(processor, PostProcessor::FrbDartExcludeFunctions(..)),
            "Fourth PostProcessFile must use FrbDartExcludeFunctions processor"
        );
        assert_eq!(
            path, &frb_generated_path,
            "Fourth PostProcessFile must target frb_generated.dart"
        );
    }

    // (5) sealed_variants on frb_generated.dart — reused for the published-package
    // native-lib loader injection (idempotent — the loader fix is keyed off the FRB
    // loader config present only in that file).
    if let PostBuildStep::PostProcessFile { path, processor } = post_process_steps[4] {
        assert_eq!(
            *processor,
            PostProcessor::FrbDartSealedVariants,
            "Fifth PostProcessFile must use FrbDartSealedVariants processor (for native-lib loader)"
        );
        assert_eq!(
            path, &frb_generated_path,
            "Fifth PostProcessFile must target frb_generated.dart for the loader injection"
        );
    }
}

#[test]
fn build_config_for_frb_run_command_precedes_post_process_file() {
    use alef::core::backend::PostBuildStep;

    let config = make_config();
    let bc = DartBackend
        .build_config_for(&config)
        .expect("FRB style must yield a BuildConfig");

    // RunCommand (flutter_rust_bridge_codegen) must run before PostProcessFile steps so the
    // generated lib.dart exists when the rewriters run.
    let steps: Vec<&str> = bc
        .post_build
        .iter()
        .map(|s| match s {
            PostBuildStep::RunCommand { .. } => "RunCommand",
            PostBuildStep::PostProcessFile { .. } => "PostProcessFile",
            PostBuildStep::PatchFile { .. } => "PatchFile",
        })
        .collect();

    assert_eq!(
        steps,
        vec![
            "RunCommand",
            "PostProcessFile",
            "PostProcessFile",
            "PostProcessFile",
            "PostProcessFile",
            "PostProcessFile"
        ],
        "RunCommand must come before all PostProcessFile steps in post_build steps"
    );
}

#[test]
fn build_config_for_frb_run_command_uses_config_file() {
    use alef::core::backend::PostBuildStep;

    let config = make_config();
    let bc = DartBackend
        .build_config_for(&config)
        .expect("FRB style must yield a BuildConfig");

    let run_command = bc
        .post_build
        .iter()
        .find_map(|step| match step {
            PostBuildStep::RunCommand { cmd, args } => Some((*cmd, args)),
            _ => None,
        })
        .expect("FRB config must run flutter_rust_bridge_codegen");

    assert_eq!(run_command.0, "flutter_rust_bridge_codegen");
    assert_eq!(
        run_command.1,
        &vec![
            "generate",
            "--config-file",
            "packages/dart/rust/flutter_rust_bridge.yaml"
        ],
        "flutter_rust_bridge_codegen must read the generated config file"
    );
}
