use crate::ir::abi::AbiContract;
use crate::ir::contract::FfiContract;
use crate::ir::ops::{ReadOp, ReadSeq, SizeExpr, ValueExpr, WriteOp, WriteSeq};
use crate::ir::types::{PrimitiveType, TypeExpr};
use boltffi_ffi_rules::transport::EnumTagStrategy;

use crate::ir::codec::{EnumLayout, VecLayout};

use super::JavaOptions;
use super::lower::JavaLowerer;
use super::mappings;
use super::names::NamingConvention;
use super::plan::JavaEnumKind;
use super::templates::{
    CStyleEnumTemplate, CallbackCallbacksTemplate, CallbackTraitTemplate, ClassTemplate,
    ClosureCallbacksTemplate, ClosureTemplate, DataEnumAbstractTemplate, DataEnumSealedTemplate,
    ErrorEnumTemplate, FunctionsTemplate, NativeTemplate, PreambleTemplate, RecordTemplate,
};
use askama::Template;

pub struct JavaFile {
    pub file_name: String,
    pub source: String,
}

pub struct JavaOutput {
    pub files: Vec<JavaFile>,
    pub class_name: String,
    pub package_path: String,
}

pub struct JavaEmitter;

impl JavaEmitter {
    pub fn emit(
        ffi: &FfiContract,
        abi: &AbiContract,
        package_name: String,
        module_name: String,
        options: JavaOptions,
    ) -> JavaOutput {
        let lowerer = JavaLowerer::new(ffi, abi, package_name, module_name, options);
        let module = lowerer.module();
        let package_path = module.package_path();
        let class_name = module.class_name.clone();

        let mut files = Vec::new();

        for enumeration in &module.enums {
            let source = match enumeration.kind {
                JavaEnumKind::CStyle => {
                    let template = CStyleEnumTemplate {
                        enumeration,
                        package_name: &module.package_name,
                    };
                    template.render().expect("c-style enum template failed")
                }
                JavaEnumKind::Error => {
                    let template = ErrorEnumTemplate {
                        enumeration,
                        package_name: &module.package_name,
                    };
                    template.render().expect("error enum template failed")
                }
                JavaEnumKind::SealedInterface => {
                    let template = DataEnumSealedTemplate {
                        enumeration,
                        package_name: &module.package_name,
                    };
                    template.render().expect("sealed enum template failed")
                }
                JavaEnumKind::AbstractClass => {
                    let template = DataEnumAbstractTemplate {
                        enumeration,
                        package_name: &module.package_name,
                    };
                    template.render().expect("abstract enum template failed")
                }
                JavaEnumKind::ErrorAbstractClass => {
                    let template = DataEnumAbstractTemplate {
                        enumeration,
                        package_name: &module.package_name,
                    };
                    template
                        .render()
                        .expect("error abstract enum template failed")
                }
            };
            files.push(JavaFile {
                file_name: format!("{}.java", enumeration.class_name),
                source,
            });
        }

        for record in &module.records {
            let record_template = RecordTemplate {
                record,
                package_name: &module.package_name,
            };
            files.push(JavaFile {
                file_name: format!("{}.java", record.class_name),
                source: record_template.render().expect("record template failed"),
            });
        }

        for class in &module.classes {
            let class_template = ClassTemplate {
                class,
                package_name: &module.package_name,
                async_mode: &module.async_mode,
            };
            files.push(JavaFile {
                file_name: format!("{}.java", class.class_name),
                source: class_template.render().expect("class template failed"),
            });
        }

        let closure_files = module.closures.iter().flat_map(|closure| {
            let pkg = &module.package_name;
            [
                JavaFile {
                    file_name: format!("{}.java", closure.interface_name),
                    source: ClosureTemplate {
                        closure,
                        package_name: pkg,
                    }
                    .render()
                    .expect("closure interface template failed"),
                },
                JavaFile {
                    file_name: format!("{}.java", closure.callbacks_class_name),
                    source: ClosureCallbacksTemplate {
                        closure,
                        package_name: pkg,
                    }
                    .render()
                    .expect("closure callbacks template failed"),
                },
            ]
        });

        let callback_files = module.callbacks.iter().flat_map(|callback| {
            let pkg = &module.package_name;
            [
                JavaFile {
                    file_name: format!("{}.java", callback.interface_name),
                    source: CallbackTraitTemplate {
                        callback,
                        package_name: pkg,
                    }
                    .render()
                    .expect("callback trait template failed"),
                },
                JavaFile {
                    file_name: format!("{}.java", callback.callbacks_class_name()),
                    source: CallbackCallbacksTemplate {
                        callback,
                        package_name: pkg,
                    }
                    .render()
                    .expect("callback callbacks template failed"),
                },
            ]
        });

        files.extend(closure_files.chain(callback_files));

        let mut main_source = String::new();

        let preamble = PreambleTemplate { module: &module };
        main_source.push_str(&preamble.render().expect("preamble template failed"));

        let native = NativeTemplate { module: &module };
        main_source.push_str(&native.render().expect("native template failed"));

        main_source.push('\n');

        let functions = FunctionsTemplate { module: &module };
        main_source.push_str(&functions.render().expect("functions template failed"));

        files.push(JavaFile {
            file_name: format!("{}.java", class_name),
            source: main_source,
        });

        JavaOutput {
            files,
            class_name,
            package_path,
        }
    }
}

