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

// ---------------------------------------------------------------------------
// Test helpers
// ---------------------------------------------------------------------------

fn resolved_one(toml: &str) -> ResolvedCrateConfig {
    let cfg: NewAlefConfig = toml::from_str(toml).unwrap();
    cfg.resolve().unwrap().remove(0)
}

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_function(name: &str, params: Vec<ParamDef>, return_type: TypeRef, is_async: bool) -> FunctionDef {
    FunctionDef {
        name: name.to_string(),
        rust_path: format!("demo::{name}"),
        original_rust_path: String::new(),
        params,
        return_type,
        is_async,
        doc: String::new(),
        error_type: None,
        cfg: None,
        sanitized: false,
        return_sanitized: false,
        returns_ref: false,
        returns_cow: false,
        return_newtype_wrapper: None,
        binding_excluded: false,
        binding_exclusion_reason: None,
    }
}

/// Build a multiplatform-target config.
fn make_mpp_config(crate_name: &str) -> ResolvedCrateConfig {
    resolved_one(&format!(
        r#"
[workspace]
languages = ["kotlin", "ffi"]

[[crates]]
name = "{crate_name}"
sources = ["src/lib.rs"]

[crates.ffi]
prefix = "demo"
header_name = "demo.h"
lib_name = "demo_ffi"

[crates.kotlin]
package = "dev.sample_crate.demo"
target = "multiplatform"
"#
    ))
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

/// KMP target emits five files: commonMain .kt, jvmMain .kt, nativeMain .kt, .def, build.gradle.kts.
#[test]
fn mpp_emits_five_files() {
    let api = ApiSurface {
        crate_name: "my-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(),
        services: vec![],
        handler_contracts: vec![],
    };
    let config = make_mpp_config("my-crate");
    let files = KotlinBackend.generate_bindings(&api, &config).unwrap();
    assert_eq!(
        files.len(),
        5,
        "expected 5 files (commonMain, jvmMain, nativeMain, def, gradle): {files:?}"
    );
}

/// commonMain contains `expect object` with function signatures only (no bodies).
#[test]
fn mpp_common_contains_expect_object() {
    let api = ApiSurface {
        crate_name: "my-crate".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![make_function(
            "convert_html",
            vec![make_param("input", TypeRef::String)],
            TypeRef::String,
            false,
        )],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };
    let config = make_mpp_config("my-crate");
    let files = KotlinBackend.generate_bindings(&api, &config).unwrap();

    let common = files
        .iter()
        .find(|f| f.path.to_string_lossy().contains("commonMain"))
        .expect("commonMain file missing");

    assert!(
        common.content.contains("expect object MyCrate"),
        "missing expect object: {}",
        common.content
    );
    // Expect signature — no body (no opening brace after the signature line).
    assert!(
        common.content.contains("fun convertHtml(input: String): String"),
        "missing expect signature: {}",
        common.content
    );
    // No Bridge call in commonMain.
    assert!(
        !common.content.contains("Bridge."),
        "commonMain must not contain Bridge calls: {}",
        common.content
    );
    // No memScoped in commonMain.
    assert!(
        !common.content.contains("memScoped"),
        "commonMain must not contain memScoped: {}",
        common.content
    );
}

/// jvmMain contains `actual object` with Bridge delegate calls.
#[test]
fn mpp_jvm_contains_actual_object_with_bridge() {
    let api = ApiSurface {
        crate_name: "my-crate".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![make_function(
            "greet",
            vec![make_param("name", TypeRef::String)],
            TypeRef::Primitive(PrimitiveType::I32),
            false,
        )],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };
    let config = make_mpp_config("my-crate");
    let files = KotlinBackend.generate_bindings(&api, &config).unwrap();

    let jvm = files
        .iter()
        .find(|f| f.path.to_string_lossy().contains("jvmMain"))
        .expect("jvmMain file missing");

    assert!(
        jvm.content.contains("actual object MyCrate"),
        "missing actual object: {}",
        jvm.content
    );
    assert!(
        jvm.content.contains("Bridge.greet("),
        "missing Bridge call in jvmMain: {}",
        jvm.content
    );
}

/// nativeMain contains `actual object` with memScoped bodies.
#[test]
fn mpp_native_contains_actual_object_with_mem_scoped() {
    let api = ApiSurface {
        crate_name: "my-crate".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![make_function(
            "convert_html",
            vec![make_param("input", TypeRef::String)],
            TypeRef::String,
            false,
        )],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };
    let config = make_mpp_config("my-crate");
    let files = KotlinBackend.generate_bindings(&api, &config).unwrap();

    let native = files
        .iter()
        .find(|f| f.path.to_string_lossy().contains("nativeMain"))
        .expect("nativeMain file missing");

    assert!(
        native.content.contains("actual object MyCrate"),
        "missing actual object in nativeMain: {}",
        native.content
    );
    assert!(
        native.content.contains("memScoped {"),
        "missing memScoped in nativeMain: {}",
        native.content
    );
    assert!(
        native.content.contains("demo_convert_html("),
        "missing C call in nativeMain: {}",
        native.content
    );
}

/// DTOs (data classes) appear in commonMain only — not duplicated in jvmMain or nativeMain.
#[test]
fn mpp_dtos_only_in_common_main() {
    let api = ApiSurface {
        crate_name: "my-crate".into(),
        version: "0.1.0".into(),
        types: vec![make_type(
            "ConversionOptions",
            vec![make_field("max_width", TypeRef::Primitive(PrimitiveType::U32), false)],
        )],
        functions: vec![],
        enums: vec![],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };
    let config = make_mpp_config("my-crate");
    let files = KotlinBackend.generate_bindings(&api, &config).unwrap();

    let common = files
        .iter()
        .find(|f| f.path.to_string_lossy().contains("commonMain"))
        .expect("commonMain file missing");
    let jvm = files
        .iter()
        .find(|f| f.path.to_string_lossy().contains("jvmMain"))
        .expect("jvmMain file missing");
    let native = files
        .iter()
        .find(|f| f.path.to_string_lossy().contains("nativeMain"))
        .expect("nativeMain file missing");

    assert!(
        common.content.contains("data class ConversionOptions("),
        "DTO missing from commonMain: {}",
        common.content
    );
    assert!(
        !jvm.content.contains("data class ConversionOptions"),
        "DTO must not be duplicated in jvmMain: {}",
        jvm.content
    );
    assert!(
        !native.content.contains("data class ConversionOptions"),
        "DTO must not be duplicated in nativeMain: {}",
        native.content
    );
}

/// .def file references the correct header, prefix, and library name.
#[test]
fn mpp_def_file_has_correct_fields() {
    let api = ApiSurface {
        crate_name: "my-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(),
        services: vec![],
        handler_contracts: vec![],
    };
    let config = make_mpp_config("my-crate");
    let files = KotlinBackend.generate_bindings(&api, &config).unwrap();

    let def = files
        .iter()
        .find(|f| f.path.extension().is_some_and(|e| e == "def"))
        .expect(".def file missing");

    assert!(
        def.content.contains("headers = demo.h"),
        "missing header: {}",
        def.content
    );
    assert!(
        def.content.contains("headerFilter = demo_*"),
        "missing headerFilter: {}",
        def.content
    );
    assert!(
        def.content.contains("-ldemo_ffi"),
        "missing linker flag: {}",
        def.content
    );
}

/// build.gradle.kts contains the multiplatform plugin and jvm() + linuxX64 + macosArm64 targets.
#[test]
fn mpp_gradle_uses_multiplatform_plugin() {
    let api = ApiSurface {
        crate_name: "my-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(),
        services: vec![],
        handler_contracts: vec![],
    };
    let config = make_mpp_config("my-crate");
    let files = KotlinBackend.generate_bindings(&api, &config).unwrap();

    let gradle = files
        .iter()
        .find(|f| f.path.file_name().is_some_and(|n| n == "build.gradle.kts"))
        .expect("build.gradle.kts missing");

    assert!(
        gradle.content.contains(r#"kotlin("multiplatform")"#),
        "missing multiplatform plugin: {}",
        gradle.content
    );
    assert!(
        gradle.content.contains("jvm()"),
        "missing jvm target: {}",
        gradle.content
    );
    assert!(
        gradle.content.contains("linuxX64"),
        "missing linuxX64 target: {}",
        gradle.content
    );
    assert!(
        gradle.content.contains("macosArm64"),
        "missing macosArm64 target: {}",
        gradle.content
    );
}

/// Sealed enum with payload fields works in commonMain (pure Kotlin, no platform deps).
#[test]
fn mpp_sealed_enum_in_common_main() {
    let api = ApiSurface {
        crate_name: "my-crate".into(),
        version: "0.1.0".into(),
        types: vec![],
        functions: vec![],
        enums: vec![EnumDef {
            name: "OutputFormat".to_string(),
            rust_path: "demo::OutputFormat".to_string(),
            original_rust_path: String::new(),
            doc: String::new(),
            variants: vec![
                EnumVariant {
                    name: "Markdown".to_string(),
                    fields: vec![],
                    doc: String::new(),
                    is_default: false,
                    serde_rename: None,
                    is_tuple: false,
                },
                EnumVariant {
                    name: "PlainText".to_string(),
                    fields: vec![],
                    doc: String::new(),
                    is_default: false,
                    serde_rename: None,
                    is_tuple: false,
                },
            ],
            cfg: None,
            serde_tag: None,
            serde_untagged: false,
            serde_rename_all: None,

            is_copy: false,
            has_serde: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
        }],
        errors: vec![],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };
    let config = make_mpp_config("my-crate");
    let files = KotlinBackend.generate_bindings(&api, &config).unwrap();

    let common = files
        .iter()
        .find(|f| f.path.to_string_lossy().contains("commonMain"))
        .expect("commonMain file missing");

    assert!(
        common.content.contains("enum class OutputFormat"),
        "missing enum in commonMain: {}",
        common.content
    );
}

/// Error sealed class appears in commonMain only.
#[test]
fn mpp_error_sealed_class_in_common_main() {
    let api = ApiSurface {
        crate_name: "my-crate".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(),
            doc: 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(),
            }],
            methods: vec![],
            binding_excluded: false,
            binding_exclusion_reason: None,
        }],
        excluded_type_paths: ::std::collections::HashMap::new(),
        excluded_trait_names: ::std::collections::HashSet::new(),
        services: vec![],
        handler_contracts: vec![],
    };
    let config = make_mpp_config("my-crate");
    let files = KotlinBackend.generate_bindings(&api, &config).unwrap();

    let common = files
        .iter()
        .find(|f| f.path.to_string_lossy().contains("commonMain"))
        .expect("commonMain file missing");
    let jvm = files
        .iter()
        .find(|f| f.path.to_string_lossy().contains("jvmMain"))
        .expect("jvmMain file missing");

    assert!(
        common.content.contains("sealed class ApiError"),
        "error class missing from commonMain: {}",
        common.content
    );
    assert!(
        !jvm.content.contains("sealed class ApiError"),
        "error class must not be duplicated in jvmMain: {}",
        jvm.content
    );
}
