use alef::backends::swift::SwiftBackend;
use alef::core::backend::Backend;
use alef::core::config::{ResolvedCrateConfig, new_config::NewAlefConfig};
use alef::core::ir::{
    ApiSurface, CoreWrapper, EnumDef, EnumVariant, ErrorDef, ErrorVariant, FieldDef, FunctionDef, ParamDef,
    PrimitiveType, TypeDef, TypeRef,
};

// ── helpers ─────────────────────────────────────────────────────────────────

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 = ["swift"]

[[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)
}

// ── struct tests ─────────────────────────────────────────────────────────────

#[test]
fn struct_with_primitive_fields_emits_public_struct() {
    let ty = make_type(
        "Point",
        vec![
            make_field("x_coord", TypeRef::Primitive(PrimitiveType::I32), false),
            make_field("y_coord", TypeRef::Primitive(PrimitiveType::I32), false),
        ],
    );
    // Non-Codable structs become typealiases to RustBridge.X
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![ty],
        functions: vec![],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
    };

    let files = SwiftBackend.generate_bindings(&api, &make_config()).unwrap();
    // generate_bindings returns 1 Swift file + 3 Rust-crate files (Cargo.toml, src/lib.rs, build.rs)
    assert_eq!(files.len(), 4);
    let content = &files[0].content;

    assert!(
        content.contains("// Generated by alef. Do not edit by hand."),
        "missing header: {content}"
    );
    assert!(
        content.contains("import Foundation"),
        "missing Foundation import: {content}"
    );
    assert!(
        content.contains("public typealias Point = RustBridge.Point"),
        "missing typealias declaration: {content}"
    );
}

#[test]
fn rust_bridge_constructor_omits_binding_excluded_fields() {
    let mut internal_cache = make_field("internal_cache", TypeRef::String, false);
    internal_cache.binding_excluded = true;
    internal_cache.binding_exclusion_reason = Some("internal implementation detail".to_string());
    internal_cache.cfg = Some("feature = \"internal\"".to_string());

    let mut ty = make_type(
        "Config",
        vec![make_field("name", TypeRef::String, false), internal_cache],
    );
    ty.has_default = true;
    ty.has_serde = true;
    ty.has_stripped_cfg_fields = true;

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

    let files = SwiftBackend.generate_bindings(&api, &make_config()).unwrap();
    let rust_bridge = files
        .iter()
        .find(|file| {
            file.path
                .to_string_lossy()
                .replace('\\', "/")
                .ends_with("packages/swift/rust/src/lib.rs")
        })
        .expect("generated Rust bridge file must exist");

    assert!(
        !rust_bridge.content.contains("internal_cache"),
        "binding-excluded fields must not appear in Swift Rust bridge constructors or getters:\n{}",
        rust_bridge.content
    );
    assert!(
        rust_bridge.content.contains("fn new(name: String) -> Config;"),
        "extern constructor should contain only binding-visible fields:\n{}",
        rust_bridge.content
    );
    assert!(
        rust_bridge.content.contains("pub fn new(name: String) -> Config"),
        "wrapper constructor should contain only binding-visible fields:\n{}",
        rust_bridge.content
    );
}

#[test]
fn struct_with_optional_array_and_dict_fields() {
    // Non-Codable structs become typealiases to RustBridge.X
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![make_type(
            "Container",
            vec![
                make_field("tag", TypeRef::Optional(Box::new(TypeRef::String)), false),
                make_field("items", TypeRef::Vec(Box::new(TypeRef::String)), false),
                make_field(
                    "meta",
                    TypeRef::Map(
                        Box::new(TypeRef::String),
                        Box::new(TypeRef::Primitive(PrimitiveType::I64)),
                    ),
                    false,
                ),
            ],
        )],
        functions: vec![],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
    };

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

    assert!(
        content.contains("public typealias Container = RustBridge.Container"),
        "missing typealias declaration: {content}"
    );
}

#[test]
fn struct_with_serde_derives_codable() {
    // All non-trait structs become typealiases to RustBridge.X
    let mut ty = make_type(
        "Config",
        vec![make_field("value", TypeRef::Primitive(PrimitiveType::I32), false)],
    );
    ty.has_serde = true;
    ty.has_default = true;

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

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

    // Config now emits as a first-class Codable struct (has_serde: true, has fields)
    assert!(
        content.contains("public struct Config: Codable, Sendable, Hashable"),
        "expected first-class Codable struct: {content}"
    );
    assert!(
        content.contains("public let value: Int32"),
        "expected stored property: {content}"
    );
    // RustBridge typealias must NOT appear for first-class structs
    assert!(
        !content.contains("public typealias Config = RustBridge.Config"),
        "must not emit typealias for first-class struct: {content}"
    );
}

#[test]
fn primitive_only_serde_struct_without_default_emits_direct_bulk_constructor() {
    // Primitive-only serde DTOs (e.g. Point { row: u32, column: u32 },
    // ByteRange { start: usize, end: usize }) are positionally constructable via
    // swift-bridge even without a `Default` impl, so the Swift binding must emit a
    // first-class struct with a direct `RustBridge.{Type}(...)` `intoRust()` body —
    // NOT a typealias and NOT a `*FromJson` JSON-roundtrip path (the matching Rust
    // `*_from_json` shim would not link without the shared json-fallback predicate).
    let mut ty = make_type(
        "Config",
        vec![make_field("value", TypeRef::Primitive(PrimitiveType::I32), false)],
    );
    ty.has_serde = true;
    ty.has_default = false;

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

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

    assert!(
        !content.contains("public typealias Config = RustBridge.Config"),
        "primitive-only serde DTOs must NOT be reduced to a RustBridge typealias: {content}"
    );
    assert!(
        content.contains("public struct Config: Codable, Sendable, Hashable"),
        "primitive-only serde DTOs must emit a first-class Swift struct: {content}"
    );
    assert!(
        content.contains("return RustBridge.Config(self.value)"),
        "intoRust must call the direct bulk constructor, not a JSON shim: {content}"
    );
    // A top-level `configFromJson` forwarder is intentionally still emitted (every
    // from_json-eligible type gets one — see `emit_from_json_forwarders`), but it
    // routes through JSONDecoder, not `RustBridge.configFromJson`. The intoRust
    // path must remain the direct bulk constructor.
    assert!(
        !content.contains("return try RustBridge.configFromJson("),
        "intoRust must NOT route through *_from_json for primitive-only DTOs: {content}"
    );
}

#[test]
fn empty_struct_emits_single_line() {
    // Non-Codable structs become typealiases to RustBridge.X
    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(),
    };

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

    assert!(
        content.contains("public typealias Empty = RustBridge.Empty"),
        "missing typealias declaration: {content}"
    );
}

// ── enum tests ────────────────────────────────────────────────────────────────

#[test]
fn unit_only_enum_emits_lower_camel_cases() {
    // Non-Codable enums become typealiases to RustBridge.X
    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(),
    };

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

    assert!(
        content.contains("public typealias Status = RustBridge.Status"),
        "missing typealias declaration: {content}"
    );
}

#[test]
fn data_bearing_enum_emits_associated_values() {
    // Non-Codable enums become typealiases to RustBridge.X
    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: "Unit".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(),
    };

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

    assert!(
        content.contains("public typealias Shape = RustBridge.Shape"),
        "missing typealias declaration: {content}"
    );
}