fn render_value(expr: &ValueExpr) -> String {
    match expr {
        ValueExpr::Instance => String::new(),
        ValueExpr::Var(name) => name.clone(),
        ValueExpr::Named(name) => NamingConvention::field_name(name),
        ValueExpr::Field(parent, field) => {
            let parent_str = render_value(parent);
            let field_str = NamingConvention::field_name(field.as_str());
            if parent_str.is_empty() {
                field_str
            } else {
                format!("{}.{}", parent_str, field_str)
            }
        }
    }
}

#[derive(Default)]
struct JavaEmitContext {
    read_lambda_index: usize,
    write_loop_index: usize,
    size_lambda_index: usize,
}

impl JavaEmitContext {
    fn next_read_lambda(&mut self) -> String {
        let next_index = self.read_lambda_index;
        self.read_lambda_index += 1;
        format!("readIndex{}", next_index)
    }

    fn next_write_loop_var(&mut self) -> String {
        let next_index = self.write_loop_index;
        self.write_loop_index += 1;
        format!("item{}", next_index)
    }

    fn next_size_lambda(&mut self) -> String {
        let next_index = self.size_lambda_index;
        self.size_lambda_index += 1;
        format!("sizeItem{}", next_index)
    }
}

pub fn primitive_read_method(primitive: PrimitiveType) -> &'static str {
    match primitive {
        PrimitiveType::Bool => "readBool",
        PrimitiveType::I8 | PrimitiveType::U8 => "readI8",
        PrimitiveType::I16 | PrimitiveType::U16 => "readI16",
        PrimitiveType::I32 | PrimitiveType::U32 => "readI32",
        PrimitiveType::I64 | PrimitiveType::U64 | PrimitiveType::ISize | PrimitiveType::USize => {
            "readI64"
        }
        PrimitiveType::F32 => "readF32",
        PrimitiveType::F64 => "readF64",
    }
}

fn primitive_write_method(primitive: PrimitiveType) -> &'static str {
    match primitive {
        PrimitiveType::Bool => "writeBool",
        PrimitiveType::I8 | PrimitiveType::U8 => "writeI8",
        PrimitiveType::I16 | PrimitiveType::U16 => "writeI16",
        PrimitiveType::I32 | PrimitiveType::U32 => "writeI32",
        PrimitiveType::I64 | PrimitiveType::U64 | PrimitiveType::ISize | PrimitiveType::USize => {
            "writeI64"
        }
        PrimitiveType::F32 => "writeF32",
        PrimitiveType::F64 => "writeF64",
    }
}

pub fn emit_reader_read(seq: &ReadSeq) -> String {
    let mut context = JavaEmitContext::default();
    emit_reader_read_with_context(seq, &mut context)
}

fn emit_reader_read_with_context(seq: &ReadSeq, context: &mut JavaEmitContext) -> String {
    let op = seq.ops.first().expect("read ops");
    match op {
        ReadOp::Primitive { primitive, .. } => {
            format!("reader.{}()", primitive_read_method(*primitive))
        }
        ReadOp::String { .. } => "reader.readString()".to_string(),
        ReadOp::Bytes { .. } => "reader.readBytes()".to_string(),
        ReadOp::Record { id, .. } => {
            format!(
                "{}.decode(reader)",
                NamingConvention::class_name(id.as_str())
            )
        }
        ReadOp::Enum { id, layout, .. } => match layout {
            EnumLayout::CStyle {
                tag_type,
                tag_strategy,
                ..
            } => match tag_strategy {
                EnumTagStrategy::Discriminant => {
                    format!(
                        "{}.fromValue(reader.{}())",
                        NamingConvention::class_name(id.as_str()),
                        primitive_read_method(*tag_type),
                    )
                }
                EnumTagStrategy::OrdinalIndex => {
                    format!(
                        "{}.fromTag(reader.readI32())",
                        NamingConvention::class_name(id.as_str()),
                    )
                }
            },
            EnumLayout::Data { .. } | EnumLayout::Recursive => {
                format!(
                    "{}.decode(reader)",
                    NamingConvention::class_name(id.as_str())
                )
            }
        },
        ReadOp::Option { some, .. } => {
            let inner = emit_reader_read_with_context(some, context);
            format!(
                "reader.readI8() == 0 ? java.util.Optional.empty() : java.util.Optional.ofNullable({})",
                inner,
            )
        }
        ReadOp::Vec {
            element_type,
            element,
            layout,
            ..
        } => emit_reader_vec_with_context(element_type, element, layout, context),
        ReadOp::Result { .. } => {
            panic!(
                "ReadOp::Result should be handled via ResultDecode strategy, not emit_reader_read"
            )
        }
        other => panic!("unsupported Java read op: {:?}", other),
    }
}

