use crate::config::E2eConfig;
use crate::escape::sanitize_filename;
use crate::field_access::FieldResolver;
use crate::fixture::{Fixture, FixtureGroup};
use alef_core::backend::GeneratedFile;
use alef_core::config::ResolvedCrateConfig;
use alef_core::template_versions::{maven, toolchain};
use anyhow::Result;
use heck::ToUpperCamelCase;
use std::collections::HashSet;
use std::path::PathBuf;
use super::E2eCodegen;
use super::kotlin;
pub struct KotlinAndroidE2eCodegen;
impl E2eCodegen for KotlinAndroidE2eCodegen {
fn generate(
&self,
groups: &[FixtureGroup],
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
type_defs: &[alef_core::ir::TypeDef],
) -> Result<Vec<GeneratedFile>> {
let lang = self.language_name();
let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
let mut files = Vec::new();
let call = &e2e_config.call;
let overrides = call.overrides.get(lang);
let _module_path = overrides
.and_then(|o| o.module.as_ref())
.cloned()
.unwrap_or_else(|| call.module.clone());
let function_name = overrides
.and_then(|o| o.function.as_ref())
.cloned()
.unwrap_or_else(|| call.function.clone());
let class_name = overrides
.and_then(|o| o.class.as_ref())
.cloned()
.unwrap_or_else(|| config.name.to_upper_camel_case());
let result_is_simple = overrides.is_some_and(|o| o.result_is_simple);
let result_var = &call.result_var;
let kotlin_android_pkg = e2e_config.resolve_package("kotlin_android");
let pkg_name = kotlin_android_pkg
.as_ref()
.and_then(|p| p.name.as_ref())
.cloned()
.unwrap_or_else(|| config.name.clone());
let _kotlin_android_pkg_path = kotlin_android_pkg
.as_ref()
.and_then(|p| p.path.as_deref())
.unwrap_or("../../packages/kotlin-android");
let kotlin_android_version = kotlin_android_pkg
.as_ref()
.and_then(|p| p.version.as_ref())
.cloned()
.or_else(|| config.resolved_version())
.unwrap_or_else(|| "0.1.0".to_string());
let kotlin_pkg_id = config.kotlin_package();
let needs_mock_server = groups
.iter()
.flat_map(|g| g.fixtures.iter())
.any(|f| f.needs_mock_server());
files.push(GeneratedFile {
path: output_base.join("build.gradle.kts"),
content: render_build_gradle_kotlin_android(
&pkg_name,
&kotlin_pkg_id,
&kotlin_android_version,
e2e_config.dep_mode,
needs_mock_server,
),
generated_header: false,
});
let mut test_base = output_base.join("src").join("test").join("kotlin");
for segment in kotlin_pkg_id.split('.') {
test_base = test_base.join(segment);
}
let test_base = test_base.join("e2e");
if needs_mock_server {
files.push(GeneratedFile {
path: test_base.join("MockServerListener.kt"),
content: kotlin::render_mock_server_listener_kt(&kotlin_pkg_id),
generated_header: true,
});
files.push(GeneratedFile {
path: output_base
.join("src")
.join("test")
.join("resources")
.join("META-INF")
.join("services")
.join("org.junit.platform.launcher.LauncherSessionListener"),
content: format!("{kotlin_pkg_id}.e2e.MockServerListener\n"),
generated_header: false,
});
}
let options_type = overrides.and_then(|o| o.options_type.clone());
let field_resolver = FieldResolver::new(
&e2e_config.fields,
&e2e_config.fields_optional,
&e2e_config.result_fields,
&e2e_config.fields_array,
&HashSet::new(),
);
let struct_names: HashSet<&str> = type_defs.iter().map(|td| td.name.as_str()).collect();
let type_enum_fields: std::collections::HashMap<String, HashSet<String>> = type_defs
.iter()
.filter_map(|td| {
let enum_field_names: HashSet<String> = td
.fields
.iter()
.filter(|field| is_enum_typed(&field.ty, &struct_names))
.map(|field| field.name.clone())
.collect();
if enum_field_names.is_empty() {
None
} else {
Some((td.name.clone(), enum_field_names))
}
})
.collect();
for group in groups {
let active: Vec<&Fixture> = group
.fixtures
.iter()
.filter(|f| super::should_include_fixture(f, lang, e2e_config))
.collect();
if active.is_empty() {
continue;
}
let class_file_name = format!("{}Test.kt", sanitize_filename(&group.category).to_upper_camel_case());
let content = kotlin::render_test_file(
&group.category,
&active,
&class_name,
&function_name,
&kotlin_pkg_id,
result_var,
&e2e_config.call.args,
options_type.as_deref(),
&field_resolver,
result_is_simple,
&e2e_config.fields_enum,
e2e_config,
&type_enum_fields,
);
files.push(GeneratedFile {
path: test_base.join(class_file_name),
content,
generated_header: true,
});
}
Ok(files)
}
fn language_name(&self) -> &'static str {
"kotlin_android"
}
}
fn is_enum_typed(ty: &alef_core::ir::TypeRef, struct_names: &HashSet<&str>) -> bool {
use alef_core::ir::TypeRef;
match ty {
TypeRef::Named(name) => !struct_names.contains(name.as_str()),
TypeRef::Optional(inner) => {
matches!(inner.as_ref(), TypeRef::Named(name) if !struct_names.contains(name.as_str()))
}
_ => false,
}
}
fn render_build_gradle_kotlin_android(
_pkg_name: &str,
kotlin_pkg_id: &str,
_pkg_version: &str,
_dep_mode: crate::config::DependencyMode,
needs_mock_server: bool,
) -> String {
let kotlin_plugin = maven::KOTLIN_JVM_PLUGIN;
let junit = maven::JUNIT;
let jackson = maven::JACKSON_E2E;
let jvm_target = toolchain::JVM_TARGET;
let jna = maven::JNA;
let jspecify = maven::JSPECIFY;
let coroutines = maven::KOTLINX_COROUTINES_CORE;
let launcher_dep = if needs_mock_server {
format!(r#" testImplementation("org.junit.platform:junit-platform-launcher:{junit}")"#)
} else {
String::new()
};
format!(
r#"import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {{
kotlin("jvm") version "{kotlin_plugin}"
java
}}
group = "{kotlin_pkg_id}"
version = "0.1.0"
java {{
sourceCompatibility = JavaVersion.VERSION_{jvm_target}
targetCompatibility = JavaVersion.VERSION_{jvm_target}
}}
kotlin {{
compilerOptions {{
jvmTarget.set(JvmTarget.JVM_{jvm_target})
}}
}}
repositories {{
mavenCentral()
}}
sourceSets {{
test {{
// Include the AAR-bundled Java facade as test sources
java.srcDir("../../packages/kotlin-android/src/main/java")
// Include the AAR-bundled Kotlin wrapper as test sources
kotlin.srcDir("../../packages/kotlin-android/src/main/kotlin")
}}
}}
dependencies {{
// JNA for loading libkreuzberg_ffi from java.library.path
testImplementation("net.java.dev.jna:jna:{jna}")
// Jackson for JSON assertion helpers
testImplementation("com.fasterxml.jackson.core:jackson-annotations:{jackson}")
testImplementation("com.fasterxml.jackson.core:jackson-databind:{jackson}")
testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:{jackson}")
// jspecify for null-safety annotations on wrapped types
testImplementation("org.jspecify:jspecify:{jspecify}")
// Kotlin coroutines for async test helpers
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:{coroutines}")
// JUnit 5 API and engine
testImplementation("org.junit.jupiter:junit-jupiter-api:{junit}")
testImplementation("org.junit.jupiter:junit-jupiter-engine:{junit}")
{launcher_dep}
// Kotlin stdlib test helpers
testImplementation(kotlin("test"))
}}
tasks.test {{
useJUnitPlatform()
// Resolve libkreuzberg_ffi location (e.g., ../../target/release)
val libPath = System.getProperty("kb.lib.path") ?: "${{rootDir}}/../../target/release"
systemProperty("java.library.path", libPath)
systemProperty("jna.library.path", libPath)
// Resolve fixture paths (e.g. "docx/fake.docx") against test_documents/
workingDir = file("${{rootDir}}/../../test_documents")
}}
"#
)
}