#[test]
fn unit_enum_escapes_swift_keyword_variants_with_backticks() {
    // Reproduces the HtmlTheme regression in the kreuzberg Swift binding:
    // the `Default` variant must emit as `case `default`` (backtick-escaped),
    // not `case default_` (trailing-underscore). Non-keyword variants are
    // unaffected — `GitHub` stays as `case gitHub`.
    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: "GitHub".into(),
                    fields: vec![],
                    doc: String::new(),
                    is_default: false,
                    serde_rename: None,
                    is_tuple: false,
                },
                EnumVariant {
                    name: "Default".into(),
                    fields: vec![],
                    doc: String::new(),
                    is_default: false,
                    serde_rename: None,
                    is_tuple: false,
                },
                EnumVariant {
                    name: "Dracula".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: true triggers the native-Swift enum codepath in
            // emit_enum() (instead of the typealias fallback).
            has_serde: true,
            binding_excluded: false,
            binding_exclusion_reason: None,
        }],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
    };

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

    assert!(
        content.contains("case gitHub"),
        "expected non-keyword variant `GitHub` to emit as `case gitHub`: {content}"
    );
    assert!(
        content.contains("case `default`"),
        "expected reserved-keyword variant `Default` to emit as backtick-escaped `case `default``: {content}"
    );
    assert!(
        content.contains("case dracula"),
        "expected non-keyword variant `Dracula` to emit as `case dracula`: {content}"
    );
    assert!(
        !content.contains("case default_"),
        "must not emit trailing-underscore escape `case default_`: {content}"
    );
}

/// Repro of the spikard `SecuritySchemeInfo` bug: a serde-derived data-variant
/// enum whose associated values are bridge-safe primitives (String + Option<String>)
/// must be emitted as a `Codable` Swift enum so the generated `*FromJson`
/// JSONDecoder round-trip compiles. Without `Codable` conformance the generated
/// `intoRust()` extension and the `securitySchemeInfoFromJson(_:)` forwarder both
/// fail to compile because `JSONEncoder().encode(self)` requires `Encodable` and
/// `JSONDecoder().decode(SecuritySchemeInfo.self, ...)` requires `Decodable`.
#[test]
fn data_variant_serde_enum_with_bridge_safe_fields_emits_codable() {
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![],
        enums: vec![EnumDef {
            name: "SecuritySchemeInfo".into(),
            rust_path: "demo::SecuritySchemeInfo".into(),
            original_rust_path: String::new(),
            variants: vec![
                EnumVariant {
                    name: "Http".into(),
                    fields: vec![
                        make_field("scheme", TypeRef::String, false),
                        make_field("bearer_format", TypeRef::Optional(Box::new(TypeRef::String)), true),
                    ],
                    doc: String::new(),
                    is_default: false,
                    serde_rename: None,
                    is_tuple: false,
                },
                EnumVariant {
                    name: "ApiKey".into(),
                    fields: vec![
                        make_field("location", TypeRef::String, false),
                        make_field("name", TypeRef::String, false),
                    ],
                    doc: String::new(),
                    is_default: false,
                    serde_rename: None,
                    is_tuple: false,
                },
            ],
            doc: String::new(),
            cfg: None,
            serde_tag: Some("type".into()),
            serde_untagged: false,
            serde_rename_all: Some("lowercase".into()),
            is_copy: false,
            has_serde: true,
            binding_excluded: false,
            binding_exclusion_reason: None,
        }],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
    };

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

    assert!(
        content.contains("public enum SecuritySchemeInfo: Codable"),
        "data-variant serde enum with bridge-safe fields must conform to Codable so JSONEncoder/JSONDecoder round-trip compiles. Got:\n{content}"
    );
    assert!(
        !content.contains("public enum SecuritySchemeInfo {\n"),
        "must NOT emit bare `public enum SecuritySchemeInfo {{` without Codable conformance:\n{content}"
    );
}

/// When a data-variant serde enum references a Named type that is NOT in
/// `known_dto_names`, `emit_enum` falls back to a typealias to the opaque
/// `RustBridge.{Enum}` class (Swift cannot synthesise `Codable` for the data
/// variant). In that case the `*FromJson` forwarder must NOT use `JSONDecoder`
/// — opaque bridge classes are not Codable — and must instead forward to the
/// `RustBridge.{enum}FromJson` shim that the Rust crate emits.
#[test]
fn data_variant_serde_enum_with_opaque_field_falls_back_to_rust_bridge_from_json() {
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![],
        enums: vec![EnumDef {
            name: "TaggedPayload".into(),
            rust_path: "demo::TaggedPayload".into(),
            original_rust_path: String::new(),
            variants: vec![
                EnumVariant {
                    name: "Plain".into(),
                    fields: vec![make_field("text", TypeRef::String, false)],
                    doc: String::new(),
                    is_default: false,
                    serde_rename: None,
                    is_tuple: false,
                },
                EnumVariant {
                    // `OpaqueHandle` is not declared anywhere, so it is not in
                    // `known_dto_names`; `all_variants_codable_safe` returns false.
                    name: "Handle".into(),
                    fields: vec![make_field("handle", TypeRef::Named("OpaqueHandle".into()), false)],
                    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: true,
            binding_excluded: false,
            binding_exclusion_reason: None,
        }],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
    };

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

    // The enum itself falls back to a typealias.
    assert!(
        content.contains("public typealias TaggedPayload = RustBridge.TaggedPayload"),
        "data-variant enum with non-Codable-safe fields must fall back to typealias:\n{content}"
    );
    // The `*FromJson` forwarder must NOT JSONDecoder-decode an opaque bridge
    // class — it must forward to the Rust-side shim.
    assert!(
        content.contains("public func taggedPayloadFromJson(_ json: String) throws -> TaggedPayload"),
        "fromJson forwarder must still be emitted for typealiased serde enums:\n{content}"
    );
    assert!(
        content.contains("return try RustBridge.taggedPayloadFromJson(json)"),
        "fromJson forwarder must forward to RustBridge.*FromJson for typealiased enums:\n{content}"
    );
    assert!(
        !content.contains("JSONDecoder().decode(TaggedPayload.self"),
        "must NOT JSONDecoder-decode an opaque RustBridge typealias enum:\n{content}"
    );
}

/// Repro of the spikard `schemaQueryOnly()` bug: a non-throwing free function
/// whose return type is a first-class DTO must wrap the bridge call in the
/// `try {Dto}(_:)` converter, otherwise Swift rejects the body with
/// `cannot convert return expression of type 'RustBridge.QueryOnlyConfig' to
/// return type '{module}.QueryOnlyConfig'`. The forwarder signature widens to
/// `throws` because the converter is throwing.
#[test]
fn nullary_free_function_returning_named_dto_wraps_bridge_call_in_converter() {
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![TypeDef {
            name: "QueryOnlyConfig".to_string(),
            rust_path: "demo::QueryOnlyConfig".to_string(),
            original_rust_path: String::new(),
            fields: vec![
                make_field("name", TypeRef::String, false),
                make_field("limit", TypeRef::Primitive(PrimitiveType::U32), false),
            ],
            methods: vec![],
            is_opaque: false,
            is_clone: true,
            is_copy: false,
            doc: "Schema config.".to_string(),
            cfg: None,
            is_trait: false,
            has_default: true,
            has_stripped_cfg_fields: false,
            is_return_type: true,
            serde_rename_all: None,
            // has_serde + non-empty primitive fields → first-class Codable
            // struct emission, which seeds `known_dto_names`.
            has_serde: true,
            super_traits: vec![],
            binding_excluded: false,
            binding_exclusion_reason: None,
        }],
        functions: vec![FunctionDef {
            name: "schema_query_only".into(),
            rust_path: "demo::schema_query_only".into(),
            original_rust_path: String::new(),
            params: vec![],
            return_type: TypeRef::Named("QueryOnlyConfig".to_string()),
            is_async: false,
            error_type: None,
            doc: "Default schema config.".to_string(),
            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(),
    };

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

    assert!(
        content.contains("public func schemaQueryOnly() throws -> QueryOnlyConfig"),
        "free function returning a Named DTO must declare `throws` because the converter is throwing:\n{content}"
    );
    assert!(
        content.contains("let _rb = RustBridge.schemaQueryOnly()"),
        "forwarder must bind the bridge result to a local before the converter call:\n{content}"
    );
    assert!(
        content.contains("return try QueryOnlyConfig(_rb)"),
        "forwarder must wrap the bridge value in the `try {{Dto}}(_:)` converter:\n{content}"
    );
    assert!(
        !content.contains("return RustBridge.schemaQueryOnly()"),
        "forwarder must NOT return the low-level `RustBridge.{{Dto}}` value directly — it does not satisfy the declared type:\n{content}"
    );
}

// ── function tests ────────────────────────────────────────────────────────────

#[test]
fn sync_function_emits_public_static_func() {
    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(),
    };

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

    // Function wrappers are disabled (Phase 2D). No module enum wrapper or static func emitted.
    assert!(
        !content.contains("public enum DemoCrate {"),
        "function wrappers should be disabled, no module enum wrapper: {content}"
    );
    assert!(
        !content.contains("public static func greetUser(userName: String)"),
        "function wrappers should be disabled, no static func: {content}"
    );
}

#[test]
fn async_function_emits_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(),
    };

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

    // Function wrappers are disabled (Phase 2D). Async qualifiers not emitted.
    assert!(
        !content.contains("public static func fetchData() async"),
        "function wrappers disabled, no async static func: {content}"
    );
}