fn emit_reader_vec_with_context(
    element_type: &TypeExpr,
    element: &ReadSeq,
    layout: &VecLayout,
    context: &mut JavaEmitContext,
) -> String {
    match layout {
        VecLayout::Blittable { .. } => match element_type {
            TypeExpr::Primitive(primitive) => {
                format!("reader.{}()", primitive_array_read_method(*primitive))
            }
            TypeExpr::Record(id) => {
                format!(
                    "{}.decodeBlittableVec(reader)",
                    NamingConvention::class_name(id.as_str())
                )
            }
            _ => {
                let inner = emit_reader_read_with_context(element, context);
                let lambda_var = context.next_read_lambda();
                format!("reader.readList({} -> {})", lambda_var, inner)
            }
        },
        VecLayout::Encoded => match element_type {
            TypeExpr::Primitive(primitive) => {
                format!("reader.{}()", primitive_array_read_method(*primitive))
            }
            _ => {
                let inner = emit_reader_read_with_context(element, context);
                let lambda_var = context.next_read_lambda();
                format!("reader.readList({} -> {})", lambda_var, inner)
            }
        },
    }
}

pub fn primitive_array_read_method(primitive: PrimitiveType) -> &'static str {
    match primitive {
        PrimitiveType::Bool => "readBooleanArray",
        PrimitiveType::I8 | PrimitiveType::U8 => "readByteArray",
        PrimitiveType::I16 | PrimitiveType::U16 => "readShortArray",
        PrimitiveType::I32 | PrimitiveType::U32 => "readIntArray",
        PrimitiveType::I64 | PrimitiveType::U64 | PrimitiveType::ISize | PrimitiveType::USize => {
            "readLongArray"
        }
        PrimitiveType::F32 => "readFloatArray",
        PrimitiveType::F64 => "readDoubleArray",
    }
}

pub fn emit_write_expr(seq: &WriteSeq, writer_name: &str) -> String {
    let mut context = JavaEmitContext::default();
    emit_write_expr_with_context(seq, writer_name, &mut context)
}

fn emit_write_expr_with_context(
    seq: &WriteSeq,
    writer_name: &str,
    context: &mut JavaEmitContext,
) -> String {
    let op = seq.ops.first().expect("write ops");
    match op {
        WriteOp::Primitive { primitive, value } => {
            format!(
                "{}.{}({})",
                writer_name,
                primitive_write_method(*primitive),
                render_value(value)
            )
        }
        WriteOp::String { value } => {
            format!("{}.writeString({})", writer_name, render_value(value))
        }
        WriteOp::Bytes { value } => {
            format!("{}.writeBytes({})", writer_name, render_value(value))
        }
        WriteOp::Option { value, some } => {
            let option_expr = render_value(value);
            let inner = emit_write_expr_with_context(some, writer_name, context);
            let some_value_expr = format!("({}).get()", option_expr);
            let remapped_inner = replace_identifier_occurrences(&inner, "v", &some_value_expr);
            format!(
                "if (({}).isPresent()) {{ {}.writeI8((byte)1); {}; }} else {{ {}.writeI8((byte)0); }}",
                option_expr, writer_name, remapped_inner, writer_name,
            )
        }
        WriteOp::Record { value, .. } => {
            format!("{}.wireEncodeTo({})", render_value(value), writer_name)
        }
        WriteOp::Enum { value, layout, .. } => match layout {
            EnumLayout::CStyle {
                tag_type,
                tag_strategy,
                ..
            } => match tag_strategy {
                EnumTagStrategy::Discriminant => {
                    format!(
                        "{}.{}({}.value)",
                        writer_name,
                        primitive_write_method(*tag_type),
                        render_value(value),
                    )
                }
                EnumTagStrategy::OrdinalIndex => {
                    format!(
                        "{}.writeI32({}.wireTag())",
                        writer_name,
                        render_value(value),
                    )
                }
            },
            EnumLayout::Data { .. } | EnumLayout::Recursive => {
                format!("{}.wireEncodeTo({})", render_value(value), writer_name)
            }
        },
        WriteOp::Result { value, ok, err } => {
            let result_expr = render_value(value);
            let ok_inner = emit_write_expr_with_context(ok, writer_name, context);
            let err_inner = emit_write_expr_with_context(err, writer_name, context);
            let ok_value_expr = format!("({}).okValue()", result_expr);
            let err_value_expr = format!("({}).errValue()", result_expr);
            let remapped_ok = replace_identifier_occurrences(&ok_inner, "okVal", &ok_value_expr);
            let remapped_err =
                replace_identifier_occurrences(&err_inner, "errVal", &err_value_expr);
            format!(
                "if (({}).isOk()) {{ {}.writeI8((byte)0); {}; }} else {{ {}.writeI8((byte)1); {}; }}",
                result_expr, writer_name, remapped_ok, writer_name, remapped_err,
            )
        }
        WriteOp::Vec {
            value,
            element_type,
            element,
            layout,
        } => emit_write_vec_with_context(
            writer_name,
            &render_value(value),
            element_type,
            element,
            layout,
            context,
        ),
        other => panic!("unsupported Java write op: {:?}", other),
    }
}

