use std::path::{Path, PathBuf};

use crate::build::CargoBuildProfile;
use crate::cargo::Cargo;
use crate::cli::{CliError, Result};
use crate::commands::generate::run_generate_java_with_output_from_source_dir;
use crate::config::{Config, DebugSymbolsBundle, DebugSymbolsFormat, Target};
use crate::pack::resolve_build_cargo_args;
use crate::pack::symbols::{
    DebugSymbolArtifact, DebugSymbolArtifactKind, ensure_debug_symbols_profile_has_debuginfo,
    write_debug_symbols_zip,
};
use crate::reporter::Reporter;
use crate::target::JavaHostTarget;
use crate::toolchain::NativeHostToolchain;

use super::link::{
    build_jvm_native_library, compile_jni_library, resolve_jni_include_directories,
    validate_desktop_jni_symbol_stripping_for,
};
use super::outputs::{
    remove_stale_flat_jvm_outputs_if_current_host_unrequested,
    remove_stale_requested_jvm_shared_library_copies_after_success,
    remove_stale_structured_jvm_outputs,
};

#[derive(Debug, Clone)]
pub(crate) struct JvmCargoContext {
    pub(crate) host_target: JavaHostTarget,
    pub(crate) rust_target_triple: String,
    pub(crate) release: bool,
    pub(crate) build_profile: CargoBuildProfile,
    pub(crate) artifact_name: String,
    pub(crate) cargo_manifest_path: PathBuf,
    pub(crate) manifest_path: PathBuf,
    pub(crate) package_selector: Option<String>,
    pub(crate) target_directory: PathBuf,
    pub(crate) cargo_command_args: Vec<String>,
    pub(crate) toolchain_selector: Option<String>,
    pub(crate) crate_outputs: JvmCrateOutputs,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct JvmCrateOutputs {
    pub(crate) builds_staticlib: bool,
    pub(crate) builds_cdylib: bool,
}

pub(crate) struct JvmPackagingTarget {
    pub(crate) cargo_context: JvmCargoContext,
    pub(crate) toolchain: NativeHostToolchain,
}

pub(crate) struct PreparedJvmPackaging {
    pub(crate) host_targets: Vec<JavaHostTarget>,
    pub(crate) packaging_targets: Vec<JvmPackagingTarget>,
}

impl JvmCargoContext {
    pub(crate) fn artifact_directory(&self) -> PathBuf {
        self.target_directory
            .join(&self.rust_target_triple)
            .join(self.build_profile.output_directory_name())
    }
}

pub(crate) fn check_java_packaging_prereqs(
    config: &Config,
    release: bool,
    cargo_args: &[String],
) -> Result<()> {
    prepare_java_packaging(config, release, cargo_args).map(|_| ())
}

pub(crate) fn pack_java(
    config: &Config,
    options: crate::commands::pack::PackJavaOptions,
    prepared: Option<PreparedJvmPackaging>,
    reporter: &Reporter,
) -> Result<()> {
    if !config.is_java_jvm_enabled() {
        return Err(CliError::CommandFailed {
            command: "targets.java.jvm.enabled = false".to_string(),
            status: None,
        });
    }

    reporter.section("☕", "Packing Java");

    ensure_java_no_build_supported(
        config,
        options.execution.no_build,
        options.experimental,
        "pack java",
    )?;

    let PreparedJvmPackaging {
        host_targets,
        packaging_targets,
    } = if let Some(prepared) = prepared {
        prepared
    } else {
        let step = reporter.step("Validating JVM toolchains");
        let prepared = prepare_java_packaging(
            config,
            options.execution.release,
            &options.execution.cargo_args,
        )?;
        step.finish_success();
        prepared
    };

    if options.execution.regenerate {
        let source_directory = selected_jvm_package_source_directory(&packaging_targets)?;
        let artifact_name = selected_jvm_package_artifact_name(&packaging_targets)?;
        let step = reporter.step("Generating C header");
        generate_java_header(config, &source_directory, artifact_name)?;
        step.finish_success();

        let step = reporter.step("Generating Java bindings");
        run_generate_java_with_output_from_source_dir(
            config,
            Some(config.java_jvm_output()),
            &source_directory,
            artifact_name,
        )?;
        step.finish_success();
    }

    let mut packaged_outputs = Vec::with_capacity(packaging_targets.len());
    for packaging_target in &packaging_targets {
        let host_target = packaging_target.cargo_context.host_target;
        let step = reporter.step(&format!(
            "Building Rust library for {}",
            host_target.canonical_name()
        ));
        let build_artifacts =
            build_jvm_native_library(packaging_target, options.execution.release, &step)?;
        step.finish_success();

        let step = reporter.step(&format!(
            "Compiling JNI library for {}",
            host_target.canonical_name()
        ));
        packaged_outputs.push(compile_jni_library(
            config,
            packaging_target,
            &build_artifacts,
            &step,
        )?);
        step.finish_success();
    }

    let artifact_name = selected_jvm_package_artifact_name(&packaging_targets)?;
    if config.java_jvm_debug_symbols_enabled() {
        let step = reporter.step("Bundling JVM debug symbols");
        write_jvm_debug_symbols(config, artifact_name, &packaged_outputs)?;
        step.finish_success();
    }
    remove_stale_requested_jvm_shared_library_copies_after_success(
        &config.java_jvm_output(),
        &packaged_outputs,
        artifact_name,
    )?;
    remove_stale_structured_jvm_outputs(&config.java_jvm_output().join("native"), &host_targets)?;
    remove_stale_flat_jvm_outputs_if_current_host_unrequested(
        &config.java_jvm_output(),
        JavaHostTarget::current(),
        &host_targets,
        artifact_name,
    )?;

    reporter.finish();
    Ok(())
}

pub(crate) fn prepare_java_packaging(
    config: &Config,
    release: bool,
    cargo_args: &[String],
) -> Result<PreparedJvmPackaging> {
    let prepared = prepare_jvm_packaging_matrix(
        config,
        release,
        cargo_args,
        config.java_jvm_strip_symbols(),
        "pack java",
    )?;
    if config.java_jvm_debug_symbols_enabled() {
        let build_cargo_args = resolve_build_cargo_args(config, cargo_args);
        ensure_debug_symbols_profile_has_debuginfo(
            &build_cargo_args,
            &prepared.packaging_targets[0].cargo_context.build_profile,
            "targets.java.jvm.debug_symbols",
            &prepared
                .packaging_targets
                .iter()
                .map(|target| target.cargo_context.rust_target_triple.clone())
                .collect::<Vec<_>>(),
        )?;
    }

    Ok(prepared)
}

pub(crate) fn prepare_kmp_jvm_packaging(
    config: &Config,
    release: bool,
    cargo_args: &[String],
) -> Result<PreparedJvmPackaging> {
    prepare_jvm_packaging_matrix(config, release, cargo_args, false, "pack kmp")
}

fn prepare_jvm_packaging_matrix(
    config: &Config,
    release: bool,
    cargo_args: &[String],
    strip_symbols: bool,
    command_name: &str,
) -> Result<PreparedJvmPackaging> {
    let build_cargo_args = resolve_build_cargo_args(config, cargo_args);
    ensure_jvm_pack_cargo_args_supported(&build_cargo_args, command_name)?;
    let build_profile = crate::build::resolve_build_profile(release, &build_cargo_args);
    let host_targets = resolve_java_host_targets_for_packaging(config)?;
    if host_targets.is_empty() {
        return Err(CliError::CommandFailed {
            command: "targets.java.jvm.host_targets must be non-empty when provided".to_string(),
            status: None,
        });
    }
    let packaging_targets = resolve_jvm_packaging_targets(
        config,
        &build_cargo_args,
        release,
        build_profile,
        &host_targets,
        strip_symbols,
    )?;

    Ok(PreparedJvmPackaging {
        host_targets,
        packaging_targets,
    })
}

pub(crate) fn ensure_java_no_build_supported(
    config: &Config,
    no_build: bool,
    experimental: bool,
    command_name: &str,
) -> Result<()> {
    if no_build && config.should_process(Target::Java, experimental) {
        return Err(CliError::CommandFailed {
            command: format!(
                "{command_name} --no-build is unsupported in Phase 4 when JVM packaging is enabled; rerun without --no-build"
            ),
            status: None,
        });
    }

    Ok(())
}

fn ensure_jvm_pack_cargo_args_supported(cargo_args: &[String], command_name: &str) -> Result<()> {
    if let Some(target_selector) = Cargo::current(cargo_args)?.target_selector() {
        return Err(CliError::CommandFailed {
            command: format!(
                "{command_name} resolves desktop targets from targets.java.jvm.host_targets; remove cargo --target '{}'",
                target_selector
            ),
            status: None,
        });
    }

    Ok(())
}

pub(crate) fn selected_jvm_package_source_directory(
    packaging_targets: &[JvmPackagingTarget],
) -> Result<PathBuf> {
    packaging_targets
        .first()
        .and_then(|target| target.cargo_context.manifest_path.parent())
        .map(Path::to_path_buf)
        .ok_or_else(|| CliError::CommandFailed {
            command: "could not resolve selected Cargo package source directory for JVM generation"
                .to_string(),
            status: None,
        })
}

pub(crate) fn selected_jvm_package_artifact_name(
    packaging_targets: &[JvmPackagingTarget],
) -> Result<&str> {
    packaging_targets
        .first()
        .map(|target| target.cargo_context.artifact_name.as_str())
        .ok_or_else(|| CliError::CommandFailed {
            command: "could not resolve selected Cargo package artifact name for JVM generation"
                .to_string(),
            status: None,
        })
}

pub(crate) fn generate_java_header(
    config: &Config,
    source_directory: &Path,
    crate_name: &str,
) -> Result<()> {
    generate_jvm_header(
        source_directory,
        crate_name,
        &config.java_jvm_output().join("jni"),
        crate_name,
    )
}

pub(crate) fn generate_jvm_header(
    source_directory: &Path,
    crate_name: &str,
    output_directory: &Path,
    header_name: &str,
) -> Result<()> {
    use boltffi_bindgen::{CHeaderLowerer, ir, scan_crate_with_pointer_width};

    let output_path = output_directory.join(format!("{header_name}.h"));

    std::fs::create_dir_all(output_directory).map_err(|source| {
        CliError::CreateDirectoryFailed {
            path: output_directory.to_path_buf(),
            source,
        }
    })?;
    let host_pointer_width_bits = match usize::BITS {
        32 => Some(32),
        64 => Some(64),
        _ => None,
    };
    let mut module =
        scan_crate_with_pointer_width(source_directory, crate_name, host_pointer_width_bits)
            .map_err(|error| CliError::CommandFailed {
                command: format!("scan_crate: {}", error),
                status: None,
            })?;

    let contract = ir::build_contract(&mut module);
    let abi = ir::Lowerer::new(&contract).to_abi_contract();
    let header_code = CHeaderLowerer::new(&contract, &abi).generate();
    std::fs::write(&output_path, header_code).map_err(|source| CliError::WriteFailed {
        path: output_path,
        source,
    })?;

    Ok(())
}

fn write_jvm_debug_symbols(
    config: &Config,
    artifact_name: &str,
    outputs: &[super::link::JvmPackagedNativeOutput],
) -> Result<()> {
    let mut artifacts = Vec::with_capacity(outputs.len() * 2);
    for output in outputs {
        artifacts.push(DebugSymbolArtifact {
            source_path: output.jni_library_path.clone(),
            archive_path: PathBuf::from("native")
                .join(output.host_target.canonical_name())
                .join(
                    output
                        .jni_library_path
                        .file_name()
                        .expect("jni library should have a filename"),
                ),
            kind: DebugSymbolArtifactKind::Jni,
            target_triple: None,
            platform: None,
            architecture: None,
            abi: None,
            host_target: Some(output.host_target.canonical_name().to_string()),
        });

        if let Some(shared_library_path) = output.shared_library_path.as_ref() {
            artifacts.push(DebugSymbolArtifact {
                source_path: shared_library_path.clone(),
                archive_path: PathBuf::from("native")
                    .join(output.host_target.canonical_name())
                    .join(
                        shared_library_path
                            .file_name()
                            .expect("shared library should have a filename"),
                    ),
                kind: DebugSymbolArtifactKind::Shared,
                target_triple: None,
                platform: None,
                architecture: None,
                abi: None,
                host_target: Some(output.host_target.canonical_name().to_string()),
            });
        }

        for sidecar_path in &output.debug_info_sidecars {
            artifacts.push(DebugSymbolArtifact {
                source_path: sidecar_path.clone(),
                archive_path: PathBuf::from("native")
                    .join(output.host_target.canonical_name())
                    .join(
                        sidecar_path
                            .file_name()
                            .expect("debug info sidecar should have a filename"),
                    ),
                kind: DebugSymbolArtifactKind::DebugInfo,
                target_triple: None,
                platform: None,
                architecture: None,
                abi: None,
                host_target: Some(output.host_target.canonical_name().to_string()),
            });
        }
    }

    write_debug_symbols_zip(
        &config.java_jvm_debug_symbols_output(),
        &match config.java_jvm_debug_symbols_format() {
            DebugSymbolsFormat::Zip => format!("{artifact_name}.jvm.symbols.zip"),
        },
        "java-jvm",
        match config.java_jvm_debug_symbols_bundle() {
            DebugSymbolsBundle::Unstripped => "unstripped",
        },
        &artifacts,
    )?;

    Ok(())
}

fn resolve_java_host_targets_for_packaging(config: &Config) -> Result<Vec<JavaHostTarget>> {
    config
        .java_jvm_host_targets()
        .map_err(|message| CliError::CommandFailed {
            command: message,
            status: None,
        })
}

fn resolve_jvm_packaging_targets(
    config: &Config,
    build_cargo_args: &[String],
    release: bool,
    build_profile: CargoBuildProfile,
    host_targets: &[JavaHostTarget],
    strip_symbols: bool,
) -> Result<Vec<JvmPackagingTarget>> {
    let current_host = JavaHostTarget::current().ok_or_else(|| CliError::CommandFailed {
        command:
            "JVM packaging is only supported on darwin-arm64, darwin-x86_64, linux-x86_64, linux-aarch64, and windows-x86_64 hosts".to_string(),
        status: None,
    })?;
    let cargo = Cargo::current(build_cargo_args)?;
    let metadata = cargo.metadata()?;
    let cargo_manifest_path = cargo.manifest_path()?;
    let package_selector =
        cargo.effective_package_selector(config, &metadata, &cargo_manifest_path);
    let package = metadata.find_package(&cargo_manifest_path, package_selector.as_deref())?;
    let artifact_name = package
        .resolve_library_artifact_name(&config.crate_artifact_name(), &cargo_manifest_path)?
        .to_string();
    let toolchain_selector = cargo.toolchain_selector().map(str::to_owned);
    let cargo_command_args = cargo.probe_command_arguments();
    let crate_outputs = JvmCrateOutputs::from_metadata(
        &metadata,
        &artifact_name,
        &cargo_manifest_path,
        package_selector.as_deref(),
    )?;

    host_targets
        .iter()
        .copied()
        .map(|host_target| {
            validate_desktop_jni_symbol_stripping_for(strip_symbols, host_target)?;
            let toolchain = NativeHostToolchain::discover(
                toolchain_selector.as_deref(),
                &cargo_command_args,
                host_target,
                current_host,
            )?;
            let cargo_context = JvmCargoContext {
                host_target,
                rust_target_triple: toolchain.rust_target_triple().to_string(),
                release,
                build_profile: build_profile.clone(),
                artifact_name: artifact_name.clone(),
                cargo_manifest_path: cargo_manifest_path.clone(),
                manifest_path: package.manifest_path.clone(),
                package_selector: package_selector.clone(),
                target_directory: metadata.target_directory.clone(),
                cargo_command_args: cargo_command_args.clone(),
                toolchain_selector: toolchain_selector.clone(),
                crate_outputs,
            };
            let _ = resolve_jni_include_directories(&cargo_context)?;
            Ok(JvmPackagingTarget {
                cargo_context,
                toolchain,
            })
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use std::fs;
    use std::path::PathBuf;
    use std::time::{SystemTime, UNIX_EPOCH};

    use super::{
        JvmCargoContext, JvmCrateOutputs, JvmPackagingTarget, ensure_java_no_build_supported,
        ensure_jvm_pack_cargo_args_supported, resolve_jvm_packaging_targets,
        selected_jvm_package_source_directory, write_jvm_debug_symbols,
    };
    use crate::build::CargoBuildProfile;
    use crate::cli::CliError;
    use crate::config::{CargoConfig, Config, PackageConfig, TargetsConfig};
    use crate::pack::java::link::JvmPackagedNativeOutput;
    use crate::target::JavaHostTarget;
    use crate::toolchain::NativeHostToolchain;

    fn temporary_directory(prefix: &str) -> PathBuf {
        let unique = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("time went backwards")
            .as_nanos();
        std::env::temp_dir().join(format!("{prefix}-{unique}"))
    }

    fn config(java_enabled: bool) -> Config {
        Config {
            experimental: Vec::new(),
            cargo: CargoConfig::default(),
            package: PackageConfig {
                name: "workspace-member".to_string(),
                crate_name: None,
                version: None,
                description: None,
                license: None,
                repository: None,
            },
            targets: TargetsConfig {
                java: crate::config::JavaConfig {
                    jvm: crate::config::JavaJvmConfig {
                        enabled: java_enabled,
                        ..Default::default()
                    },
                    ..Default::default()
                },
                ..Default::default()
            },
        }
    }

    fn config_with_host_targets(
        java_enabled: bool,
        host_targets: Vec<JavaHostTarget>,
        strip_symbols: bool,
    ) -> Config {
        let mut config = config(java_enabled);
        config.targets.java.jvm.host_targets = Some(host_targets);
        config.targets.java.jvm.strip_symbols = strip_symbols;
        config
    }

    #[test]
    fn rejects_pack_all_no_build_when_java_is_enabled() {
        let error = ensure_java_no_build_supported(&config(true), true, false, "pack all")
            .expect_err("expected no-build rejection");

        assert!(matches!(
            error,
            CliError::CommandFailed { command, status: None }
                if command.contains("pack all --no-build is unsupported in Phase 4")
        ));
    }

    #[test]
    fn allows_pack_all_no_build_when_java_is_disabled() {
        ensure_java_no_build_supported(&config(false), true, false, "pack all")
            .expect("expected no-build to be allowed");
    }

    #[test]
    fn rejects_explicit_cargo_target_for_pack_java() {
        let error = ensure_jvm_pack_cargo_args_supported(
            &[
                "--target".to_string(),
                "x86_64-unknown-linux-gnu".to_string(),
            ],
            "pack java",
        )
        .expect_err("expected explicit target rejection");

        assert!(matches!(
            error,
            CliError::CommandFailed { command, status: None }
                if command.contains("remove cargo --target 'x86_64-unknown-linux-gnu'")
        ));
    }

    #[test]
    fn rejects_explicit_cargo_target_for_pack_kmp_with_kmp_command_name() {
        let error = ensure_jvm_pack_cargo_args_supported(
            &[
                "--target".to_string(),
                "x86_64-unknown-linux-gnu".to_string(),
            ],
            "pack kmp",
        )
        .expect_err("expected explicit target rejection");

        assert!(matches!(
            error,
            CliError::CommandFailed { command, status: None }
                if command.contains("pack kmp resolves desktop targets")
                    && command.contains("targets.java.jvm.host_targets")
        ));
    }

    #[test]
    fn pack_java_no_longer_requires_experimental_gate() {
        ensure_java_no_build_supported(&config(true), false, false, "pack java")
            .expect("expected pack java to proceed without experimental gate");
    }

    #[test]
    fn rejects_windows_strip_symbols_during_preflight() {
        let error = match resolve_jvm_packaging_targets(
            &config_with_host_targets(true, vec![JavaHostTarget::WindowsX86_64], true),
            &[],
            false,
            CargoBuildProfile::Named("dist".to_string()),
            &[JavaHostTarget::WindowsX86_64],
            true,
        ) {
            Ok(_) => panic!("expected unsupported windows strip config to fail during preflight"),
            Err(error) => error,
        };

        assert!(matches!(
            error,
            CliError::CommandFailed { status: None, .. }
        ));
    }

    #[test]
    fn resolves_selected_jvm_package_source_directory_from_selected_package_manifest() {
        let current_host = JavaHostTarget::current().expect("current host");
        let packaging_targets = vec![JvmPackagingTarget {
            cargo_context: JvmCargoContext {
                host_target: current_host,
                rust_target_triple: "x86_64-unknown-linux-gnu".to_string(),
                release: false,
                build_profile: CargoBuildProfile::Debug,
                artifact_name: "workspace_member".to_string(),
                cargo_manifest_path: PathBuf::from("/tmp/workspace/Cargo.toml"),
                manifest_path: PathBuf::from("/tmp/workspace/member/Cargo.toml"),
                package_selector: Some("workspace-member".to_string()),
                target_directory: PathBuf::from("/tmp/boltffi-target"),
                cargo_command_args: Vec::new(),
                toolchain_selector: None,
                crate_outputs: JvmCrateOutputs {
                    builds_staticlib: true,
                    builds_cdylib: true,
                },
            },
            toolchain: NativeHostToolchain::discover(None, &[], current_host, current_host)
                .expect("native host toolchain"),
        }];

        let source_directory =
            selected_jvm_package_source_directory(&packaging_targets).expect("source directory");

        assert_eq!(source_directory, PathBuf::from("/tmp/workspace/member"));
    }

    #[test]
    fn jvm_debug_symbols_archive_uses_selected_artifact_name_and_includes_bundled_cdylib() {
        let temp_root = temporary_directory("boltffi-jvm-debug-symbols");
        fs::create_dir_all(&temp_root).expect("create temp root");
        let jni_library = temp_root.join("libdemo_jni.dylib");
        let shared_library = temp_root.join("libdemo.dylib");
        fs::write(&jni_library, b"jni").expect("write jni library");
        fs::write(&shared_library, b"shared").expect("write shared library");

        let config = Config {
            experimental: Vec::new(),
            cargo: CargoConfig::default(),
            package: PackageConfig {
                name: "workspace-root".to_string(),
                crate_name: None,
                version: None,
                description: None,
                license: None,
                repository: None,
            },
            targets: TargetsConfig {
                java: crate::config::JavaConfig {
                    jvm: crate::config::JavaJvmConfig {
                        enabled: true,
                        output: temp_root.join("java"),
                        ..Default::default()
                    },
                    ..Default::default()
                },
                ..Default::default()
            },
        };

        write_jvm_debug_symbols(
            &config,
            "workspace_member",
            &[JvmPackagedNativeOutput {
                host_target: JavaHostTarget::DarwinArm64,
                has_shared_library_copy: true,
                jni_library_path: jni_library,
                shared_library_path: Some(shared_library),
                debug_info_sidecars: Vec::new(),
            }],
        )
        .expect("write jvm debug symbols");

        let archive_path = config
            .java_jvm_debug_symbols_output()
            .join("workspace_member.jvm.symbols.zip");
        let archive_file = fs::File::open(&archive_path).expect("open archive");
        let mut archive = zip::ZipArchive::new(archive_file).expect("read zip archive");
        let bundle_root = "workspace_member.jvm.symbols";

        archive
            .by_name(&format!(
                "{bundle_root}/native/darwin-arm64/libdemo_jni.dylib"
            ))
            .expect("jni entry");
        archive
            .by_name(&format!("{bundle_root}/native/darwin-arm64/libdemo.dylib"))
            .expect("shared entry");

        fs::remove_dir_all(&temp_root).expect("cleanup temp dir");
    }

    #[test]
    fn jvm_debug_symbols_archive_includes_windows_pdb_sidecars() {
        let temp_root = temporary_directory("boltffi-jvm-debug-symbols-pdb");
        fs::create_dir_all(&temp_root).expect("create temp root");
        let jni_library = temp_root.join("demo_jni.dll");
        let jni_pdb = temp_root.join("demo_jni.pdb");
        let shared_library = temp_root.join("demo.dll");
        let shared_pdb = temp_root.join("demo.pdb");
        fs::write(&jni_library, b"jni").expect("write jni library");
        fs::write(&jni_pdb, b"jni-pdb").expect("write jni pdb");
        fs::write(&shared_library, b"shared").expect("write shared library");
        fs::write(&shared_pdb, b"shared-pdb").expect("write shared pdb");

        let config = Config {
            experimental: Vec::new(),
            cargo: CargoConfig::default(),
            package: PackageConfig {
                name: "workspace-root".to_string(),
                crate_name: None,
                version: None,
                description: None,
                license: None,
                repository: None,
            },
            targets: TargetsConfig {
                java: crate::config::JavaConfig {
                    jvm: crate::config::JavaJvmConfig {
                        enabled: true,
                        output: temp_root.join("java"),
                        ..Default::default()
                    },
                    ..Default::default()
                },
                ..Default::default()
            },
        };

        write_jvm_debug_symbols(
            &config,
            "workspace_member",
            &[JvmPackagedNativeOutput {
                host_target: JavaHostTarget::WindowsX86_64,
                has_shared_library_copy: true,
                jni_library_path: jni_library,
                shared_library_path: Some(shared_library),
                debug_info_sidecars: vec![jni_pdb, shared_pdb],
            }],
        )
        .expect("write jvm debug symbols");

        let archive_path = config
            .java_jvm_debug_symbols_output()
            .join("workspace_member.jvm.symbols.zip");
        let archive_file = fs::File::open(&archive_path).expect("open archive");
        let mut archive = zip::ZipArchive::new(archive_file).expect("read zip archive");
        let bundle_root = "workspace_member.jvm.symbols";

        archive
            .by_name(&format!("{bundle_root}/native/windows-x86_64/demo_jni.dll"))
            .expect("jni entry");
        archive
            .by_name(&format!("{bundle_root}/native/windows-x86_64/demo_jni.pdb"))
            .expect("jni pdb entry");
        archive
            .by_name(&format!("{bundle_root}/native/windows-x86_64/demo.dll"))
            .expect("shared entry");
        archive
            .by_name(&format!("{bundle_root}/native/windows-x86_64/demo.pdb"))
            .expect("shared pdb entry");

        fs::remove_dir_all(&temp_root).expect("cleanup temp dir");
    }

    #[test]
    fn jvm_debug_symbols_archive_includes_directory_sidecars() {
        let temp_root = temporary_directory("boltffi-jvm-debug-symbols-dsym");
        fs::create_dir_all(&temp_root).expect("create temp root");
        let jni_library = temp_root.join("libdemo_jni.dylib");
        let dsym_dir = temp_root.join("libdemo_jni.dylib.dSYM");
        let dsym_dwarf_dir = dsym_dir.join("Contents").join("Resources").join("DWARF");
        fs::write(&jni_library, b"jni").expect("write jni library");
        fs::create_dir_all(&dsym_dwarf_dir).expect("create dsym dwarf dir");
        fs::write(dsym_dir.join("Contents").join("Info.plist"), "<plist />")
            .expect("write dsym plist");
        fs::write(dsym_dwarf_dir.join("libdemo_jni.dylib"), b"debug").expect("write dsym dwarf");

        let config = Config {
            experimental: Vec::new(),
            cargo: CargoConfig::default(),
            package: PackageConfig {
                name: "workspace-root".to_string(),
                crate_name: None,
                version: None,
                description: None,
                license: None,
                repository: None,
            },
            targets: TargetsConfig {
                java: crate::config::JavaConfig {
                    jvm: crate::config::JavaJvmConfig {
                        enabled: true,
                        output: temp_root.join("java"),
                        ..Default::default()
                    },
                    ..Default::default()
                },
                ..Default::default()
            },
        };

        write_jvm_debug_symbols(
            &config,
            "workspace_member",
            &[JvmPackagedNativeOutput {
                host_target: JavaHostTarget::DarwinArm64,
                has_shared_library_copy: false,
                jni_library_path: jni_library,
                shared_library_path: None,
                debug_info_sidecars: vec![dsym_dir],
            }],
        )
        .expect("write jvm debug symbols");

        let archive_path = config
            .java_jvm_debug_symbols_output()
            .join("workspace_member.jvm.symbols.zip");
        let archive_file = fs::File::open(&archive_path).expect("open archive");
        let mut archive = zip::ZipArchive::new(archive_file).expect("read zip archive");
        let bundle_root = "workspace_member.jvm.symbols";

        archive
            .by_name(&format!(
                "{bundle_root}/native/darwin-arm64/libdemo_jni.dylib.dSYM/Contents/Info.plist"
            ))
            .expect("dsym plist entry");
        archive
            .by_name(&format!(
                "{bundle_root}/native/darwin-arm64/libdemo_jni.dylib.dSYM/Contents/Resources/DWARF/libdemo_jni.dylib"
            ))
            .expect("dsym dwarf entry");

        fs::remove_dir_all(&temp_root).expect("cleanup temp dir");
    }
}