#[test]
fn error_throwing_function_emits_throws() {
    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::Primitive(PrimitiveType::I32),
            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(),
    };

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

    // Function wrappers are disabled (Phase 2D). Throws qualifiers not emitted.
    assert!(
        !content.contains("public static func parseInput(raw: String) throws"),
        "function wrappers disabled, no throws static func: {content}"
    );
}

#[test]
fn async_throws_function_emits_both_qualifiers() {
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![FunctionDef {
            name: "load".into(),
            rust_path: "demo::load".into(),
            original_rust_path: String::new(),
            params: vec![],
            return_type: TypeRef::String,
            is_async: true,
            error_type: Some("IoError".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(),
    };

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

    // Function wrappers are disabled (Phase 2D). Async throws qualifiers not emitted.
    assert!(
        !content.contains("public static func load() async throws"),
        "function wrappers disabled, no async throws static func: {content}"
    );
}

// ── error tests ───────────────────────────────────────────────────────────────

#[test]
fn error_enum_conforms_to_error_protocol() {
    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: "Timeout".into(),
                    message_template: Some("Request timed out".into()),
                    fields: vec![],
                    has_source: false,
                    has_from: false,
                    is_unit: true,
                    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(),
    };

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

    assert!(
        content.contains("public enum ApiError: Swift.Error {"),
        "missing Swift.Error conformance: {content}"
    );
    assert!(content.contains("case notFound\n"), "missing notFound case: {content}");
    assert!(content.contains("case timeout\n"), "missing timeout case: {content}");
}

#[test]
fn error_enum_named_error_is_renamed_to_module_error() {
    // When the Rust error type is literally named `Error`, Swift parses
    // `public enum Error: Error` as a circular raw-type binding rather than
    // protocol conformance, causing compile errors. The codegen must rename it
    // to `{ModuleName}Error` (here `DemoCrateError`) and qualify the protocol
    // as `Swift.Error`.
    let api = ApiSurface {
        crate_name: "demo-crate".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![],
        enums: vec![],
        errors: vec![ErrorDef {
            name: "Error".into(),
            rust_path: "demo_crate::Error".into(),
            original_rust_path: String::new(),
            variants: vec![ErrorVariant {
                name: "NotFound".into(),
                message_template: Some("not found".into()),
                fields: vec![],
                has_source: false,
                has_from: false,
                is_unit: true,
                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(),
    };

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

    // Must NOT emit `public enum Error: Error` — that is a Swift compile error.
    assert!(
        !content.contains("public enum Error: Error"),
        "bare `Error: Error` must not be emitted: {content}"
    );
    // Must rename to `{ModuleName}Error` and qualify with `Swift.Error`.
    assert!(
        content.contains("public enum DemoCrateError: Swift.Error {"),
        "renamed enum must be emitted as `DemoCrateError: Swift.Error`: {content}"
    );
    assert!(content.contains("case notFound\n"), "missing notFound case: {content}");
}

#[test]
fn error_enum_with_methods_emits_extension_properties() {
    use alef::core::ir::{MethodDef, ReceiverKind};

    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("not found".into()),
                    fields: vec![],
                    has_source: false,
                    has_from: false,
                    is_unit: true,
                    doc: String::new(),
                },
                ErrorVariant {
                    name: "ServerError".into(),
                    message_template: Some("server error".into()),
                    fields: vec![make_field("status", TypeRef::Primitive(PrimitiveType::U16), false)],
                    has_source: false,
                    has_from: false,
                    is_unit: false,
                    doc: String::new(),
                },
            ],
            doc: String::new(),
            methods: vec![
                MethodDef {
                    name: "status_code".into(),
                    params: vec![],
                    return_type: TypeRef::Primitive(PrimitiveType::U16),
                    is_async: false,
                    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,
                },
                MethodDef {
                    name: "is_transient".into(),
                    params: vec![],
                    return_type: TypeRef::Primitive(PrimitiveType::Bool),
                    is_async: false,
                    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,
                },
                MethodDef {
                    name: "error_type".into(),
                    params: vec![],
                    return_type: TypeRef::String,
                    is_async: false,
                    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,
                },
            ],
            binding_excluded: false,
            binding_exclusion_reason: None,
        }],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
    };

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

    // Extension block must be emitted after the enum.
    assert!(
        content.contains("extension ApiError {"),
        "missing extension block: {content}"
    );
    // statusCode property as UInt16.
    assert!(
        content.contains("public var statusCode: UInt16 {"),
        "missing statusCode property: {content}"
    );
    // isTransient property as Bool.
    assert!(
        content.contains("public var isTransient: Bool {"),
        "missing isTransient property: {content}"
    );
    // errorType property as String.
    assert!(
        content.contains("public var errorType: String {"),
        "missing errorType property: {content}"
    );
    // switch self must be emitted inside each property.
    assert!(content.contains("switch self {"), "missing switch self: {content}");
}

// ── convenience wrapper tests ─────────────────────────────────────────────────

/// Helper: build a FunctionDef with no error type and not async.
fn make_sync_fn(name: &str, params: Vec<ParamDef>, return_type: TypeRef) -> FunctionDef {
    FunctionDef {
        name: name.to_string(),
        rust_path: format!("demo::{name}"),
        original_rust_path: String::new(),
        params,
        return_type,
        is_async: false,
        error_type: Some("DemoError".to_string()),
        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,
    }
}

/// Helper: build an optional ParamDef.
fn make_optional_param(name: &str, ty: TypeRef) -> ParamDef {
    ParamDef {
        name: name.to_string(),
        ty,
        optional: true,
        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,
    }
}

#[test]
fn bytes_first_param_skips_overload_when_name_shadows_bridge() {
    // IR: analyze_bytes(content: Bytes, config: AnalyzeConfig) -> AnalyzeResult
    // The convenience wrapper name (`analyzeBytes`) collides with the bridge
    // function name (`analyzeBytes`) because there is no `_sync` suffix to
    // strip. Swift overload resolution would resolve the unlabeled inner
    // positional call to the wrapper itself (recursive shadow), and module-
    // prefix qualification of free functions is rejected by the compiler. We
    // therefore skip emitting the convenience overload entirely; users call
    // the bridge function directly with `makeByteVec(...)`.
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![make_sync_fn(
            "analyze_bytes",
            vec![
                make_param("content", TypeRef::Bytes),
                make_param("config", TypeRef::Named("AnalyzeConfig".into())),
            ],
            TypeRef::Named("AnalyzeResult".into()),
        )],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
    };

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

    // No convenience overload for `analyzeBytes` — the only function in the
    // surface would shadow itself.
    assert!(
        !content.contains("public func analyzeBytes(\n    content: String"),
        "shadowing String overload must be skipped: {content}"
    );
    assert!(
        !content.contains("public func analyzeBytes(\n    content: [UInt8]"),
        "shadowing [UInt8] overload must be skipped: {content}"
    );

    // Since this is the only candidate, no convenience-wrapper section header
    // should be emitted at all.
    assert!(
        !content.contains("// MARK: - Convenience Wrapper Functions"),
        "wrapper section must not appear when all candidates shadow: {content}"
    );
    assert!(
        !content.contains("makeByteVec"),
        "makeByteVec must not appear when all candidates shadow: {content}"
    );
}