fn emit_write_vec_with_context(
    writer_name: &str,
    value: &str,
    element_type: &TypeExpr,
    element: &WriteSeq,
    layout: &VecLayout,
    context: &mut JavaEmitContext,
) -> String {
    match layout {
        VecLayout::Blittable { .. } => match element_type {
            TypeExpr::Primitive(primitive) => {
                format!(
                    "{}.{}({})",
                    writer_name,
                    primitive_array_write_method(*primitive),
                    value,
                )
            }
            TypeExpr::Record(id) => {
                format!(
                    "{}.encodeBlittableVec({}, {})",
                    NamingConvention::class_name(id.as_str()),
                    writer_name,
                    value,
                )
            }
            _ => {
                let inner = emit_write_expr_with_context(element, writer_name, context);
                let loop_var = context.next_write_loop_var();
                let remapped_inner = replace_identifier_occurrences(&inner, "item", &loop_var);
                let iter_type = java_type_for_iteration(element_type);
                format!(
                    "{}.writeI32({}.size()); for ({} {} : {}) {{ {}; }}",
                    writer_name, value, iter_type, loop_var, value, remapped_inner,
                )
            }
        },
        VecLayout::Encoded => match element_type {
            TypeExpr::Primitive(primitive) => {
                format!(
                    "{}.{}({})",
                    writer_name,
                    primitive_array_write_method(*primitive),
                    value,
                )
            }
            _ => {
                let inner = emit_write_expr_with_context(element, writer_name, context);
                let loop_var = context.next_write_loop_var();
                let remapped_inner = replace_identifier_occurrences(&inner, "item", &loop_var);
                let iter_type = java_type_for_iteration(element_type);
                format!(
                    "{}.writeI32({}.size()); for ({} {} : {}) {{ {}; }}",
                    writer_name, value, iter_type, loop_var, value, remapped_inner,
                )
            }
        },
    }
}

fn java_type_for_iteration(ty: &TypeExpr) -> String {
    match ty {
        TypeExpr::Primitive(primitive) => mappings::java_boxed_type(*primitive).to_string(),
        TypeExpr::String => "String".to_string(),
        TypeExpr::Bytes => "byte[]".to_string(),
        TypeExpr::Record(id) => NamingConvention::class_name(id.as_str()),
        TypeExpr::Enum(id) => NamingConvention::class_name(id.as_str()),
        TypeExpr::Option(inner) => {
            format!("java.util.Optional<{}>", java_type_for_iteration(inner))
        }
        TypeExpr::Vec(inner) => match inner.as_ref() {
            TypeExpr::Primitive(primitive) => {
                mappings::java_primitive_array_type(*primitive).to_string()
            }
            _ => format!("java.util.List<{}>", java_type_for_iteration(inner)),
        },
        _ => "Object".to_string(),
    }
}

fn primitive_array_write_method(primitive: PrimitiveType) -> &'static str {
    match primitive {
        PrimitiveType::Bool => "writeBooleanArray",
        PrimitiveType::I8 | PrimitiveType::U8 => "writeByteArray",
        PrimitiveType::I16 | PrimitiveType::U16 => "writeShortArray",
        PrimitiveType::I32 | PrimitiveType::U32 => "writeIntArray",
        PrimitiveType::I64 | PrimitiveType::U64 | PrimitiveType::ISize | PrimitiveType::USize => {
            "writeLongArray"
        }
        PrimitiveType::F32 => "writeFloatArray",
        PrimitiveType::F64 => "writeDoubleArray",
    }
}

pub fn emit_size_expr(size: &SizeExpr) -> String {
    let mut context = JavaEmitContext::default();
    emit_size_expr_with_context(size, &mut context)
}