#[test]
fn bytes_overload_with_string_return_appends_to_string_for_throws() {
    // IR: detect_mime_type_from_bytes_sync(content: Bytes) -> Result<String, E>
    //
    // swift-bridge maps `Result<String, _>` to `throws -> RustString` (NOT
    // Swift-native String — the error path forces the union through the
    // RustString ABI). The forwarder must call `.toString()` on the bridge
    // result to coerce to the native Swift `String` declared on the public
    // signature. Without this the Swift compiler rejects the body with
    // `cannot convert return expression of type 'RustString' to return
    // type 'String'`.
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![make_sync_fn(
            "detect_mime_sync",
            vec![make_param("content", TypeRef::Bytes)],
            TypeRef::String,
        )],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
    };

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

    // Wrapper exists (no shadow: `detectMime` ≠ `detectMimeSync`).
    assert!(
        content.contains("public func detectMime(\n    content: String"),
        "missing detectMime String overload: {content}"
    );

    // Bridge result must have `.toString()` appended to coerce RustString → String.
    assert!(
        content.contains("return try RustBridge.detectMimeSync(_rb_content).toString()"),
        "must append .toString() to coerce RustString to Swift String: {content}"
    );
}

#[test]
fn bytes_sync_suffix_stripped_in_wrapper_name() {
    // IR: process_bytes_sync(content: Bytes, config: ProcessConfig) -> ProcessResult
    // Wrapper name should be `processBytes` (strip "Sync"), inner call `processBytesSync`.
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![make_sync_fn(
            "process_bytes_sync",
            vec![
                make_param("content", TypeRef::Bytes),
                make_param("config", TypeRef::Named("ProcessConfig".into())),
            ],
            TypeRef::Named("ProcessResult".into()),
        )],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
    };

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

    // Public wrapper is named `processBytes` (Sync stripped)
    assert!(
        content.contains("public func processBytes(\n    content: String"),
        "missing String overload with Sync stripped: {content}"
    );
    assert!(
        content.contains("public func processBytes(\n    content: [UInt8]"),
        "missing [UInt8] overload with Sync stripped: {content}"
    );
    // Inner call delegates to `processBytesSync`
    assert!(
        content.contains("return try processBytesSync(makeByteVec("),
        "inner call should use processBytesSync: {content}"
    );
}

#[test]
fn path_first_param_emits_string_path_overload() {
    // IR: load_file_sync(path: Path, mime_type: String (optional), cfg: LoadConfig) -> LoadResult
    // Should emit a `loadFile` wrapper accepting `path: String`.
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![make_sync_fn(
            "load_file_sync",
            vec![
                make_param("path", TypeRef::Path),
                make_optional_param("mime_type", TypeRef::String),
                make_param("cfg", TypeRef::Named("LoadConfig".into())),
            ],
            TypeRef::Named("LoadResult".into()),
        )],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
    };

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

    // Public wrapper is `loadFile` (Sync stripped)
    assert!(
        content.contains("public func loadFile(\n    path: String"),
        "missing String path overload for loadFile: {content}"
    );
    // Inner call delegates to `loadFileSync`
    assert!(
        content.contains("return try loadFileSync(path"),
        "inner call should use loadFileSync: {content}"
    );
    // Optional mime_type gets `= nil` default
    assert!(
        content.contains("mimeType: String? = nil"),
        "optional mime_type should have nil default: {content}"
    );
}

#[test]
fn no_bytes_or_path_functions_emits_no_wrapper_section() {
    // IR with only a String-param function — no convenience wrappers should be emitted.
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![make_sync_fn(
            "greet",
            vec![make_param("name", TypeRef::String)],
            TypeRef::String,
        )],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
    };

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

    assert!(
        !content.contains("// MARK: - Convenience Wrapper Functions"),
        "wrapper section must not appear when no bytes/path candidates: {content}"
    );
    assert!(
        !content.contains("makeByteVec"),
        "makeByteVec must not appear when no bytes candidates: {content}"
    );
}

#[test]
fn async_bytes_function_emits_async_forwarder() {
    // Async bytes functions are forwarded through a detached task with byte-vector conversion.
    let async_fn = FunctionDef {
        name: "fetch_bytes".to_string(),
        rust_path: "demo::fetch_bytes".to_string(),
        original_rust_path: String::new(),
        params: vec![
            make_param("content", TypeRef::Bytes),
            make_param("config", TypeRef::Named("FetchConfig".into())),
        ],
        return_type: TypeRef::Named("FetchResult".into()),
        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,
    };

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

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

    assert!(
        content.contains("public func fetchBytes(content: [UInt8], config: FetchConfig) async -> FetchResult"),
        "async bytes function should generate an async forwarder: {content}"
    );
    assert!(
        content.contains("let _rb_content: RustVec<UInt8>"),
        "async bytes forwarder should convert byte parameters: {content}"
    );
    assert!(
        content.contains("return await Task.detached(priority: .userInitiated)"),
        "async bytes forwarder should call through a detached task: {content}"
    );
}

// ── output path tests ─────────────────────────────────────────────────────────

#[test]
fn output_path_uses_pascal_case_module_name() {
    let api = ApiSurface {
        crate_name: "demo-crate".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(),
    };

    let files = SwiftBackend.generate_bindings(&api, &make_config()).unwrap();
    // generate_bindings returns 1 Swift file + 3 Rust-crate files (Cargo.toml, src/lib.rs, build.rs)
    assert_eq!(files.len(), 4);

    // The Swift wrapper must be a .swift file with PascalCase module name.
    // When using ResolvedCrateConfig, output_paths["swift"] is set from the template
    // to packages/swift/, so the file lands at packages/swift/DemoCrate.swift.
    let swift_file = files
        .iter()
        .find(|f| f.path.to_string_lossy().ends_with(".swift"))
        .unwrap();
    let path = swift_file.path.to_string_lossy();
    assert!(path.contains("DemoCrate.swift"), "unexpected output path: {path}");
}

// ── streaming-method emission tests ──────────────────────────────────────────

/// Helper: build a `ResolvedCrateConfig` with one streaming adapter on
/// `DefaultClient` plus a `client_constructor_body` override (required to make
/// `emit_client_class` consider the owner type).
fn make_streaming_config() -> ResolvedCrateConfig {
    let toml = r#"
[workspace]
languages = ["swift"]

[[crates]]
name = "demo-crate"
sources = ["src/lib.rs"]

[[crates.adapters]]
name = "chat_stream"
pattern = "streaming"
core_path = "demo_crate::chat_stream"
owner_type = "DefaultClient"
item_type = "ChatCompletionChunk"
error_type = "DemoError"

[[crates.adapters.params]]
name = "req"
type = "demo_crate::ChatCompletionRequest"

[crates.swift]
client_constructor_body.DefaultClient = "Self { inner: ::demo_crate::DefaultClient::new(api_key, base_url) }"
"#;
    let cfg: NewAlefConfig = toml::from_str(toml).expect("test config must parse");
    cfg.resolve().expect("test config must resolve").remove(0)
}

/// Build an `ApiSurface` exposing `DefaultClient` as an opaque type so the
/// gen_bindings pipeline emits a public Swift class wrapper for it.
fn make_streaming_api() -> ApiSurface {
    use alef::core::ir::{MethodDef, ReceiverKind};
    ApiSurface {
        crate_name: "demo-crate".into(),
        version: "0.1.0".into(),
        types: vec![TypeDef {
            name: "DefaultClient".to_string(),
            rust_path: "demo_crate::DefaultClient".to_string(),
            original_rust_path: String::new(),
            fields: vec![],
            methods: vec![MethodDef {
                name: "ping".to_string(),
                params: vec![],
                return_type: TypeRef::String,
                is_async: false,
                is_static: false,
                error_type: None,
                doc: String::new(),
                sanitized: false,
                returns_ref: false,
                returns_cow: false,
                return_newtype_wrapper: None,
                receiver: Some(ReceiverKind::Ref),
                trait_source: None,
                has_default_impl: false,
                binding_excluded: false,
                binding_exclusion_reason: None,
            }],
            is_opaque: true,
            is_clone: false,
            is_copy: false,
            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![],
            doc: String::new(),
            cfg: None,
            binding_excluded: false,
            binding_exclusion_reason: None,
        }],
        functions: vec![],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
    }
}

/// The streaming wrapper must return an `AsyncThrowingStream<Item, Error>` so
/// Swift consumers can drive it with `for try await chunk in stream`.
#[test]
fn streaming_adapter_emits_async_throwing_stream_wrapper() {
    let api = make_streaming_api();
    let config = make_streaming_config();
    let files = SwiftBackend.generate_bindings(&api, &config).unwrap();
    let swift = files
        .iter()
        .find(|f| f.path.to_string_lossy().ends_with(".swift"))
        .unwrap();

    assert!(
        swift.content.contains(
            "public func chatStream(_ req: ChatCompletionRequest) async throws -> AsyncThrowingStream<ChatCompletionChunk, Error>"
        ),
        "streaming wrapper must declare AsyncThrowingStream return type; got:\n{}",
        swift.content
    );
    assert!(
        swift.content.contains("RustBridge.defaultClientChatStreamStart(inner"),
        "streaming wrapper must call defaultClientChatStreamStart with the owner handle; got:\n{}",
        swift.content
    );
    // Must drain via handle.next() in a detached task.
    assert!(
        swift.content.contains("handle.next().toString()"),
        "streaming wrapper must drain via handle.next(); got:\n{}",
        swift.content
    );
    // Must terminate cleanly on the empty-string EOF sentinel.
    assert!(
        swift.content.contains("if json.isEmpty { break }"),
        "streaming wrapper must break on empty-string EOF sentinel; got:\n{}",
        swift.content
    );
    // Must deserialise each chunk via the Rust-side from_json bridge function so the
    // yielded value is the opaque swift-bridge type (with full method API available).
    assert!(
        swift.content.contains("RustBridge.chatCompletionChunkFromJson(json)"),
        "streaming wrapper must call RustBridge.chatCompletionChunkFromJson to deserialise each chunk; got:\n{}",
        swift.content
    );
    // Must cancel the drain task on stream termination so the Rust handle's deinit runs.
    assert!(
        swift
            .content
            .contains("continuation.onTermination = { _ in task.cancel() }"),
        "streaming wrapper must cancel the drain task onTermination; got:\n{}",
        swift.content
    );
}

/// Capability flag must reflect the new streaming support so downstream tooling
/// (alef CLI, e2e generators) emits Swift streaming fixtures.
#[test]
fn swift_backend_reports_supports_streaming_true() {
    let backend = SwiftBackend;
    let capabilities = alef::core::backend::Backend::capabilities(&backend);
    assert!(
        capabilities.supports_streaming,
        "SwiftBackend must report supports_streaming = true now that AsyncThrowingStream is implemented"
    );
}

// ── Defect 1: StreamHandle @unchecked Sendable ───────────────────────────────

/// Each streaming adapter's `{Owner}{Adapter}StreamHandle` must have an
/// `extension RustBridge.XStreamHandle: @unchecked Sendable {}` emitted in the
/// generated Swift file.
///
/// Swift 6 strict-concurrency rejects passing non-Sendable types across
/// `Task.detached` boundaries.  The Rust side wraps `Mutex<stream>` and a
/// `tokio::runtime::Runtime` — both thread-safe — so `@unchecked Sendable` is
/// the correct annotation.
#[test]
fn streaming_handle_emits_unchecked_sendable_extension() {
    let api = make_streaming_api();
    let config = make_streaming_config();
    let files = SwiftBackend.generate_bindings(&api, &config).unwrap();
    let swift = files
        .iter()
        .find(|f| f.path.to_string_lossy().ends_with(".swift"))
        .unwrap();

    assert!(
        swift
            .content
            .contains("extension RustBridge.DefaultClientChatStreamStreamHandle: @unchecked Sendable {}"),
        "generated Swift must declare @unchecked Sendable for the StreamHandle so Swift 6 \
         strict-concurrency allows passing it across Task.detached boundaries; got:\n{}",
        swift.content
    );
}

// ── Defect 2: streaming chunk type emits as Codable struct ───────────────────

/// When the streaming adapter's `item_type` appears in `api.types` with
/// `has_serde: true` and populated fields, the Swift backend must emit a native
/// `public struct X: Codable { … }` instead of an opaque typealias.
///
/// The streaming wrapper calls `JSONDecoder().decode(X.self, from: data)`, which
/// requires `X: Decodable`.  An `RustBridge.X` typealias (swift-bridge opaque
/// class) does not satisfy that requirement.
#[test]
fn streaming_chunk_type_with_serde_and_fields_emits_codable_struct() {
    use alef::core::ir::{MethodDef, ReceiverKind};

    // Build an API surface with:
    // - DefaultClient (opaque, has_serde: false) — the streaming owner
    // - ChatCompletionChunk (non-opaque, has_serde/default, with fields) — the item type
    let api = ApiSurface {
        crate_name: "demo-crate".into(),
        version: "0.1.0".into(),
        types: vec![
            TypeDef {
                name: "DefaultClient".to_string(),
                rust_path: "demo_crate::DefaultClient".to_string(),
                original_rust_path: String::new(),
                fields: vec![],
                methods: vec![MethodDef {
                    name: "ping".to_string(),
                    params: vec![],
                    return_type: TypeRef::String,
                    is_async: false,
                    is_static: false,
                    error_type: None,
                    doc: String::new(),
                    sanitized: false,
                    returns_ref: false,
                    returns_cow: false,
                    return_newtype_wrapper: None,
                    receiver: Some(ReceiverKind::Ref),
                    trait_source: None,
                    has_default_impl: false,
                    binding_excluded: false,
                    binding_exclusion_reason: None,
                }],
                is_opaque: true,
                is_clone: false,
                is_copy: false,
                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![],
                doc: String::new(),
                cfg: None,
                binding_excluded: false,
                binding_exclusion_reason: None,
            },
            TypeDef {
                name: "ChatCompletionChunk".to_string(),
                rust_path: "demo_crate::ChatCompletionChunk".to_string(),
                original_rust_path: String::new(),
                fields: vec![
                    make_field("id", TypeRef::String, false),
                    make_field("content", TypeRef::Optional(Box::new(TypeRef::String)), true),
                ],
                methods: vec![],
                is_opaque: false,
                is_clone: true,
                is_copy: false,
                is_trait: false,
                has_default: true,
                has_stripped_cfg_fields: false,
                is_return_type: true,
                serde_rename_all: None,
                // has_serde: true — the type derives Serialize + Deserialize
                has_serde: true,
                super_traits: vec![],
                doc: String::new(),
                cfg: None,
                binding_excluded: false,
                binding_exclusion_reason: None,
            },
        ],
        functions: vec![],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
    };

    let toml = r#"
[workspace]
languages = ["swift"]

[[crates]]
name = "demo-crate"
sources = ["src/lib.rs"]

[[crates.adapters]]
name = "chat_stream"
pattern = "streaming"
core_path = "demo_crate::chat_stream"
owner_type = "DefaultClient"
item_type = "ChatCompletionChunk"
error_type = "DemoError"

[[crates.adapters.params]]
name = "req"
type = "demo_crate::ChatCompletionRequest"

[crates.swift]
client_constructor_body.DefaultClient = "Self { inner: ::demo_crate::DefaultClient::new(api_key, base_url) }"
"#;
    let cfg: NewAlefConfig = toml::from_str(toml).expect("test config must parse");
    let config = cfg.resolve().expect("test config must resolve").remove(0);
    let files = SwiftBackend.generate_bindings(&api, &config).unwrap();
    let swift = files
        .iter()
        .find(|f| f.path.to_string_lossy().ends_with(".swift"))
        .unwrap();

    // Streaming item types with has_serde + fields now emit as first-class Codable structs.
    // The streaming wrapper uses JSONDecoder directly, not the RustBridge shim.
    assert!(
        swift
            .content
            .contains("public struct ChatCompletionChunk: Codable, Sendable, Hashable"),
        "streaming item type must be emitted as a first-class Codable struct; got:\n{}",
        swift.content
    );
    // Must NOT emit an opaque typealias — first-class structs replace typealiases.
    assert!(
        !swift
            .content
            .contains("typealias ChatCompletionChunk = RustBridge.ChatCompletionChunk"),
        "must not emit typealias for first-class Codable struct; got:\n{}",
        swift.content
    );
    // The streaming wrapper must use JSONDecoder for first-class struct chunks.
    assert!(
        swift
            .content
            .contains("JSONDecoder().decode(ChatCompletionChunk.self, from: chunkData)"),
        "streaming wrapper must use JSONDecoder for first-class struct; got:\n{}",
        swift.content
    );
}