fn emit_size_expr_with_context(size: &SizeExpr, context: &mut JavaEmitContext) -> String {
    match size {
        SizeExpr::Fixed(value) => value.to_string(),
        SizeExpr::StringLen(value) => {
            format!("(4 + ({}).length() * 3)", render_value(value),)
        }
        SizeExpr::BytesLen(value) => {
            format!("{}.length", render_value(value))
        }
        SizeExpr::WireSize { value, .. } => {
            format!("{}.wireEncodedSize()", render_value(value))
        }
        SizeExpr::OptionSize { value, inner } => {
            let option_expr = render_value(value);
            let inner_expr = emit_size_expr_with_context(inner, context);
            let some_value_expr = format!("({}).get()", option_expr);
            let remapped_inner = replace_identifier_occurrences(&inner_expr, "v", &some_value_expr);
            format!(
                "(1 + (({}).isPresent() ? ({}) : 0))",
                option_expr, remapped_inner,
            )
        }
        SizeExpr::ResultSize { value, ok, err } => {
            let result_expr = render_value(value);
            let ok_expr = emit_size_expr_with_context(ok, context);
            let err_expr = emit_size_expr_with_context(err, context);
            let ok_value_expr = format!("({}).okValue()", result_expr);
            let err_value_expr = format!("({}).errValue()", result_expr);
            let remapped_ok = replace_identifier_occurrences(&ok_expr, "okVal", &ok_value_expr);
            let remapped_err = replace_identifier_occurrences(&err_expr, "errVal", &err_value_expr);
            format!(
                "(1 + (({}).isOk() ? ({}) : ({})))",
                result_expr, remapped_ok, remapped_err,
            )
        }
        SizeExpr::VecSize {
            value,
            inner,
            layout,
        } => emit_vec_size_expr_with_context(&render_value(value), inner, layout, context),
        SizeExpr::Sum(parts) => {
            let rendered = parts
                .iter()
                .map(|part| emit_size_expr_with_context(part, context))
                .collect::<Vec<_>>()
                .join(" + ");
            format!("({})", rendered)
        }
        other => panic!("unsupported Java size expr: {:?}", other),
    }
}

fn emit_vec_size_expr_with_context(
    value: &str,
    inner: &SizeExpr,
    layout: &VecLayout,
    context: &mut JavaEmitContext,
) -> String {
    match layout {
        VecLayout::Blittable { element_size } => {
            format!("(4 + {} * {})", emit_vec_length_expr(value), element_size)
        }
        VecLayout::Encoded => {
            let inner_expr = emit_size_expr_with_context(inner, context);
            if inner_expr.contains("item") {
                let lambda_var = context.next_size_lambda();
                let remapped_inner =
                    replace_identifier_occurrences(&inner_expr, "item", &lambda_var);
                format!(
                    "WireWriter.listWireSize({}, {} -> {})",
                    value, lambda_var, remapped_inner,
                )
            } else {
                format!("(4 + {} * {})", emit_vec_length_expr(value), inner_expr)
            }
        }
    }
}

fn emit_vec_length_expr(value: &str) -> String {
    format!("WireWriter.vecLength({})", value)
}

fn replace_identifier_occurrences(expression: &str, identifier: &str, replacement: &str) -> String {
    if identifier.is_empty() {
        return expression.to_string();
    }

    let mut result = String::with_capacity(expression.len());
    let mut cursor = 0;

    while let Some(relative_index) = expression[cursor..].find(identifier) {
        let start = cursor + relative_index;
        let end = start + identifier.len();
        let previous = expression[..start].chars().next_back();
        let next = expression[end..].chars().next();
        let previous_is_identifier = previous.map(is_identifier_char).unwrap_or(false);
        let next_is_identifier = next.map(is_identifier_char).unwrap_or(false);

        if previous_is_identifier || next_is_identifier {
            result.push_str(&expression[cursor..end]);
            cursor = end;
        } else {
            result.push_str(&expression[cursor..start]);
            result.push_str(replacement);
            cursor = end;
        }
    }

    result.push_str(&expression[cursor..]);
    result
}