/// Methods on opaque client classes must surface their rustdoc as `///` lines
/// immediately above the `public func …` declaration, routed through the same
/// `emit_doc_comment` helper used for types/enums/errors so formatting matches.
#[test]
fn opaque_class_method_emits_doc_comment_above_signature() {
    use alef::core::ir::{MethodDef, ReceiverKind};

    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![TypeDef {
            name: "Client".to_string(),
            rust_path: "demo::Client".to_string(),
            original_rust_path: String::new(),
            fields: vec![],
            methods: vec![MethodDef {
                name: "ping".to_string(),
                params: vec![],
                return_type: TypeRef::String,
                is_async: false,
                is_static: false,
                error_type: None,
                doc: "Send a ping request.\nReturns the server greeting.".to_string(),
                sanitized: false,
                returns_ref: false,
                returns_cow: false,
                return_newtype_wrapper: None,
                receiver: Some(ReceiverKind::Ref),
                trait_source: None,
                has_default_impl: false,
                binding_excluded: false,
                binding_exclusion_reason: None,
            }],
            is_opaque: true,
            is_clone: false,
            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,
        }],
        functions: vec![],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
    };

    let toml = r#"
[workspace]
languages = ["swift"]

[[crates]]
name = "demo-crate"
sources = ["src/lib.rs"]

[crates.swift]
client_constructor_body.Client = "Self { inner: ::demo::Client::new(api_key, base_url) }"
"#;
    let cfg: NewAlefConfig = toml::from_str(toml).expect("test config must parse");
    let config = cfg.resolve().expect("test config must resolve").remove(0);
    let files = SwiftBackend.generate_bindings(&api, &config).unwrap();
    let swift = files
        .iter()
        .find(|f| f.path.to_string_lossy().ends_with(".swift"))
        .unwrap();

    assert!(
        swift.content.contains("    /// Send a ping request.\n"),
        "method doc summary line must be emitted; got:\n{}",
        swift.content
    );
    assert!(
        swift.content.contains("    /// Returns the server greeting.\n"),
        "method doc continuation line must be emitted; got:\n{}",
        swift.content
    );
    assert!(
        swift
            .content
            .contains("    /// Returns the server greeting.\n    public func ping()"),
        "method doc must sit immediately above the signature; got:\n{}",
        swift.content
    );
}

// ── first-class DTO call-site conversion tests ───────────────────────────────

/// When a method on an opaque client class takes a first-class Swift DTO as a
/// parameter, the generated call site must apply `.intoRust()` to convert the
/// Swift wrapper into the `RustBridge.T` raw type that the bridge function
/// expects.  Without this conversion the Swift compiler rejects the call with
/// "cannot convert value of type 'LiterLlm.T' to expected argument type
/// 'RustBridge.T'".
///
/// The method signature must also carry `throws` because `intoRust()` is itself
/// a throwing function.
#[test]
fn method_with_first_class_dto_param_calls_into_rust_at_call_site() {
    use alef::core::ir::{MethodDef, ReceiverKind};

    // CreateImageRequest: first-class DTO with has_serde + has_default + string fields.
    let mut request_type = make_type(
        "CreateImageRequest",
        vec![
            make_field("prompt", TypeRef::String, false),
            make_field("model", TypeRef::Optional(Box::new(TypeRef::String)), true),
        ],
    );
    request_type.has_serde = true;
    request_type.has_default = true;

    // ImageClient: opaque type with `generateImage(_ request: CreateImageRequest)`.
    let client_type = TypeDef {
        name: "ImageClient".to_string(),
        rust_path: "demo::ImageClient".to_string(),
        original_rust_path: String::new(),
        fields: vec![],
        methods: vec![MethodDef {
            name: "generate_image".to_string(),
            params: vec![make_param("request", TypeRef::Named("CreateImageRequest".into()))],
            return_type: TypeRef::String,
            is_async: true,
            is_static: false,
            error_type: Some("DemoError".to_string()),
            doc: String::new(),
            sanitized: false,
            returns_ref: false,
            returns_cow: false,
            return_newtype_wrapper: None,
            receiver: Some(ReceiverKind::Ref),
            trait_source: None,
            has_default_impl: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
        }],
        is_opaque: true,
        is_clone: false,
        is_copy: false,
        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![],
        doc: String::new(),
        cfg: None,
        binding_excluded: false,
        binding_exclusion_reason: None,
    };

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

    let toml = r#"
[workspace]
languages = ["swift"]

[[crates]]
name = "demo"
sources = ["src/lib.rs"]

[crates.swift]
client_constructor_body.ImageClient = "Self { inner: ::demo::ImageClient::new(api_key, base_url) }"
"#;
    let cfg: alef::core::config::new_config::NewAlefConfig = toml::from_str(toml).expect("test config must parse");
    let config = cfg.resolve().expect("test config must resolve").remove(0);
    let files = SwiftBackend.generate_bindings(&api, &config).unwrap();
    let swift = files
        .iter()
        .find(|f| f.path.to_string_lossy().ends_with(".swift"))
        .unwrap();

    // The call site must apply `.intoRust()` to convert the Swift DTO wrapper
    // into the RustBridge raw type.
    assert!(
        swift.content.contains("try request.intoRust()"),
        "call site must apply try request.intoRust() for first-class DTO params; got:\n{}",
        swift.content
    );
    // The method signature must carry `throws` (both from error_type and intoRust).
    assert!(
        swift
            .content
            .contains("public func generateImage(_ request: CreateImageRequest) async throws -> String"),
        "method signature must include throws when param is a first-class DTO; got:\n{}",
        swift.content
    );
    // Must NOT pass the raw Swift wrapper directly to the bridge.
    assert!(
        !swift.content.contains(", request)"),
        "must not forward the Swift wrapper directly to the bridge without .intoRust(); got:\n{}",
        swift.content
    );
}

/// When only DTO params are present (no `error_type`), the method must still
/// emit `throws` on its signature because `intoRust()` is throwing.
#[test]
fn method_with_dto_param_only_adds_throws_even_without_error_type() {
    use alef::core::ir::{MethodDef, ReceiverKind};

    let mut req_type = make_type("SpeechRequest", vec![make_field("text", TypeRef::String, false)]);
    req_type.has_serde = true;
    req_type.has_default = true;

    let client_type = TypeDef {
        name: "SpeechClient".to_string(),
        rust_path: "demo::SpeechClient".to_string(),
        original_rust_path: String::new(),
        fields: vec![],
        methods: vec![MethodDef {
            name: "create_speech".to_string(),
            params: vec![make_param("req", TypeRef::Named("SpeechRequest".into()))],
            return_type: TypeRef::Bytes,
            is_async: false,
            is_static: false,
            error_type: None, // no error_type — throws must still come from intoRust()
            doc: String::new(),
            sanitized: false,
            returns_ref: false,
            returns_cow: false,
            return_newtype_wrapper: None,
            receiver: Some(ReceiverKind::Ref),
            trait_source: None,
            has_default_impl: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
        }],
        is_opaque: true,
        is_clone: false,
        is_copy: false,
        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![],
        doc: String::new(),
        cfg: None,
        binding_excluded: false,
        binding_exclusion_reason: None,
    };

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

    let toml = r#"
[workspace]
languages = ["swift"]

[[crates]]
name = "demo"
sources = ["src/lib.rs"]

[crates.swift]
client_constructor_body.SpeechClient = "Self { inner: ::demo::SpeechClient::new(api_key, base_url) }"
"#;
    let cfg: alef::core::config::new_config::NewAlefConfig = toml::from_str(toml).expect("test config must parse");
    let config = cfg.resolve().expect("test config must resolve").remove(0);
    let files = SwiftBackend.generate_bindings(&api, &config).unwrap();
    let swift = files
        .iter()
        .find(|f| f.path.to_string_lossy().ends_with(".swift"))
        .unwrap();

    assert!(
        swift
            .content
            .contains("public func createSpeech(_ req: SpeechRequest) throws"),
        "method with only DTO params must still emit throws; got:\n{}",
        swift.content
    );
    assert!(
        swift.content.contains("try req.intoRust()"),
        "call site must apply try req.intoRust(); got:\n{}",
        swift.content
    );
}

/// First-class struct fields must emit their rustdoc as `///` lines immediately
/// above the `public let` declaration, mirroring how type/enum docs are surfaced.
#[test]
fn first_class_struct_field_emits_doc_comment_above_let() {
    let mut field = make_field("user_id", TypeRef::String, false);
    field.doc = "Stable, opaque identifier.".to_string();
    let mut ty = make_type("User", vec![field]);
    ty.has_serde = true;
    ty.has_default = true;

    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![ty],
        functions: vec![],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
    };
    let config = make_config();
    let files = SwiftBackend.generate_bindings(&api, &config).unwrap();
    let swift = files
        .iter()
        .find(|f| f.path.to_string_lossy().ends_with(".swift"))
        .unwrap();

    assert!(
        swift
            .content
            .contains("    /// Stable, opaque identifier.\n    public let userId: String"),
        "field doc must sit immediately above the `public let`; got:\n{}",
        swift.content
    );
}

/// A DTO with a `Vec<Named>` field referencing another DTO in the same API must be
/// emitted as a first-class `public struct`, with the correct `[T]` stored property
/// and `try rb.field().map { try T($0) }` in `init(_ rb:)`.
#[test]
fn complex_dto_with_vec_named_field_emits_first_class_struct() {
    let mut message_type = make_type("Message", vec![make_field("content", TypeRef::String, false)]);
    message_type.has_serde = true;
    message_type.has_default = true;

    // Request: DTO with a Vec<Message> field.  has_default is NOT set — intoRust() uses JSON.
    let mut request_type = make_type(
        "ChatRequest",
        vec![
            make_field("model", TypeRef::String, false),
            make_field(
                "messages",
                TypeRef::Vec(Box::new(TypeRef::Named("Message".into()))),
                false,
            ),
            make_field("temperature", TypeRef::Primitive(PrimitiveType::F64), true),
        ],
    );
    request_type.has_serde = true;

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

    let config = make_config();
    let files = SwiftBackend.generate_bindings(&api, &config).unwrap();
    let swift = files
        .iter()
        .find(|f| f.path.to_string_lossy().ends_with(".swift"))
        .unwrap();
    let content = &swift.content;

    // Both types must be emitted as public structs.
    assert!(
        content.contains("public struct Message:"),
        "Message must be first-class; got:\n{content}"
    );
    assert!(
        content.contains("public struct ChatRequest:"),
        "ChatRequest must be first-class; got:\n{content}"
    );

    // Vec<Message> → [Message] stored property.
    assert!(
        content.contains("public let messages: [Message]"),
        "Vec<Message> field must emit [Message]; got:\n{content}"
    );

    // init(_ rb:) must convert Vec<Named> via .map.
    assert!(
        content.contains("try rb.messages().map { try Message($0) }"),
        "init must convert RustVec<Message> via .map; got:\n{content}"
    );

    // intoRust() must fall back to JSON (no direct constructor for complex type).
    assert!(
        content.contains("JSONEncoder().encode(self)"),
        "intoRust() must use JSON fallback; got:\n{content}"
    );
    assert!(
        content.contains("chatRequestFromJson"),
        "intoRust() must call from-json shim; got:\n{content}"
    );
}

/// A DTO with a `Named(S)` field (optional nested struct via field.optional=true) must be
/// emitted as first-class, with `try rb.field().map { try S($0) }` in `init(_ rb:)`.
#[test]
fn complex_dto_with_named_struct_field_emits_first_class_struct() {
    let mut usage_type = make_type(
        "Usage",
        vec![
            make_field("prompt_tokens", TypeRef::Primitive(PrimitiveType::I32), false),
            make_field("completion_tokens", TypeRef::Primitive(PrimitiveType::I32), false),
        ],
    );
    usage_type.has_serde = true;
    usage_type.has_default = true;

    let mut response_type = make_type(
        "ChatResponse",
        vec![
            make_field("model", TypeRef::String, false),
            // Optional nested struct via field.optional=true, TypeRef::Named
            make_field("usage", TypeRef::Named("Usage".into()), true),
        ],
    );
    response_type.has_serde = true;

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

    let config = make_config();
    let files = SwiftBackend.generate_bindings(&api, &config).unwrap();
    let swift = files
        .iter()
        .find(|f| f.path.to_string_lossy().ends_with(".swift"))
        .unwrap();
    let content = &swift.content;

    assert!(
        content.contains("public struct ChatResponse:"),
        "ChatResponse must be first-class; got:\n{content}"
    );

    // Optional Named field emits `Usage?`.
    assert!(
        content.contains("public let usage: Usage?"),
        "Optional Named field must be Usage?; got:\n{content}"
    );

    // init must convert optional Named field via .map.
    assert!(
        content.contains("try rb.usage().map { try Usage($0) }"),
        "init must convert optional Named field via .map; got:\n{content}"
    );
}

/// A DTO with an `Optional<Vec<Named>>` field (TypeRef::Optional wrapping) must be
/// emitted as first-class, with `?.map` conversion in `init(_ rb:)`.
#[test]
fn complex_dto_with_optional_vec_named_field_emits_first_class_struct() {
    let mut tool_type = make_type("Tool", vec![make_field("name", TypeRef::String, false)]);
    tool_type.has_serde = true;
    tool_type.has_default = true;

    let mut request_type = make_type(
        "ToolRequest",
        vec![
            make_field("model", TypeRef::String, false),
            make_field(
                "tools",
                TypeRef::Optional(Box::new(TypeRef::Vec(Box::new(TypeRef::Named("Tool".into()))))),
                false,
            ),
        ],
    );
    request_type.has_serde = true;

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

    let config = make_config();
    let files = SwiftBackend.generate_bindings(&api, &config).unwrap();
    let swift = files
        .iter()
        .find(|f| f.path.to_string_lossy().ends_with(".swift"))
        .unwrap();
    let content = &swift.content;

    assert!(
        content.contains("public struct ToolRequest:"),
        "ToolRequest must be first-class; got:\n{content}"
    );

    // Optional<Vec<Tool>> → [Tool]?
    assert!(
        content.contains("public let tools: [Tool]?"),
        "Optional<Vec<Tool>> must emit [Tool]?; got:\n{content}"
    );

    // init must handle Optional<Vec<Named>> via ?.map.
    assert!(
        content.contains("try rb.tools()?.map { try Tool($0) }"),
        "init must convert Optional<Vec<Tool>> via ?.map; got:\n{content}"
    );
}

// ── forwarder regression tests for v0.17.9+ swift codegen bug ─────────────────