fn is_identifier_char(character: char) -> bool {
    character.is_ascii_alphanumeric() || character == '_'
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ir::Lowerer as IrLowerer;
    use crate::ir::contract::{FfiContract, PackageInfo};
    use crate::ir::definitions::{
        CallbackKind, CallbackMethodDef, CallbackTraitDef, ClassDef, ConstructorDef, FunctionDef,
        MethodDef, ParamDef, ParamPassing, Receiver, ReturnDef,
    };
    use crate::ir::ids::{CallbackId, ClassId, FunctionId, MethodId, ParamName};
    use crate::ir::types::{PrimitiveType, TypeExpr};
    use crate::render::java::JavaVersion;
    use boltffi_ffi_rules::callable::ExecutionKind;
    use std::env;
    use std::fs;
    use std::process::Command;
    use std::time::{SystemTime, UNIX_EPOCH};

    fn empty_contract() -> FfiContract {
        FfiContract {
            package: PackageInfo {
                name: "test".to_string(),
                version: None,
            },
            functions: vec![],
            catalog: Default::default(),
        }
    }

    fn class_def(id: &str, constructors: Vec<ConstructorDef>, methods: Vec<MethodDef>) -> ClassDef {
        ClassDef {
            id: ClassId::from(id),
            constructors,
            methods,
            streams: vec![],
            doc: None,
            deprecated: None,
        }
    }

    fn default_ctor(params: Vec<ParamDef>) -> ConstructorDef {
        ConstructorDef::Default {
            params,
            is_fallible: false,
            is_optional: false,
            doc: None,
            deprecated: None,
        }
    }

    fn param(name: &str, type_expr: TypeExpr) -> ParamDef {
        ParamDef {
            name: ParamName::from(name),
            type_expr,
            passing: ParamPassing::Value,
            doc: None,
        }
    }

    fn instance_method(name: &str, params: Vec<ParamDef>, returns: ReturnDef) -> MethodDef {
        MethodDef {
            id: MethodId::from(name),
            receiver: Receiver::RefSelf,
            params,
            returns,
            execution_kind: ExecutionKind::Sync,
            doc: None,
            deprecated: None,
        }
    }

    fn async_callback_method(
        name: &str,
        params: Vec<ParamDef>,
        returns: ReturnDef,
    ) -> CallbackMethodDef {
        CallbackMethodDef {
            id: MethodId::from(name),
            params,
            returns,
            execution_kind: ExecutionKind::Async,
            doc: None,
        }
    }

    #[test]
    fn emit_generates_class_file() {
        let mut contract = empty_contract();
        contract
            .catalog
            .insert_class(class_def("counter", vec![default_ctor(vec![])], vec![]));
        let abi = IrLowerer::new(&contract).to_abi_contract();

        let output = JavaEmitter::emit(
            &contract,
            &abi,
            "com.test".to_string(),
            "test".to_string(),
            JavaOptions::default(),
        );

        let class_file = output
            .files
            .iter()
            .find(|file| file.file_name == "Counter.java");
        assert!(class_file.is_some());
        let class_source = class_file
            .map(|file| file.source.as_str())
            .expect("class file should exist");
        assert!(class_source.contains("implements AutoCloseable"));
    }

    #[test]
    fn emit_main_file_includes_class_native_declarations() {
        let mut contract = empty_contract();
        contract.catalog.insert_class(class_def(
            "counter",
            vec![default_ctor(vec![])],
            vec![instance_method(
                "get",
                vec![param("slot", TypeExpr::Primitive(PrimitiveType::I32))],
                ReturnDef::Value(TypeExpr::Primitive(PrimitiveType::I32)),
            )],
        ));
        let abi = IrLowerer::new(&contract).to_abi_contract();

        let output = JavaEmitter::emit(
            &contract,
            &abi,
            "com.test".to_string(),
            "test".to_string(),
            JavaOptions::default(),
        );

        let main_file = output
            .files
            .iter()
            .find(|file| file.file_name == "Test.java")
            .expect("main file should exist");

        assert!(
            main_file
                .source
                .contains("static native void boltffi_counter_free(long handle);")
        );
        assert!(
            main_file
                .source
                .contains("static native long boltffi_counter_new();")
        );
        assert!(
            main_file
                .source
                .contains("static native int boltffi_counter_get(long handle, int slot);")
        );
    }

    #[test]
    fn emit_generated_java_compiles_with_javac_when_available() {
        if Command::new("javac").arg("-version").output().is_err() {
            return;
        }

        let mut contract = empty_contract();
        contract.catalog.insert_class(class_def(
            "counter",
            vec![default_ctor(vec![])],
            vec![instance_method(
                "get",
                vec![param("slot", TypeExpr::Primitive(PrimitiveType::I32))],
                ReturnDef::Value(TypeExpr::Primitive(PrimitiveType::I32)),
            )],
        ));
        let abi = IrLowerer::new(&contract).to_abi_contract();
        let output = JavaEmitter::emit(
            &contract,
            &abi,
            "com.test".to_string(),
            "test".to_string(),
            JavaOptions::default(),
        );

        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("time should be monotonic")
            .as_nanos();
        let tmp_root = env::temp_dir().join(format!("boltffi-java-{}", nanos));
        let package_dir = tmp_root.join(&output.package_path);
        fs::create_dir_all(&package_dir).expect("should create package directory");

        let source_paths: Vec<_> = output
            .files
            .iter()
            .map(|file| {
                let path = package_dir.join(&file.file_name);
                fs::write(&path, &file.source).expect("should write generated source");
                path
            })
            .collect();

        let status = Command::new("javac")
            .args(source_paths)
            .status()
            .expect("javac should execute");
        assert!(status.success());

        let _ = fs::remove_dir_all(tmp_root);
    }

    #[test]
    fn emit_callback_return_java_compiles_with_javac_when_available() {
        if Command::new("javac").arg("-version").output().is_err() {
            return;
        }

        let mut contract = empty_contract();
        contract.catalog.insert_callback(CallbackTraitDef {
            id: CallbackId::new("Listener"),
            methods: vec![CallbackMethodDef {
                id: MethodId::from("on_value"),
                params: vec![param("value", TypeExpr::Primitive(PrimitiveType::I32))],
                returns: ReturnDef::Value(TypeExpr::Primitive(PrimitiveType::I32)),
                execution_kind: ExecutionKind::Sync,
                doc: None,
            }],
            kind: CallbackKind::Trait,
            doc: None,
        });
        contract.functions.push(FunctionDef {
            id: FunctionId::new("next_listener"),
            params: vec![],
            returns: ReturnDef::Value(TypeExpr::Callback(CallbackId::new("Listener"))),
            execution_kind: ExecutionKind::Sync,
            doc: None,
            deprecated: None,
        });

        let abi = IrLowerer::new(&contract).to_abi_contract();
        let output = JavaEmitter::emit(
            &contract,
            &abi,
            "com.test".to_string(),
            "test".to_string(),
            JavaOptions::default(),
        );

        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("time should be monotonic")
            .as_nanos();
        let tmp_root = env::temp_dir().join(format!("boltffi-java-callbacks-{}", nanos));
        let package_dir = tmp_root.join(&output.package_path);
        fs::create_dir_all(&package_dir).expect("should create package directory");

        let source_paths: Vec<_> = output
            .files
            .iter()
            .map(|file| {
                let path = package_dir.join(&file.file_name);
                fs::write(&path, &file.source).expect("should write generated source");
                path
            })
            .collect();

        let status = Command::new("javac")
            .args(source_paths)
            .status()
            .expect("javac should execute");
        assert!(status.success());

        let _ = fs::remove_dir_all(tmp_root);
    }

    fn async_function(name: &str, params: Vec<ParamDef>, returns: ReturnDef) -> FunctionDef {
        FunctionDef {
            id: FunctionId::new(name),
            params,
            returns,
            execution_kind: ExecutionKind::Async,
            doc: None,
            deprecated: None,
        }
    }

    fn async_instance_method(name: &str, params: Vec<ParamDef>, returns: ReturnDef) -> MethodDef {
        MethodDef {
            id: MethodId::from(name),
            receiver: Receiver::RefSelf,
            params,
            returns,
            execution_kind: ExecutionKind::Async,
            doc: None,
            deprecated: None,
        }
    }

    fn static_method(name: &str, params: Vec<ParamDef>, returns: ReturnDef) -> MethodDef {
        MethodDef {
            id: MethodId::from(name),
            receiver: Receiver::Static,
            params,
            returns,
            execution_kind: ExecutionKind::Sync,
            doc: None,
            deprecated: None,
        }
    }

    #[test]
    fn emit_async_java_compiles_with_javac_when_available() {
        if Command::new("javac").arg("-version").output().is_err() {
            return;
        }

        let mut contract = empty_contract();

        contract.functions.push(async_function(
            "fetch_name",
            vec![param("id", TypeExpr::Primitive(PrimitiveType::I64))],
            ReturnDef::Value(TypeExpr::String),
        ));
        contract
            .functions
            .push(async_function("fire_event", vec![], ReturnDef::Void));
        contract.functions.push(async_function(
            "get_count",
            vec![],
            ReturnDef::Value(TypeExpr::Primitive(PrimitiveType::I32)),
        ));

        contract.catalog.insert_class(class_def(
            "session",
            vec![default_ctor(vec![])],
            vec![
                async_instance_method("load", vec![], ReturnDef::Value(TypeExpr::String)),
                async_instance_method("save", vec![], ReturnDef::Void),
                instance_method(
                    "get_id",
                    vec![],
                    ReturnDef::Value(TypeExpr::Primitive(PrimitiveType::I64)),
                ),
                static_method(
                    "count",
                    vec![],
                    ReturnDef::Value(TypeExpr::Primitive(PrimitiveType::I32)),
                ),
            ],
        ));

        let abi = IrLowerer::new(&contract).to_abi_contract();
        let output = JavaEmitter::emit(
            &contract,
            &abi,
            "com.test".to_string(),
            "test".to_string(),
            JavaOptions::default(),
        );

        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("time should be monotonic")
            .as_nanos();
        let tmp_root = env::temp_dir().join(format!("boltffi-java-async-{}", nanos));
        let package_dir = tmp_root.join(&output.package_path);
        fs::create_dir_all(&package_dir).expect("should create package directory");

        let source_paths: Vec<_> = output
            .files
            .iter()
            .map(|file| {
                let path = package_dir.join(&file.file_name);
                fs::write(&path, &file.source).expect("should write generated source");
                path
            })
            .collect();

        let status = Command::new("javac")
            .args(&source_paths)
            .status()
            .expect("javac should execute");

        if !status.success() {
            for file in &output.files {
                eprintln!("=== {} ===\n{}", file.file_name, file.source);
            }
        }
        assert!(status.success(), "async Java sources should compile");

        let _ = fs::remove_dir_all(tmp_root);
    }

    #[test]
    fn emit_async_vt_java_compiles_with_javac_when_available() {
        if Command::new("javac").arg("-version").output().is_err() {
            return;
        }

        let mut contract = empty_contract();

        contract.functions.push(async_function(
            "fetch_name",
            vec![param("id", TypeExpr::Primitive(PrimitiveType::I64))],
            ReturnDef::Value(TypeExpr::String),
        ));
        contract
            .functions
            .push(async_function("fire_event", vec![], ReturnDef::Void));
        contract.functions.push(async_function(
            "get_count",
            vec![],
            ReturnDef::Value(TypeExpr::Primitive(PrimitiveType::I32)),
        ));

        contract.catalog.insert_class(class_def(
            "session",
            vec![default_ctor(vec![])],
            vec![
                async_instance_method("load", vec![], ReturnDef::Value(TypeExpr::String)),
                async_instance_method("save", vec![], ReturnDef::Void),
                instance_method(
                    "get_id",
                    vec![],
                    ReturnDef::Value(TypeExpr::Primitive(PrimitiveType::I64)),
                ),
                static_method(
                    "count",
                    vec![],
                    ReturnDef::Value(TypeExpr::Primitive(PrimitiveType::I32)),
                ),
            ],
        ));

        let abi = IrLowerer::new(&contract).to_abi_contract();
        let output = JavaEmitter::emit(
            &contract,
            &abi,
            "com.test".to_string(),
            "test".to_string(),
            JavaOptions {
                min_java_version: JavaVersion::JAVA_21,
                ..Default::default()
            },
        );

        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("time should be monotonic")
            .as_nanos();
        let tmp_root = env::temp_dir().join(format!("boltffi-java-async-vt-{}", nanos));
        let package_dir = tmp_root.join(&output.package_path);
        fs::create_dir_all(&package_dir).expect("should create package directory");

        let source_paths: Vec<_> = output
            .files
            .iter()
            .map(|file| {
                let path = package_dir.join(&file.file_name);
                fs::write(&path, &file.source).expect("should write generated source");
                path
            })
            .collect();

        let status = Command::new("javac")
            .args(&source_paths)
            .status()
            .expect("javac should execute");

        if !status.success() {
            for file in &output.files {
                eprintln!("=== {} ===\n{}", file.file_name, file.source);
            }
        }
        assert!(
            status.success(),
            "async virtual-thread Java sources should compile"
        );

        let _ = fs::remove_dir_all(tmp_root);
    }

    #[test]
    fn emit_async_callback_bridge_normalizes_throwable_paths() {
        let mut contract = empty_contract();
        contract.catalog.insert_callback(CallbackTraitDef {
            id: CallbackId::new("listener"),
            methods: vec![async_callback_method(
                "fetch",
                vec![param("id", TypeExpr::Primitive(PrimitiveType::I32))],
                ReturnDef::Result {
                    ok: TypeExpr::Primitive(PrimitiveType::I32),
                    err: TypeExpr::String,
                },
            )],
            kind: CallbackKind::Trait,
            doc: None,
        });

        let abi = IrLowerer::new(&contract).to_abi_contract();
        let output = JavaEmitter::emit(
            &contract,
            &abi,
            "com.test".to_string(),
            "test".to_string(),
            JavaOptions::default(),
        );

        let callback_file = output
            .files
            .iter()
            .find(|file| file.file_name == "ListenerCallbacks.java")
            .expect("callback bridge should be generated");

        assert!(
            callback_file
                .source
                .contains("future.completeExceptionally(throwable);")
        );
        assert!(
            callback_file
                .source
                .contains("Throwable callbackThrowable = unwrapAsyncThrowable(throwable);")
        );
        assert!(
            callback_file
                .source
                .contains("callbackThrowable instanceof RuntimeException")
        );
    }
}