/// Repro of the tslp `detectLanguageFromExtension` bug fixed in v0.17.11.
///
/// A non-throwing free function whose Rust signature is
/// `fn detect_language_from_extension(ext: &str) -> Option<&'static str>` is
/// JSON-bridged by swift-bridge: the bridge crate declares the function as
/// `-> String` (the JSON payload), which swift-bridge surfaces on the Swift
/// side as a NON-optional `-> RustString`. The forwarder declares the high-level
/// `-> String?` return matching the IR.
///
/// v0.17.10 attempted to apply `?.toString()` to the bridge call, which is a
/// type error: "cannot use optional chaining on non-optional value of type
/// 'RustString'". The correct fix is to JSON-decode the payload as
/// `String?.self` — JSON `null` decodes to `.none`, a present value to `.some`.
#[test]
fn forwarder_optional_string_return_emits_json_decode_body() {
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![FunctionDef {
            name: "detect_language_from_extension".into(),
            rust_path: "demo::detect_language_from_extension".into(),
            original_rust_path: String::new(),
            params: vec![make_param("ext", TypeRef::String)],
            return_type: TypeRef::Optional(Box::new(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![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
    };

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

    assert!(
        content.contains("public func detectLanguageFromExtension(ext: String) -> String?"),
        "forwarder signature must declare Optional String return:\n{content}"
    );
    assert!(
        content.contains("let _rb_json = RustBridge.detectLanguageFromExtension(ext).toString()"),
        "forwarder must read bridge return as non-optional RustString via .toString():\n{content}"
    );
    assert!(
        content.contains("JSONDecoder().decode(String?.self, from: _rb_data)"),
        "forwarder must JSON-decode the payload as String?.self:\n{content}"
    );
    assert!(
        !content.contains("?.toString()"),
        "v0.17.10 type-error pattern must not reappear (optional-chained .toString \
         on non-optional RustString):\n{content}"
    );
    assert!(
        !content.contains("return RustBridge.detectLanguageFromExtension(ext)\n"),
        "forwarder must NOT return the raw RustString bridge value:\n{content}"
    );
}

/// Repro of the tslp `process(source:config:)` bug: a free function taking a
/// host-facing Named DTO struct param must convert it via `try param.intoRust()`
/// before calling the swift-bridge extern, which expects the low-level
/// `RustBridge.{Dto}` class. Without this Swift rejects the call with "cannot
/// convert value of type '{Module}.{Dto}' to expected argument type
/// 'RustBridge.{Dto}'".
#[test]
fn forwarder_named_dto_param_calls_into_rust_before_bridge_call() {
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![
            {
                let mut ty = make_type(
                    "ProcessConfig",
                    vec![make_field("strict", TypeRef::Primitive(PrimitiveType::Bool), false)],
                );
                ty.has_serde = true;
                ty.is_return_type = true;
                ty
            },
            {
                let mut ty = make_type(
                    "ProcessResult",
                    vec![make_field("ok", TypeRef::Primitive(PrimitiveType::Bool), false)],
                );
                ty.has_serde = true;
                ty.is_return_type = true;
                ty
            },
        ],
        functions: vec![FunctionDef {
            name: "process".into(),
            rust_path: "demo::process".into(),
            original_rust_path: String::new(),
            params: vec![
                make_param("source", TypeRef::String),
                make_param("config", TypeRef::Named("ProcessConfig".into())),
            ],
            return_type: TypeRef::Named("ProcessResult".into()),
            is_async: false,
            error_type: Some("DemoError".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(),
    };

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

    assert!(
        content.contains("public func process(source: String, config: ProcessConfig) throws -> ProcessResult"),
        "forwarder signature must declare ProcessConfig (host DTO) — not RustBridge.ProcessConfig:\n{content}"
    );
    assert!(
        content.contains("let _rb_config = try config.intoRust()"),
        "forwarder must materialise the bridge value via intoRust() before the bridge call:\n{content}"
    );
    assert!(
        content.contains("RustBridge.process(source, _rb_config)"),
        "forwarder must pass the converted `_rb_config` local to the bridge:\n{content}"
    );
    assert!(
        !content.contains("RustBridge.process(source, config)"),
        "forwarder must NOT pass the host DTO directly — bridge expects RustBridge.ProcessConfig:\n{content}"
    );
}

/// Repro of `Optional<Named DTO>` param handling: the forwarder must apply
/// `try param?.intoRust()` to promote the host struct into the low-level bridge
/// type, preserving the optional. The function-level `throws` clause must widen
/// even if the bridge call itself does not throw, because `intoRust()` is
/// throwing.
#[test]
fn forwarder_optional_named_dto_param_uses_optional_chained_into_rust() {
    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![{
            let mut ty = make_type(
                "Options",
                vec![make_field("verbose", TypeRef::Primitive(PrimitiveType::Bool), false)],
            );
            ty.has_serde = true;
            ty.is_return_type = true;
            ty
        }],
        functions: vec![FunctionDef {
            name: "run".into(),
            rust_path: "demo::run".into(),
            original_rust_path: String::new(),
            params: vec![make_optional_param("opts", TypeRef::Named("Options".into()))],
            return_type: TypeRef::Unit,
            is_async: false,
            // Non-throwing bridge call to verify the throws clause widens
            // purely from the param-conversion path.
            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(),
    };

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

    assert!(
        content.contains("public func run(opts: Options?) throws"),
        "throws clause must widen to cover the throwing intoRust() param conversion:\n{content}"
    );
    assert!(
        content.contains("let _rb_opts = try opts?.intoRust()"),
        "forwarder must use optional-chained intoRust() for Optional<Named DTO> params:\n{content}"
    );
    assert!(
        content.contains("RustBridge.run(_rb_opts)"),
        "forwarder must forward the converted `_rb_opts` to the bridge call:\n{content}"
    );
}

#[test]
fn async_function_with_result_and_opaque_param_emits_forwarder() {
    // Simulate kreuzcrawl's scrape(engine: CrawlEngineHandle, url: String) -> Result<ScrapeResult>
    let api = ApiSurface {
        crate_name: "kreuzcrawl".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![FunctionDef {
            name: "scrape".into(),
            rust_path: "kreuzcrawl::scrape".into(),
            original_rust_path: String::new(),
            params: vec![
                make_param("engine", TypeRef::Named("CrawlEngineHandle".into())),
                make_param("url", TypeRef::String),
            ],
            return_type: TypeRef::Named("ScrapeResult".into()),
            is_async: true,
            error_type: Some("CrawlError".into()),
            doc: "Scrape a URL".into(),
            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(),
    };

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

    assert!(
        content.contains("public func scrape"),
        "async function with opaque param and Result return must emit a forwarder:\n{content}"
    );
    assert!(
        content.contains("async throws"),
        "async function with error_type must have both async and throws qualifiers:\n{content}"
    );
}

/// Build a serde-enabled, non-opaque DTO type for e2e-helper detection.
fn make_serde_type(name: &str) -> TypeDef {
    let mut ty = make_type(
        name,
        vec![make_field("value", TypeRef::Primitive(PrimitiveType::I32), false)],
    );
    ty.has_serde = true;
    ty.has_default = true;
    ty
}

#[test]
fn legacy_extraction_type_names_do_not_emit_e2e_wrappers() {
    // These names used to trigger hardcoded Swift e2e convenience wrappers. They
    // should now flow through the generic from-json forwarder path only.
    let mut embed_fn = make_sync_fn(
        "embed_texts_async",
        vec![
            make_param("texts", TypeRef::Vec(Box::new(TypeRef::String))),
            make_param("config", TypeRef::Named("EmbeddingConfig".into())),
        ],
        TypeRef::Vec(Box::new(TypeRef::Vec(Box::new(TypeRef::Primitive(PrimitiveType::F32))))),
    );
    embed_fn.is_async = true;

    // A bytes-first function so the convenience-wrapper section is reached.
    let bytes_fn = make_sync_fn(
        "analyze_bytes_sync",
        vec![make_param("content", TypeRef::Bytes)],
        TypeRef::String,
    );

    let api = ApiSurface {
        crate_name: "demo".into(),
        version: "0.1.0".into(),
        types: vec![
            make_serde_type("ExtractionConfig"),
            make_serde_type("BatchBytesItem"),
            make_serde_type("BatchFileItem"),
            make_serde_type("EmbeddingConfig"),
        ],
        functions: vec![embed_fn, bytes_fn],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
    };

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

    assert_eq!(
        content.matches("func embedTextsAsync(").count(),
        1,
        "embedTextsAsync must be declared exactly once by the generic free-function forwarder:\n{content}"
    );
    assert!(
        !content.contains("E2e Test Convenience Wrappers"),
        "legacy extraction type names must not trigger hardcoded e2e wrappers:\n{content}"
    );
    assert!(
        content.contains("public func extractionConfigFromJson(_ json: String) throws -> ExtractionConfig"),
        "ExtractionConfig should use the generic from-json forwarder:\n{content}"
    );
}
