use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Arc, Mutex};

use crate::build::{CargoBuildProfile, OutputCallback, run_command_streaming};
use crate::cli::{CliError, Result};
use crate::config::Config;
use crate::pack::PackError;
use crate::pack::{format_command_for_log, print_cargo_line, print_verbose_detail};
use crate::reporter::Step;
use crate::target::JavaHostTarget;

use super::plan::{JvmCargoContext, JvmCrateOutputs, JvmPackagingTarget};

pub(crate) struct JvmBuildArtifacts {
    pub(crate) native_static_libraries: Vec<String>,
    pub(crate) native_link_search_paths: Vec<String>,
    pub(crate) static_library_filename: Option<String>,
}

#[derive(Debug, Clone)]
pub(crate) struct JvmNativePackageLayout {
    pub(crate) jni_dir: PathBuf,
    pub(crate) header_name: String,
    pub(crate) jni_library_name: String,
    pub(crate) native_output_root: PathBuf,
    pub(crate) flat_output_root: Option<PathBuf>,
    pub(crate) strip_symbols: bool,
    pub(crate) debug_symbols_enabled: bool,
}

impl JvmNativePackageLayout {
    pub(crate) fn java(config: &Config, artifact_name: &str) -> Self {
        let java_output = config.java_jvm_output();

        Self {
            jni_dir: java_output.join("jni"),
            header_name: artifact_name.to_string(),
            jni_library_name: artifact_name.to_string(),
            native_output_root: java_output.join("native"),
            flat_output_root: Some(java_output),
            strip_symbols: config.java_jvm_strip_symbols(),
            debug_symbols_enabled: config.java_jvm_debug_symbols_enabled(),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DesktopJniStripMode {
    Disabled,
    Automatic,
    Explicit,
}

#[cfg(test)]
fn desktop_jni_strip_mode(
    config: &Config,
    build_profile: &CargoBuildProfile,
) -> DesktopJniStripMode {
    desktop_jni_strip_mode_for(config.java_jvm_strip_symbols(), build_profile)
}

fn desktop_jni_strip_mode_for(
    configured_strip_symbols: bool,
    build_profile: &CargoBuildProfile,
) -> DesktopJniStripMode {
    if matches!(build_profile, CargoBuildProfile::Release) {
        DesktopJniStripMode::Automatic
    } else if matches!(build_profile, CargoBuildProfile::Named(_)) && configured_strip_symbols {
        DesktopJniStripMode::Explicit
    } else {
        DesktopJniStripMode::Disabled
    }
}

#[cfg(test)]
fn validate_desktop_jni_symbol_stripping(
    config: &Config,
    host_target: JavaHostTarget,
) -> Result<()> {
    validate_desktop_jni_symbol_stripping_for(config.java_jvm_strip_symbols(), host_target)
}

pub(crate) fn validate_desktop_jni_symbol_stripping_for(
    configured_strip_symbols: bool,
    host_target: JavaHostTarget,
) -> Result<()> {
    if configured_strip_symbols && matches!(host_target, JavaHostTarget::WindowsX86_64) {
        return Err(CliError::CommandFailed {
            command: "targets.java.jvm.strip_symbols is not supported for windows-x86_64 yet"
                .to_string(),
            status: None,
        });
    }

    Ok(())
}

fn strip_packaged_desktop_shared_library(
    strip_mode: DesktopJniStripMode,
    host_target: JavaHostTarget,
    jni_compiler_program: &Path,
    rust_target_triple: &str,
    library_path: &Path,
) -> Result<()> {
    let Some(mut command) = packaged_desktop_shared_library_strip_command(
        strip_mode,
        host_target,
        jni_compiler_program,
        rust_target_triple,
    )?
    else {
        return Ok(());
    };
    command.arg(library_path);

    let status = command.status().map_err(|source| CliError::CommandFailed {
        command: format!(
            "desktop strip tool failed to launch for '{}': {}",
            library_path.display(),
            source
        ),
        status: None,
    })?;

    if !status.success() {
        return Err(CliError::CommandFailed {
            command: format!(
                "desktop strip tool failed for '{}' on '{}'",
                library_path.display(),
                host_target.canonical_name()
            ),
            status: status.code(),
        });
    }

    Ok(())
}

fn packaged_desktop_shared_library_strip_command(
    strip_mode: DesktopJniStripMode,
    host_target: JavaHostTarget,
    jni_compiler_program: &Path,
    rust_target_triple: &str,
) -> Result<Option<Command>> {
    match host_target {
        JavaHostTarget::DarwinArm64 | JavaHostTarget::DarwinX86_64 => {
            let mut command = Command::new("xcrun");
            command.arg("strip").arg("-x");
            Ok(Some(command))
        }
        JavaHostTarget::LinuxX86_64 | JavaHostTarget::LinuxAarch64 => {
            let Some(strip_program) =
                resolve_linux_strip_program(strip_mode, jni_compiler_program, rust_target_triple)?
            else {
                return Ok(None);
            };
            let mut command = Command::new(strip_program);
            command.arg("--strip-all");
            Ok(Some(command))
        }
        JavaHostTarget::Current | JavaHostTarget::WindowsX86_64 => Ok(None),
    }
}

fn resolve_linux_strip_program(
    strip_mode: DesktopJniStripMode,
    jni_compiler_program: &Path,
    rust_target_triple: &str,
) -> Result<Option<PathBuf>> {
    for candidate in linux_strip_program_candidates(jni_compiler_program, rust_target_triple) {
        if let Ok(resolved) = which::which(&candidate) {
            return Ok(Some(resolved));
        }
    }

    let message = format!(
        "no supported Linux strip tool found to strip packaged JVM shared libraries (compiler: '{}')",
        jni_compiler_program.display()
    );

    handle_missing_linux_strip_program(strip_mode, &message)
}

fn handle_missing_linux_strip_program(
    strip_mode: DesktopJniStripMode,
    message: &str,
) -> Result<Option<PathBuf>> {
    match strip_mode {
        DesktopJniStripMode::Explicit => Err(CliError::CommandFailed {
            command: message.to_string(),
            status: None,
        }),
        DesktopJniStripMode::Automatic => {
            print_cargo_line(&format!("warning: {message}; skipping symbol stripping"));
            Ok(None)
        }
        DesktopJniStripMode::Disabled => Ok(None),
    }
}

fn linux_strip_program_candidates(
    jni_compiler_program: &Path,
    rust_target_triple: &str,
) -> Vec<PathBuf> {
    let mut candidates = Vec::new();

    if let Some(directory) = jni_compiler_program.parent() {
        if let Some(prefix) = target_prefixed_binutils_prefix(jni_compiler_program) {
            push_unique_path(&mut candidates, directory.join(format!("{prefix}-strip")));
        }
        if let Some(version_suffix) = compiler_tool_version_suffix(jni_compiler_program) {
            push_unique_path(
                &mut candidates,
                directory.join(format!("llvm-strip-{version_suffix}")),
            );
        }
        push_unique_path(&mut candidates, directory.join("llvm-strip"));
    }

    for candidate in target_prefixed_strip_tool_candidates(rust_target_triple) {
        push_unique_path(&mut candidates, PathBuf::from(candidate));
    }
    if let Some(version_suffix) = compiler_tool_version_suffix(jni_compiler_program) {
        push_unique_path(
            &mut candidates,
            PathBuf::from(format!("llvm-strip-{version_suffix}")),
        );
    }
    push_unique_path(&mut candidates, PathBuf::from("llvm-strip"));

    if cfg!(target_os = "linux") {
        push_unique_path(&mut candidates, PathBuf::from("strip"));
    }

    candidates
}

fn target_prefixed_binutils_prefix(jni_compiler_program: &Path) -> Option<String> {
    let name = jni_compiler_program.file_name()?.to_str()?;
    let name = name.strip_suffix(".exe").unwrap_or(name);
    let mut parts = name.split('-').collect::<Vec<_>>();

    if parts
        .last()
        .is_some_and(|part| is_numeric_tool_suffix(part))
    {
        parts.pop();
    }

    let compiler = parts.pop()?;
    if !matches!(compiler, "clang" | "gcc" | "cc") || parts.is_empty() {
        return None;
    }

    Some(parts.join("-"))
}

fn compiler_tool_version_suffix(jni_compiler_program: &Path) -> Option<String> {
    let name = jni_compiler_program.file_name()?.to_str()?;
    let name = name.strip_suffix(".exe").unwrap_or(name);
    let parts = name.split('-').collect::<Vec<_>>();
    let version_suffix = parts.last()?;

    is_numeric_tool_suffix(version_suffix).then(|| (*version_suffix).to_string())
}

fn target_prefixed_strip_tool_candidates(rust_target_triple: &str) -> Vec<String> {
    let mut candidates = Vec::new();

    if let Some(vendorless_triple) = vendorless_linux_target_triple(rust_target_triple) {
        candidates.push(format!("{vendorless_triple}-strip"));
    }

    candidates.push(format!("{rust_target_triple}-strip"));
    candidates.dedup();
    candidates
}

fn vendorless_linux_target_triple(rust_target_triple: &str) -> Option<String> {
    let parts = rust_target_triple.split('-').collect::<Vec<_>>();
    if parts.len() < 4 {
        return None;
    }

    let vendorless = std::iter::once(parts[0])
        .chain(parts[2..].iter().copied())
        .collect::<Vec<_>>()
        .join("-");

    (vendorless != rust_target_triple && vendorless.contains("linux")).then_some(vendorless)
}

fn is_numeric_tool_suffix(part: &str) -> bool {
    !part.is_empty()
        && part
            .bytes()
            .all(|byte| byte.is_ascii_digit() || byte == b'.')
}

fn push_unique_path(paths: &mut Vec<PathBuf>, candidate: PathBuf) {
    if !paths.contains(&candidate) {
        paths.push(candidate);
    }
}

pub(crate) struct JniLinkerArgs<'a> {
    pub(crate) host_target: JavaHostTarget,
    pub(crate) release: bool,
    pub(crate) output_lib: &'a Path,
    pub(crate) jni_glue: &'a Path,
    pub(crate) link_input: &'a Path,
    pub(crate) jni_dir: &'a Path,
    pub(crate) jni_include_directories: &'a JniIncludeDirectories,
    pub(crate) rustflag_linker_args: &'a [String],
    pub(crate) native_link_search_paths: &'a [String],
    pub(crate) native_static_libraries: &'a [String],
    pub(crate) rpath_flag: Option<&'a str>,
    pub(crate) emit_debug_info: bool,
}

pub(crate) struct NativeLinkMetadata {
    pub(crate) native_static_libraries: Vec<String>,
    pub(crate) native_link_search_paths: Vec<String>,
}

#[derive(Debug, Clone)]
pub(crate) struct JvmPackagedNativeOutput {
    pub(crate) host_target: JavaHostTarget,
    pub(crate) has_shared_library_copy: bool,
    pub(crate) jni_library_path: PathBuf,
    pub(crate) shared_library_path: Option<PathBuf>,
    pub(crate) debug_info_sidecars: Vec<PathBuf>,
}

#[derive(Debug)]
pub(crate) struct JniIncludeDirectories {
    pub(crate) shared: PathBuf,
    pub(crate) platform: PathBuf,
}

pub(crate) enum JvmNativeLinkInput {
    Staticlib(PathBuf),
    Cdylib(PathBuf),
}

impl JvmNativeLinkInput {
    pub(crate) fn path(&self) -> &Path {
        match self {
            Self::Staticlib(path) | Self::Cdylib(path) => path,
        }
    }

    fn links_staticlib(&self) -> bool {
        matches!(self, Self::Staticlib(_))
    }
}

impl JvmBuildArtifacts {
    fn static_library_filename(&self) -> Option<&str> {
        self.static_library_filename.as_deref()
    }
}

pub(crate) fn compile_jni_library(
    config: &Config,
    packaging_target: &JvmPackagingTarget,
    build_artifacts: &JvmBuildArtifacts,
    step: &Step,
) -> Result<JvmPackagedNativeOutput> {
    let layout =
        JvmNativePackageLayout::java(config, &packaging_target.cargo_context.artifact_name);
    compile_jni_library_with_layout(packaging_target, build_artifacts, &layout, step)
}

pub(crate) fn compile_jni_library_with_layout(
    packaging_target: &JvmPackagingTarget,
    build_artifacts: &JvmBuildArtifacts,
    layout: &JvmNativePackageLayout,
    step: &Step,
) -> Result<JvmPackagedNativeOutput> {
    let cargo_context = &packaging_target.cargo_context;
    let host_target = cargo_context.host_target;
    validate_desktop_jni_symbol_stripping_for(layout.strip_symbols, host_target)?;
    let strip_mode = desktop_jni_strip_mode_for(layout.strip_symbols, &cargo_context.build_profile);
    let strip_symbols = !matches!(strip_mode, DesktopJniStripMode::Disabled);
    let jni_dir = &layout.jni_dir;
    let jni_glue = jni_dir.join("jni_glue.c");
    let header = jni_dir.join(format!("{}.h", layout.header_name));

    if !jni_glue.exists() {
        return Err(CliError::FileNotFound(jni_glue));
    }
    if !header.exists() {
        return Err(CliError::FileNotFound(header));
    }

    let artifact_name = &cargo_context.artifact_name;
    let jni_library_name = &layout.jni_library_name;
    let link_input = resolve_jvm_native_link_input(
        &cargo_context.artifact_directory(),
        host_target,
        artifact_name,
        cargo_context.crate_outputs,
        build_artifacts.static_library_filename(),
    )?;
    let compatibility_shared_library = bundled_jvm_shared_library_path(
        &link_input,
        &cargo_context.artifact_directory(),
        host_target,
        artifact_name,
        cargo_context.crate_outputs,
    );

    let host_native_output = layout.native_output_root.join(host_target.canonical_name());
    std::fs::create_dir_all(&host_native_output).map_err(|source| {
        CliError::CreateDirectoryFailed {
            path: host_native_output.clone(),
            source,
        }
    })?;

    let output_lib = host_native_output.join(host_target.jni_library_filename(jni_library_name));
    let jni_include_directories = resolve_jni_include_directories(cargo_context)?;
    let has_shared_library_copy = compatibility_shared_library.is_some();
    let mut debug_info_sidecars = Vec::new();

    let mut command = packaging_target.toolchain.linker_command();
    let jni_linker_args = if packaging_target.toolchain.uses_msvc_compiler() {
        clang_cl_jni_linker_args(&JniLinkerArgs {
            host_target,
            release: strip_symbols,
            output_lib: &output_lib,
            jni_glue: &jni_glue,
            link_input: link_input.path(),
            jni_dir,
            jni_include_directories: &jni_include_directories,
            rustflag_linker_args: packaging_target.toolchain.jni_rustflag_linker_args(),
            native_link_search_paths: &build_artifacts.native_link_search_paths,
            native_static_libraries: &build_artifacts.native_static_libraries,
            rpath_flag: None,
            emit_debug_info: layout.debug_symbols_enabled,
        })?
    } else {
        clang_style_jni_linker_args(&JniLinkerArgs {
            host_target,
            release: strip_symbols,
            output_lib: &output_lib,
            jni_glue: &jni_glue,
            link_input: link_input.path(),
            jni_dir,
            jni_include_directories: &jni_include_directories,
            rustflag_linker_args: packaging_target.toolchain.jni_rustflag_linker_args(),
            native_link_search_paths: &build_artifacts.native_link_search_paths,
            native_static_libraries: &build_artifacts.native_static_libraries,
            rpath_flag: host_target.rpath_flag(),
            emit_debug_info: layout.debug_symbols_enabled,
        })
    };
    command.args(jni_linker_args);

    if step.is_verbose() {
        print_verbose_detail(&format!(
            "JNI rustflag linker args: {:?}",
            packaging_target.toolchain.jni_rustflag_linker_args()
        ));
        print_verbose_detail(&format!(
            "JNI native link search paths: {:?}",
            &build_artifacts.native_link_search_paths
        ));
        print_verbose_detail(&format!(
            "JNI native static libs: {:?}",
            &build_artifacts.native_static_libraries
        ));
        print_verbose_detail(&format!(
            "JNI linker command: {}",
            format_command_for_log(&command)
        ));
    }

    let status = command.status().map_err(|source| CliError::CommandFailed {
        command: format!("desktop linker: {}", source),
        status: None,
    })?;

    if !status.success() {
        return Err(CliError::CommandFailed {
            command: format!(
                "desktop linker failed to compile JNI library for '{}'",
                host_target.canonical_name()
            ),
            status: status.code(),
        });
    }

    let current_host = JavaHostTarget::current();
    if current_host == Some(host_target)
        && let Some(flat_output_root) = &layout.flat_output_root
    {
        let compatibility_jni_copy =
            flat_output_root.join(host_target.jni_library_filename(jni_library_name));
        std::fs::copy(&output_lib, &compatibility_jni_copy).map_err(|source| {
            CliError::CopyFailed {
                from: output_lib.clone(),
                to: compatibility_jni_copy,
                source,
            }
        })?;
    }
    debug_info_sidecars.extend(existing_windows_pdb_sidecars(&output_lib));
    if should_generate_apple_dsym_sidecars(layout.debug_symbols_enabled, &output_lib, host_target) {
        debug_info_sidecars.extend(generate_apple_dsym_sidecars(
            &output_lib,
            host_target,
            step,
        )?);
    }

    let mut shared_library_path = None;
    if let Some(shared_library) = compatibility_shared_library.as_deref() {
        let shared_library_name = shared_library
            .file_name()
            .expect("shared library path should have a file name");
        let structured_copy = host_native_output.join(shared_library_name);
        std::fs::copy(shared_library, &structured_copy).map_err(|source| CliError::CopyFailed {
            from: shared_library.to_path_buf(),
            to: structured_copy.clone(),
            source,
        })?;

        if strip_symbols {
            strip_packaged_desktop_shared_library(
                strip_mode,
                host_target,
                packaging_target.toolchain.jni_compiler_program(),
                packaging_target.toolchain.rust_target_triple(),
                &structured_copy,
            )?;
        }

        if current_host == Some(host_target)
            && let Some(flat_output_root) = &layout.flat_output_root
        {
            let flat_copy = flat_output_root.join(shared_library_name);
            std::fs::copy(&structured_copy, &flat_copy).map_err(|source| CliError::CopyFailed {
                from: structured_copy.clone(),
                to: flat_copy,
                source,
            })?;
        }

        shared_library_path = Some(structured_copy);
        debug_info_sidecars.extend(existing_windows_pdb_sidecars(shared_library));
        let shared_library_path = shared_library_path
            .as_deref()
            .expect("shared library copy should be set");
        if should_generate_apple_dsym_sidecars(
            layout.debug_symbols_enabled,
            shared_library_path,
            host_target,
        ) {
            debug_info_sidecars.extend(generate_apple_dsym_sidecars(
                shared_library_path,
                host_target,
                step,
            )?);
        }
    }

    Ok(JvmPackagedNativeOutput {
        host_target,
        has_shared_library_copy,
        jni_library_path: output_lib,
        shared_library_path,
        debug_info_sidecars,
    })
}

fn existing_windows_pdb_sidecars(path: &Path) -> Vec<PathBuf> {
    let pdb_path = path.with_extension("pdb");
    pdb_path.exists().then_some(pdb_path).into_iter().collect()
}

fn should_generate_apple_dsym_sidecars(
    debug_symbols_enabled: bool,
    path: &Path,
    host_target: JavaHostTarget,
) -> bool {
    debug_symbols_enabled
        && cfg!(target_os = "macos")
        && matches!(
            host_target,
            JavaHostTarget::DarwinArm64 | JavaHostTarget::DarwinX86_64
        )
        && path.extension().and_then(|extension| extension.to_str()) == Some("dylib")
}

fn generate_apple_dsym_sidecars(
    path: &Path,
    host_target: JavaHostTarget,
    step: &Step,
) -> Result<Vec<PathBuf>> {
    if !should_generate_apple_dsym_sidecars(true, path, host_target) {
        return Ok(Vec::new());
    }

    let dsym_path = apple_dsym_bundle_path(path);
    if dsym_path.exists() {
        std::fs::remove_dir_all(&dsym_path).map_err(|source| CliError::WriteFailed {
            path: dsym_path.clone(),
            source,
        })?;
    }

    let mut command = Command::new("dsymutil");
    command.arg(path).arg("-o").arg(&dsym_path);
    if step.is_verbose() {
        print_verbose_detail(&format!(
            "Generating dSYM sidecar: {}",
            format_command_for_log(&command)
        ));
    }

    let status = command.status().map_err(|source| CliError::CommandFailed {
        command: format!("dsymutil: {source}"),
        status: None,
    })?;
    if !status.success() {
        return Err(CliError::CommandFailed {
            command: format!("dsymutil failed for '{}'", path.display()),
            status: status.code(),
        });
    }

    Ok(dsym_path
        .exists()
        .then_some(dsym_path)
        .into_iter()
        .collect())
}

fn apple_dsym_bundle_path(path: &Path) -> PathBuf {
    let file_name = path
        .file_name()
        .expect("dSYM source path should have a file name")
        .to_string_lossy();
    path.parent()
        .expect("dSYM source path should have a parent")
        .join(format!("{file_name}.dSYM"))
}

pub(crate) fn build_jvm_native_library(
    packaging_target: &JvmPackagingTarget,
    release: bool,
    step: &Step,
) -> Result<JvmBuildArtifacts> {
    let cargo_context = &packaging_target.cargo_context;
    let native_static_libraries = Arc::new(Mutex::new(Vec::<String>::new()));
    let captured_static_libraries = Arc::clone(&native_static_libraries);
    let verbose = step.is_verbose();
    let on_output: Option<OutputCallback> = Some(Box::new(move |line: &str| {
        if verbose {
            print_cargo_line(line);
        }

        if let Some(flags) = parse_native_static_libraries(line) {
            let mut libraries = captured_static_libraries
                .lock()
                .expect("native static libraries lock poisoned");
            *libraries = flags;
        }
    }));

    let crate_directory = std::env::current_dir().map_err(|source| CliError::CommandFailed {
        command: format!("current_dir: {source}"),
        status: None,
    })?;
    let mut command = Command::new("cargo");
    command.current_dir(crate_directory);

    if let Some(toolchain_selector) = cargo_context.toolchain_selector.as_deref() {
        command.arg(toolchain_selector);
    }

    command
        .arg("build")
        .arg("--target")
        .arg(&cargo_context.rust_target_triple);
    apply_jvm_cargo_package_selection(&mut command, cargo_context);

    if release {
        command.arg("--release");
    }

    command.args(&cargo_context.cargo_command_args);
    packaging_target
        .toolchain
        .configure_cargo_build(&mut command);

    if !run_command_streaming(&mut command, on_output.as_ref()) {
        return Err(PackError::BuildFailed {
            targets: vec![cargo_context.host_target.canonical_name().to_string()],
        }
        .into());
    }

    let native_static_libraries = native_static_libraries
        .lock()
        .expect("native static libraries lock poisoned")
        .clone();
    let mut native_link_search_paths = Vec::new();

    let native_static_libraries = if native_static_libraries.is_empty() {
        let static_library_filename = if cargo_context.crate_outputs.builds_staticlib {
            resolve_static_library_filename(cargo_context)?
        } else {
            None
        };
        let staticlib_path = static_library_filename
            .as_ref()
            .map(|filename| cargo_context.artifact_directory().join(filename));

        if cargo_context.crate_outputs.builds_staticlib
            && staticlib_path
                .as_ref()
                .is_some_and(|staticlib_path| staticlib_path.exists())
        {
            let link_metadata = query_native_link_metadata(packaging_target, release)?;
            native_link_search_paths = link_metadata.native_link_search_paths;
            link_metadata.native_static_libraries
        } else {
            native_static_libraries
        }
    } else {
        let static_library_filename = if cargo_context.crate_outputs.builds_staticlib {
            resolve_static_library_filename(cargo_context)?
        } else {
            None
        };
        let staticlib_path = static_library_filename
            .as_ref()
            .map(|filename| cargo_context.artifact_directory().join(filename));

        if cargo_context.crate_outputs.builds_staticlib
            && staticlib_path
                .as_ref()
                .is_some_and(|staticlib_path| staticlib_path.exists())
        {
            native_link_search_paths =
                query_native_link_metadata(packaging_target, release)?.native_link_search_paths;
        }

        native_static_libraries
    };

    let static_library_filename = if cargo_context.crate_outputs.builds_staticlib {
        resolve_static_library_filename(cargo_context)?
    } else {
        None
    };

    Ok(JvmBuildArtifacts {
        native_static_libraries,
        native_link_search_paths,
        static_library_filename,
    })
}

pub(crate) fn resolve_jvm_native_link_input(
    artifact_directory: &Path,
    host_target: JavaHostTarget,
    artifact_name: &str,
    crate_outputs: JvmCrateOutputs,
    static_library_filename: Option<&str>,
) -> Result<JvmNativeLinkInput> {
    let staticlib_path = static_library_filename.map(|filename| artifact_directory.join(filename));
    if crate_outputs.builds_staticlib
        && staticlib_path
            .as_ref()
            .is_some_and(|staticlib_path| staticlib_path.exists())
    {
        return Ok(JvmNativeLinkInput::Staticlib(
            staticlib_path.expect("checked staticlib path existence"),
        ));
    }

    let cdylib_path = artifact_directory.join(host_target.shared_library_filename(artifact_name));
    if crate_outputs.builds_cdylib && cdylib_path.exists() {
        return Ok(JvmNativeLinkInput::Cdylib(cdylib_path));
    }

    if crate_outputs.builds_staticlib {
        return Err(CliError::FileNotFound(staticlib_path.unwrap_or_else(
            || artifact_directory.join(host_target.static_library_filename(artifact_name)),
        )));
    }

    if crate_outputs.builds_cdylib {
        return Err(CliError::FileNotFound(cdylib_path));
    }

    Err(CliError::CommandFailed {
        command:
            "the current library target must enable either staticlib or cdylib for JVM packaging"
                .to_string(),
        status: None,
    })
}

pub(crate) fn existing_jvm_shared_library_path(
    artifact_directory: &Path,
    host_target: JavaHostTarget,
    artifact_name: &str,
    crate_outputs: JvmCrateOutputs,
) -> Option<PathBuf> {
    if !crate_outputs.builds_cdylib {
        return None;
    }

    let shared_library_path =
        artifact_directory.join(host_target.shared_library_filename(artifact_name));
    shared_library_path.exists().then_some(shared_library_path)
}

pub(crate) fn bundled_jvm_shared_library_path(
    link_input: &JvmNativeLinkInput,
    artifact_directory: &Path,
    host_target: JavaHostTarget,
    artifact_name: &str,
    crate_outputs: JvmCrateOutputs,
) -> Option<PathBuf> {
    if link_input.links_staticlib() {
        return None;
    }

    existing_jvm_shared_library_path(
        artifact_directory,
        host_target,
        artifact_name,
        crate_outputs,
    )
}

pub(crate) fn parse_native_static_libraries(line: &str) -> Option<Vec<String>> {
    let sanitized = strip_ansi_escape_codes(line);
    let (_, flags) = sanitized.split_once("native-static-libs:")?;
    let parsed: Vec<String> = flags
        .split_whitespace()
        .map(str::to_string)
        .filter(|flag| !flag.is_empty())
        .collect();

    (!parsed.is_empty()).then_some(parsed)
}

pub(crate) fn extract_library_filenames(output: &str) -> Vec<String> {
    output
        .lines()
        .map(str::trim)
        .filter(|line| {
            !line.is_empty()
                && !line.contains(' ')
                && [".a", ".lib", ".dylib", ".so", ".rlib", ".dll"]
                    .iter()
                    .any(|extension| line.ends_with(extension))
        })
        .map(str::to_string)
        .collect()
}

pub(crate) fn select_windows_static_library_filename(
    artifact_name: &str,
    filenames: &[String],
) -> Option<String> {
    let msvc_name = format!("{artifact_name}.lib");
    let gnu_name = format!("lib{artifact_name}.a");

    filenames
        .iter()
        .find(|filename| *filename == &msvc_name || *filename == &gnu_name)
        .cloned()
}

pub(crate) fn extract_native_static_libraries(output: &str) -> Option<Vec<String>> {
    output
        .lines()
        .filter_map(parse_native_static_libraries)
        .next_back()
}

pub(crate) fn extract_link_search_paths(output: &str) -> Vec<String> {
    #[derive(serde::Deserialize)]
    struct BuildScriptExecutedMessage {
        reason: String,
        #[serde(default)]
        linked_paths: Vec<String>,
    }

    let mut linked_paths = Vec::new();

    for line in output
        .lines()
        .map(str::trim)
        .filter(|line| line.starts_with('{'))
    {
        let Ok(message) = serde_json::from_str::<BuildScriptExecutedMessage>(line) else {
            continue;
        };

        if message.reason != "build-script-executed" {
            continue;
        }

        for linked_path in message.linked_paths {
            if !linked_paths.contains(&linked_path) {
                linked_paths.push(linked_path);
            }
        }
    }

    linked_paths
}

pub(crate) fn link_search_path_flags(link_search_paths: &[String]) -> Vec<String> {
    let mut flags = Vec::new();

    for linked_path in link_search_paths {
        let flag = if let Some(path) = linked_path.strip_prefix("framework=") {
            format!("-F{path}")
        } else if let Some(path) = linked_path.strip_prefix("native=") {
            format!("-L{path}")
        } else if let Some(path) = linked_path.strip_prefix("dependency=") {
            format!("-L{path}")
        } else if let Some(path) = linked_path.strip_prefix("all=") {
            format!("-L{path}")
        } else if let Some(path) = linked_path.strip_prefix("crate=") {
            format!("-L{path}")
        } else {
            format!("-L{linked_path}")
        };

        if !flags.contains(&flag) {
            flags.push(flag);
        }
    }

    flags
}

pub(crate) fn clang_native_static_library_flags(
    host_target: JavaHostTarget,
    native_static_libraries: &[String],
) -> Vec<String> {
    let strip_implicit_darwin_libraries = matches!(
        host_target,
        JavaHostTarget::DarwinArm64 | JavaHostTarget::DarwinX86_64
    );
    let mut flags = Vec::new();
    let mut index = 0;

    while index < native_static_libraries.len() {
        let flag = &native_static_libraries[index];

        if strip_implicit_darwin_libraries && flag == "-l" {
            if let Some(value) = native_static_libraries.get(index + 1)
                && matches!(value.as_str(), "c" | "m" | "System")
            {
                index += 2;
                continue;
            }
        } else if strip_implicit_darwin_libraries
            && matches!(flag.as_str(), "-lc" | "-lm" | "-lSystem")
        {
            index += 1;
            continue;
        }

        flags.push(flag.clone());
        index += 1;
    }

    flags
}

pub(crate) fn clang_style_jni_linker_args(args: &JniLinkerArgs<'_>) -> Vec<String> {
    let mut resolved_args = vec!["-shared".to_string(), "-fPIC".to_string()];
    if args.emit_debug_info {
        resolved_args.push("-g".to_string());
    }
    resolved_args.extend([
        "-o".to_string(),
        args.output_lib.display().to_string(),
        args.jni_glue.display().to_string(),
        args.link_input.display().to_string(),
        format!("-I{}", args.jni_dir.display()),
        format!("-I{}", args.jni_include_directories.shared.display()),
        format!("-I{}", args.jni_include_directories.platform.display()),
    ]);
    resolved_args.extend(args.rustflag_linker_args.iter().cloned());
    resolved_args.extend(link_search_path_flags(args.native_link_search_paths));
    resolved_args.extend(clang_native_static_library_flags(
        args.host_target,
        args.native_static_libraries,
    ));
    resolved_args.extend(clang_release_optimization_flags(
        args.host_target,
        args.release,
    ));
    if let Some(rpath_flag) = args.rpath_flag {
        resolved_args.push(rpath_flag.to_string());
    }
    resolved_args
}

pub(crate) fn clang_release_optimization_flags(
    host_target: JavaHostTarget,
    release: bool,
) -> Vec<String> {
    if !release {
        return Vec::new();
    }

    match host_target {
        JavaHostTarget::DarwinArm64 | JavaHostTarget::DarwinX86_64 => vec!["-Wl,-x".to_string()],
        JavaHostTarget::LinuxX86_64 | JavaHostTarget::LinuxAarch64 => {
            vec!["-Wl,--strip-all".to_string()]
        }
        JavaHostTarget::Current | JavaHostTarget::WindowsX86_64 => Vec::new(),
    }
}

pub(crate) fn clang_cl_jni_linker_args(args: &JniLinkerArgs<'_>) -> Result<Vec<String>> {
    let mut resolved_args = vec!["/LD".to_string()];
    if args.emit_debug_info {
        resolved_args.push("/Z7".to_string());
    }
    resolved_args.extend([
        args.jni_glue.display().to_string(),
        args.link_input.display().to_string(),
        format!("/I{}", args.jni_dir.display()),
        format!("/I{}", args.jni_include_directories.shared.display()),
        format!("/I{}", args.jni_include_directories.platform.display()),
        "/link".to_string(),
        format!("/OUT:{}", args.output_lib.display()),
    ]);
    if args.emit_debug_info {
        resolved_args.push("/DEBUG".to_string());
    }
    resolved_args.extend(msvc_rustflag_linker_args(args.rustflag_linker_args)?);
    resolved_args.extend(msvc_link_search_path_flags(args.native_link_search_paths));
    resolved_args.extend(msvc_native_static_library_flags(
        args.native_static_libraries,
    ));
    Ok(resolved_args)
}

pub(crate) fn msvc_link_search_path_flags(link_search_paths: &[String]) -> Vec<String> {
    let mut flags = Vec::new();

    for linked_path in link_search_paths {
        let Some(path) = linked_path
            .strip_prefix("native=")
            .or_else(|| linked_path.strip_prefix("dependency="))
            .or_else(|| linked_path.strip_prefix("all="))
            .or_else(|| linked_path.strip_prefix("crate="))
            .or_else(|| (!linked_path.starts_with("framework=")).then_some(linked_path.as_str()))
        else {
            continue;
        };

        let flag = format!("/LIBPATH:{path}");
        if !flags.contains(&flag) {
            flags.push(flag);
        }
    }

    flags
}

pub(crate) fn msvc_native_static_library_flags(native_static_libraries: &[String]) -> Vec<String> {
    let mut flags = Vec::new();
    let mut index = 0;

    while index < native_static_libraries.len() {
        let flag = &native_static_libraries[index];

        if flag == "-l" {
            if let Some(value) = native_static_libraries.get(index + 1) {
                flags.push(format!("{value}.lib"));
                index += 2;
                continue;
            }
        } else if let Some(value) = flag.strip_prefix("-l:") {
            flags.push(value.to_string());
            index += 1;
            continue;
        } else if let Some(value) = flag.strip_prefix("-l") {
            if !value.is_empty() {
                flags.push(format!("{value}.lib"));
                index += 1;
                continue;
            }
        } else if flag == "-framework" {
            index += 2;
            continue;
        } else {
            flags.push(flag.clone());
            index += 1;
            continue;
        }

        index += 1;
    }

    flags
}

pub(crate) fn msvc_rustflag_linker_args(rustflag_linker_args: &[String]) -> Result<Vec<String>> {
    let mut flags = Vec::new();
    let mut index = 0;

    while index < rustflag_linker_args.len() {
        let flag = &rustflag_linker_args[index];

        if flag == "-L" {
            if let Some(value) = rustflag_linker_args.get(index + 1) {
                flags.push(format!("/LIBPATH:{value}"));
                index += 2;
                continue;
            }
        } else if let Some(value) = flag.strip_prefix("-L") {
            if !value.is_empty() {
                flags.push(format!("/LIBPATH:{value}"));
                index += 1;
                continue;
            }
        } else if flag == "-l" {
            if let Some(value) = rustflag_linker_args.get(index + 1) {
                flags.push(format!("{value}.lib"));
                index += 2;
                continue;
            }
        } else if let Some(value) = flag.strip_prefix("-l:") {
            flags.push(value.to_string());
            index += 1;
            continue;
        } else if let Some(value) = flag.strip_prefix("-l") {
            if !value.is_empty() {
                flags.push(format!("{value}.lib"));
                index += 1;
                continue;
            }
        } else if flag == "-framework" {
            index += 2;
            continue;
        } else if flag.starts_with("-F") {
            return Err(CliError::CommandFailed {
                command: format!(
                    "unsupported Windows MSVC JNI linker arg '{}' derived from Cargo rustflags",
                    flag
                ),
                status: None,
            });
        } else if flag.starts_with('/') || flag.ends_with(".lib") || flag.ends_with(".a") {
            flags.push(flag.clone());
            index += 1;
            continue;
        } else {
            return Err(CliError::CommandFailed {
                command: format!(
                    "unsupported Windows MSVC JNI linker arg '{}' derived from Cargo rustflags",
                    flag
                ),
                status: None,
            });
        }

        index += 1;
    }

    Ok(flags)
}

pub(crate) fn resolve_jni_include_directories(
    cargo_context: &JvmCargoContext,
) -> Result<JniIncludeDirectories> {
    let java_home_override_env =
        target_specific_java_home_env_key(&cargo_context.rust_target_triple);
    let include_override_env =
        target_specific_java_include_env_key(&cargo_context.rust_target_triple);
    resolve_jni_include_directories_with_overrides(
        cargo_context,
        std::env::var_os("JAVA_HOME").map(PathBuf::from),
        std::env::var_os(&java_home_override_env).map(PathBuf::from),
        std::env::var_os(&include_override_env).map(PathBuf::from),
    )
}

pub(crate) fn resolve_jni_include_directories_with_overrides(
    cargo_context: &JvmCargoContext,
    default_java_home: Option<PathBuf>,
    target_java_home_override: Option<PathBuf>,
    target_include_override: Option<PathBuf>,
) -> Result<JniIncludeDirectories> {
    let java_home_override_env =
        target_specific_java_home_env_key(&cargo_context.rust_target_triple);
    let include_override_env =
        target_specific_java_include_env_key(&cargo_context.rust_target_triple);
    let resolved_java_home = target_java_home_override
        .as_deref()
        .and_then(|java_home| resolve_java_home_for_target(java_home, cargo_context.host_target))
        .or_else(|| {
            default_java_home.as_deref().and_then(|java_home| {
                resolve_java_home_for_target(java_home, cargo_context.host_target)
            })
        })
        .or(target_java_home_override.clone())
        .or(default_java_home.clone());

    let platform_include = target_include_override.clone().unwrap_or_else(|| {
        resolved_java_home
            .clone()
            .map(|java_home| {
                java_home
                    .join("include")
                    .join(cargo_context.host_target.jni_platform())
            })
            .unwrap_or_default()
    });

    let shared_include = target_include_override
        .as_ref()
        .and_then(|platform_include| platform_include.parent().map(Path::to_path_buf))
        .or_else(|| resolved_java_home.map(|java_home| java_home.join("include")))
        .or_else(|| platform_include.parent().map(Path::to_path_buf))
        .ok_or_else(|| CliError::CommandFailed {
            command: format!(
                "JAVA_HOME not set; for cross-host JVM packaging you can also set {} or {}",
                java_home_override_env, include_override_env
            ),
            status: None,
        })?;

    if !shared_include.exists() {
        return Err(CliError::FileNotFound(shared_include));
    }

    let shared_header = shared_include.join("jni.h");
    if !shared_header.exists() {
        return Err(CliError::FileNotFound(shared_header));
    }

    if !platform_include.exists() {
        return Err(CliError::CommandFailed {
            command: format!(
                "missing JNI platform headers for '{}' at '{}'; set {} to a directory containing jni_md.h or set {} to a target-specific JDK home",
                cargo_context.host_target.canonical_name(),
                platform_include.display(),
                include_override_env,
                java_home_override_env
            ),
            status: None,
        });
    }

    let platform_header = platform_include.join("jni_md.h");
    if !platform_header.exists() {
        return Err(CliError::FileNotFound(platform_header));
    }

    Ok(JniIncludeDirectories {
        shared: shared_include,
        platform: platform_include,
    })
}

fn resolve_java_home_for_target(java_home: &Path, host_target: JavaHostTarget) -> Option<PathBuf> {
    candidate_java_home_roots(java_home).find(|candidate| {
        let include_root = candidate.join("include");
        include_root.join("jni.h").exists()
            && include_root
                .join(host_target.jni_platform())
                .join("jni_md.h")
                .exists()
    })
}

fn candidate_java_home_roots(java_home: &Path) -> impl Iterator<Item = PathBuf> {
    let java_home = java_home.to_path_buf();
    let macos_bundle_home = java_home.join("Contents").join("Home");
    let homebrew_bundle_home = java_home
        .join("libexec")
        .join("openjdk.jdk")
        .join("Contents")
        .join("Home");

    [
        java_home.clone(),
        macos_bundle_home.clone(),
        homebrew_bundle_home,
        java_home.join("Home"),
        macos_bundle_home.join("Home"),
    ]
    .into_iter()
    .scan(Vec::<PathBuf>::new(), |seen, candidate| {
        if seen.iter().any(|existing| existing == &candidate) {
            Some(None)
        } else {
            seen.push(candidate.clone());
            Some(Some(candidate))
        }
    })
    .flatten()
}

pub(crate) fn target_specific_java_home_env_key(rust_target_triple: &str) -> String {
    format!(
        "BOLTFFI_JAVA_HOME_{}",
        rust_target_triple.replace('-', "_").to_uppercase()
    )
}

pub(crate) fn target_specific_java_include_env_key(rust_target_triple: &str) -> String {
    format!(
        "BOLTFFI_JAVA_INCLUDE_{}",
        rust_target_triple.replace('-', "_").to_uppercase()
    )
}

pub(crate) fn query_native_link_metadata(
    packaging_target: &JvmPackagingTarget,
    release: bool,
) -> Result<NativeLinkMetadata> {
    let cargo_context = &packaging_target.cargo_context;
    let crate_directory = std::env::current_dir().map_err(|source| CliError::CommandFailed {
        command: format!("current_dir: {source}"),
        status: None,
    })?;

    let mut command = Command::new("cargo");
    command.current_dir(crate_directory);

    if let Some(toolchain_selector) = cargo_context.toolchain_selector.as_deref() {
        command.arg(toolchain_selector);
    }

    command
        .arg("rustc")
        .arg("--target")
        .arg(&cargo_context.rust_target_triple);
    apply_jvm_cargo_package_selection(&mut command, cargo_context);

    if release {
        command.arg("--release");
    }

    command
        .args(&cargo_context.cargo_command_args)
        .arg("--message-format=json-render-diagnostics")
        .arg("--lib")
        .arg("--")
        .arg("--print=native-static-libs");
    packaging_target
        .toolchain
        .configure_cargo_build(&mut command);

    let output = command.output().map_err(|source| CliError::CommandFailed {
        command: format!("cargo rustc --print=native-static-libs: {source}"),
        status: None,
    })?;

    if !output.status.success() {
        return Err(CliError::CommandFailed {
            command: "cargo rustc --print=native-static-libs".to_string(),
            status: output.status.code(),
        });
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);
    let combined = format!("{stdout}\n{stderr}");
    let native_link_search_paths = extract_link_search_paths(&stdout);
    let native_static_libraries =
        extract_native_static_libraries(&combined).ok_or_else(|| CliError::CommandFailed {
            command: "cargo rustc --print=native-static-libs did not emit link metadata"
                .to_string(),
            status: None,
        })?;

    Ok(NativeLinkMetadata {
        native_static_libraries,
        native_link_search_paths,
    })
}

fn strip_ansi_escape_codes(input: &str) -> String {
    let bytes = input.as_bytes();
    let mut output = String::with_capacity(input.len());
    let mut index = 0;

    while index < bytes.len() {
        if bytes[index] == 0x1b {
            index += 1;

            if index >= bytes.len() {
                break;
            }

            if bytes[index] == b'[' {
                index += 1;
                while index < bytes.len() {
                    let byte = bytes[index];
                    index += 1;
                    if (0x40..=0x7e).contains(&byte) {
                        break;
                    }
                }
                continue;
            }

            continue;
        }

        output.push(bytes[index] as char);
        index += 1;
    }

    output
}

fn resolve_static_library_filename(cargo_context: &JvmCargoContext) -> Result<Option<String>> {
    let artifact_name = &cargo_context.artifact_name;

    if cargo_context.host_target != JavaHostTarget::WindowsX86_64 {
        return Ok(Some(
            cargo_context
                .host_target
                .static_library_filename(artifact_name),
        ));
    }

    let filenames = query_library_filenames(cargo_context)?;
    select_windows_static_library_filename(artifact_name, &filenames)
        .map(Some)
        .ok_or_else(|| CliError::CommandFailed {
            command: format!(
                "cargo rustc --print=file-names did not report a Windows static library for '{}'",
                artifact_name
            ),
            status: None,
        })
}

fn query_library_filenames(cargo_context: &JvmCargoContext) -> Result<Vec<String>> {
    let crate_directory = std::env::current_dir().map_err(|source| CliError::CommandFailed {
        command: format!("current_dir: {source}"),
        status: None,
    })?;

    let mut command = Command::new("cargo");
    command.current_dir(crate_directory);

    if let Some(toolchain_selector) = cargo_context.toolchain_selector.as_deref() {
        command.arg(toolchain_selector);
    }

    command
        .arg("rustc")
        .arg("--target")
        .arg(&cargo_context.rust_target_triple);
    apply_jvm_cargo_package_selection(&mut command, cargo_context);

    if cargo_context.release {
        command.arg("--release");
    }

    command
        .args(&cargo_context.cargo_command_args)
        .arg("--lib")
        .arg("--")
        .arg("--print=file-names");

    let output = command.output().map_err(|source| CliError::CommandFailed {
        command: format!("cargo rustc --print=file-names: {source}"),
        status: None,
    })?;

    if !output.status.success() {
        return Err(CliError::CommandFailed {
            command: "cargo rustc --print=file-names".to_string(),
            status: output.status.code(),
        });
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);
    let combined = format!("{stdout}\n{stderr}");
    let filenames = extract_library_filenames(&combined);

    if filenames.is_empty() {
        return Err(CliError::CommandFailed {
            command: "cargo rustc --print=file-names did not emit any library filenames"
                .to_string(),
            status: None,
        });
    }

    Ok(filenames)
}

fn apply_jvm_cargo_package_selection(command: &mut Command, cargo_context: &JvmCargoContext) {
    command
        .arg("--manifest-path")
        .arg(&cargo_context.cargo_manifest_path);
    if let Some(package_selector) = cargo_context.package_selector.as_deref() {
        command.arg("-p").arg(package_selector);
    }
}

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

    #[cfg(unix)]
    use std::os::unix::fs::PermissionsExt;

    use super::{
        DesktopJniStripMode, JniIncludeDirectories, JniLinkerArgs, apple_dsym_bundle_path,
        bundled_jvm_shared_library_path, clang_cl_jni_linker_args,
        clang_native_static_library_flags, clang_release_optimization_flags,
        clang_style_jni_linker_args, compiler_tool_version_suffix, desktop_jni_strip_mode,
        existing_jvm_shared_library_path, extract_library_filenames, extract_link_search_paths,
        extract_native_static_libraries, handle_missing_linux_strip_program,
        link_search_path_flags, linux_strip_program_candidates, msvc_link_search_path_flags,
        msvc_native_static_library_flags, msvc_rustflag_linker_args, parse_native_static_libraries,
        resolve_jni_include_directories_with_overrides, resolve_jvm_native_link_input,
        resolve_linux_strip_program, select_windows_static_library_filename,
        should_generate_apple_dsym_sidecars, target_prefixed_binutils_prefix,
        target_prefixed_strip_tool_candidates, target_specific_java_home_env_key,
        target_specific_java_include_env_key, validate_desktop_jni_symbol_stripping,
        vendorless_linux_target_triple,
    };
    use crate::build::CargoBuildProfile;
    use crate::cli::CliError;
    use crate::config::{CargoConfig, Config, PackageConfig, TargetsConfig};
    use crate::pack::java::plan::{JvmCargoContext, JvmCrateOutputs};
    use crate::target::JavaHostTarget;

    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 cargo_context(root: &Path, host_target: JavaHostTarget) -> JvmCargoContext {
        JvmCargoContext {
            host_target,
            rust_target_triple: "x86_64-unknown-linux-gnu".to_string(),
            release: false,
            build_profile: CargoBuildProfile::Debug,
            artifact_name: "demo".to_string(),
            cargo_manifest_path: root.join("Cargo.toml"),
            manifest_path: root.join("Cargo.toml"),
            package_selector: None,
            target_directory: root.join("target"),
            cargo_command_args: Vec::new(),
            toolchain_selector: None,
            crate_outputs: JvmCrateOutputs {
                builds_staticlib: true,
                builds_cdylib: false,
            },
        }
    }

    fn config(strip_symbols: bool) -> Config {
        let mut config = Config {
            experimental: Vec::new(),
            cargo: CargoConfig::default(),
            package: PackageConfig {
                name: "demo".to_string(),
                crate_name: None,
                version: None,
                description: None,
                license: None,
                repository: None,
            },
            targets: TargetsConfig::default(),
        };
        config.targets.java.jvm.strip_symbols = strip_symbols;
        config
    }

    #[test]
    fn parses_native_static_library_flags_from_cargo_output() {
        let parsed = parse_native_static_libraries(
            "note: native-static-libs: -framework Security -lresolv -lc++",
        )
        .expect("expected static library flags");

        assert_eq!(parsed, vec!["-framework", "Security", "-lresolv", "-lc++"]);
    }

    #[test]
    fn parses_native_static_library_flags_from_ansi_colored_cargo_output() {
        let parsed =
            parse_native_static_libraries("note: native-static-libs: -lSystem -lc -lm\u{1b}[0m")
                .expect("expected static library flags");

        assert_eq!(parsed, vec!["-lSystem", "-lc", "-lm"]);
    }

    #[test]
    fn preserves_repeated_framework_prefixes_in_native_static_library_flags() {
        let parsed = parse_native_static_libraries(
            "note: native-static-libs: -framework Security -framework SystemConfiguration -lobjc",
        )
        .expect("expected static library flags");

        assert_eq!(
            parsed,
            vec![
                "-framework",
                "Security",
                "-framework",
                "SystemConfiguration",
                "-lobjc",
            ]
        );
    }

    #[test]
    fn apple_dsym_bundle_path_appends_bundle_suffix() {
        let path = PathBuf::from("/tmp/native/libdemo.dylib");

        assert_eq!(
            apple_dsym_bundle_path(&path),
            PathBuf::from("/tmp/native/libdemo.dylib.dSYM")
        );
    }

    #[test]
    fn apple_dsym_generation_requires_debug_symbols_opt_in() {
        let path = PathBuf::from("/tmp/native/libdemo.dylib");

        assert!(!should_generate_apple_dsym_sidecars(
            false,
            &path,
            JavaHostTarget::DarwinArm64
        ));
    }

    #[test]
    fn extracts_last_native_static_library_line_from_combined_output() {
        let parsed = extract_native_static_libraries(
            "Compiling demo\nnote: native-static-libs: -lSystem\nFinished\nnote: native-static-libs: -framework CoreFoundation -lSystem\n",
        )
        .expect("expected static library flags");

        assert_eq!(parsed, vec!["-framework", "CoreFoundation", "-lSystem"]);
    }

    #[test]
    fn extracts_link_search_paths_from_build_script_messages() {
        let linked_paths = extract_link_search_paths(
            r#"{"reason":"compiler-artifact","package_id":"path+file:///tmp/demo#0.1.0"}
{"reason":"build-script-executed","package_id":"path+file:///tmp/dep#0.1.0","linked_paths":["native=/tmp/out","framework=/tmp/frameworks","native=/tmp/out"]}"#,
        );

        assert_eq!(
            linked_paths,
            vec![
                "native=/tmp/out".to_string(),
                "framework=/tmp/frameworks".to_string(),
            ]
        );
    }

    #[test]
    fn converts_link_search_paths_to_clang_flags() {
        let flags = link_search_path_flags(&[
            "native=/tmp/out".to_string(),
            "framework=/tmp/frameworks".to_string(),
            "dependency=/tmp/deps".to_string(),
            "/tmp/plain".to_string(),
            "native=/tmp/out".to_string(),
        ]);

        assert_eq!(
            flags,
            vec![
                "-L/tmp/out".to_string(),
                "-F/tmp/frameworks".to_string(),
                "-L/tmp/deps".to_string(),
                "-L/tmp/plain".to_string(),
            ]
        );
    }

    #[test]
    fn converts_link_search_paths_to_msvc_flags() {
        let flags = msvc_link_search_path_flags(&[
            "native=/tmp/out".to_string(),
            "dependency=/tmp/deps".to_string(),
            "framework=/tmp/frameworks".to_string(),
            "/tmp/plain".to_string(),
            "native=/tmp/out".to_string(),
        ]);

        assert_eq!(
            flags,
            vec![
                "/LIBPATH:/tmp/out".to_string(),
                "/LIBPATH:/tmp/deps".to_string(),
                "/LIBPATH:/tmp/plain".to_string(),
            ]
        );
    }

    #[test]
    fn converts_native_static_libraries_to_msvc_flags() {
        let flags = msvc_native_static_library_flags(&[
            "-l".to_string(),
            "bcrypt".to_string(),
            "-lws2_32".to_string(),
            "-l:custom.lib".to_string(),
            "userenv.lib".to_string(),
            "-framework".to_string(),
            "Security".to_string(),
        ]);

        assert_eq!(
            flags,
            vec![
                "bcrypt.lib".to_string(),
                "ws2_32.lib".to_string(),
                "custom.lib".to_string(),
                "userenv.lib".to_string(),
            ]
        );
    }

    #[test]
    fn strips_implicit_darwin_system_libraries_from_clang_flags() {
        let flags = clang_native_static_library_flags(
            JavaHostTarget::DarwinArm64,
            &[
                "-framework".to_string(),
                "Security".to_string(),
                "-lc".to_string(),
                "-l".to_string(),
                "m".to_string(),
                "-lSystem".to_string(),
                "-liconv".to_string(),
            ],
        );

        assert_eq!(
            flags,
            vec![
                "-framework".to_string(),
                "Security".to_string(),
                "-liconv".to_string(),
            ]
        );
    }

    #[test]
    fn preserves_linux_system_libraries_in_clang_flags() {
        let flags = clang_native_static_library_flags(
            JavaHostTarget::LinuxX86_64,
            &[
                "-ldl".to_string(),
                "-lpthread".to_string(),
                "-lm".to_string(),
            ],
        );

        assert_eq!(
            flags,
            vec![
                "-ldl".to_string(),
                "-lpthread".to_string(),
                "-lm".to_string(),
            ]
        );
    }

    #[test]
    fn converts_msvc_rustflag_linker_args() {
        let flags = msvc_rustflag_linker_args(&[
            "-L/tmp/native".to_string(),
            "-lws2_32".to_string(),
            "userenv.lib".to_string(),
            "/DEBUG".to_string(),
        ])
        .expect("msvc rustflag conversion");

        assert_eq!(
            flags,
            vec![
                "/LIBPATH:/tmp/native".to_string(),
                "ws2_32.lib".to_string(),
                "userenv.lib".to_string(),
                "/DEBUG".to_string(),
            ]
        );
    }

    #[test]
    fn rejects_unsupported_msvc_rustflag_linker_args() {
        let error = msvc_rustflag_linker_args(&["-Wl,--as-needed".to_string()])
            .expect_err("unsupported flag should fail");

        assert!(matches!(
            error,
            CliError::CommandFailed { command, status: None }
                if command.contains("-Wl,--as-needed")
        ));
    }

    #[test]
    fn builds_clang_cl_jni_linker_args_with_msvc_flags() {
        let include_directories = JniIncludeDirectories {
            shared: PathBuf::from("/tmp/jdk/include"),
            platform: PathBuf::from("/tmp/jdk/include/win32"),
        };

        let args = clang_cl_jni_linker_args(&JniLinkerArgs {
            host_target: JavaHostTarget::WindowsX86_64,
            release: true,
            output_lib: Path::new("/tmp/out/demo_jni.dll"),
            jni_glue: Path::new("/tmp/jni/jni_glue.c"),
            link_input: Path::new("/tmp/target/demo.lib"),
            jni_dir: Path::new("/tmp/jni"),
            jni_include_directories: &include_directories,
            rustflag_linker_args: &["-L/tmp/rustflag-native".to_string(), "-luser32".to_string()],
            native_link_search_paths: &["native=/tmp/native".to_string()],
            native_static_libraries: &["-lws2_32".to_string(), "userenv.lib".to_string()],
            rpath_flag: None,
            emit_debug_info: false,
        })
        .expect("msvc jni args");

        assert_eq!(
            args,
            vec![
                "/LD".to_string(),
                "/tmp/jni/jni_glue.c".to_string(),
                "/tmp/target/demo.lib".to_string(),
                "/I/tmp/jni".to_string(),
                "/I/tmp/jdk/include".to_string(),
                "/I/tmp/jdk/include/win32".to_string(),
                "/link".to_string(),
                "/OUT:/tmp/out/demo_jni.dll".to_string(),
                "/LIBPATH:/tmp/rustflag-native".to_string(),
                "user32.lib".to_string(),
                "/LIBPATH:/tmp/native".to_string(),
                "ws2_32.lib".to_string(),
                "userenv.lib".to_string(),
            ]
        );
    }

    #[test]
    fn strips_implicit_darwin_system_libraries_from_clang_jni_args() {
        let include_directories = JniIncludeDirectories {
            shared: PathBuf::from("/tmp/jdk/include"),
            platform: PathBuf::from("/tmp/jdk/include/darwin"),
        };

        let args = clang_style_jni_linker_args(&JniLinkerArgs {
            host_target: JavaHostTarget::DarwinArm64,
            release: false,
            output_lib: Path::new("/tmp/out/libdemo_jni.dylib"),
            jni_glue: Path::new("/tmp/jni/jni_glue.c"),
            link_input: Path::new("/tmp/target/libdemo.a"),
            jni_dir: Path::new("/tmp/jni"),
            jni_include_directories: &include_directories,
            rustflag_linker_args: &[],
            native_link_search_paths: &[],
            native_static_libraries: &[
                "-framework".to_string(),
                "Security".to_string(),
                "-lc".to_string(),
                "-lm".to_string(),
                "-lSystem".to_string(),
                "-liconv".to_string(),
            ],
            rpath_flag: Some("-Wl,-rpath,@loader_path"),
            emit_debug_info: false,
        });

        assert_eq!(
            args,
            vec![
                "-shared".to_string(),
                "-fPIC".to_string(),
                "-o".to_string(),
                "/tmp/out/libdemo_jni.dylib".to_string(),
                "/tmp/jni/jni_glue.c".to_string(),
                "/tmp/target/libdemo.a".to_string(),
                "-I/tmp/jni".to_string(),
                "-I/tmp/jdk/include".to_string(),
                "-I/tmp/jdk/include/darwin".to_string(),
                "-framework".to_string(),
                "Security".to_string(),
                "-liconv".to_string(),
                "-Wl,-rpath,@loader_path".to_string(),
            ]
        );
    }

    #[test]
    fn clang_style_jni_linker_args_include_debug_info_when_requested() {
        let include_directories = JniIncludeDirectories {
            shared: PathBuf::from("/tmp/jdk/include"),
            platform: PathBuf::from("/tmp/jdk/include/linux"),
        };

        let args = clang_style_jni_linker_args(&JniLinkerArgs {
            host_target: JavaHostTarget::LinuxX86_64,
            release: false,
            output_lib: Path::new("/tmp/out/libdemo_jni.so"),
            jni_glue: Path::new("/tmp/jni/jni_glue.c"),
            link_input: Path::new("/tmp/target/libdemo.a"),
            jni_dir: Path::new("/tmp/jni"),
            jni_include_directories: &include_directories,
            rustflag_linker_args: &[],
            native_link_search_paths: &[],
            native_static_libraries: &[],
            rpath_flag: None,
            emit_debug_info: true,
        });

        assert!(args.contains(&"-g".to_string()));
    }

    #[test]
    fn builds_clang_cl_jni_linker_args_with_debug_info_flags() {
        let include_directories = JniIncludeDirectories {
            shared: PathBuf::from("/tmp/jdk/include"),
            platform: PathBuf::from("/tmp/jdk/include/win32"),
        };

        let args = clang_cl_jni_linker_args(&JniLinkerArgs {
            host_target: JavaHostTarget::WindowsX86_64,
            release: false,
            output_lib: Path::new("/tmp/out/demo_jni.dll"),
            jni_glue: Path::new("/tmp/jni/jni_glue.c"),
            link_input: Path::new("/tmp/target/demo.lib"),
            jni_dir: Path::new("/tmp/jni"),
            jni_include_directories: &include_directories,
            rustflag_linker_args: &[],
            native_link_search_paths: &[],
            native_static_libraries: &[],
            rpath_flag: None,
            emit_debug_info: true,
        })
        .expect("msvc jni args with debug info");

        assert!(args.contains(&"/Z7".to_string()));
        assert!(args.contains(&"/DEBUG".to_string()));
    }

    #[test]
    fn adds_darwin_release_jni_strip_flags() {
        let flags = clang_release_optimization_flags(JavaHostTarget::DarwinArm64, true);

        assert_eq!(flags, vec!["-Wl,-x".to_string()]);
    }

    #[test]
    fn adds_linux_release_jni_strip_flags() {
        let flags = clang_release_optimization_flags(JavaHostTarget::LinuxX86_64, true);

        assert_eq!(flags, vec!["-Wl,--strip-all".to_string()]);
    }

    #[test]
    fn extracts_target_prefixed_strip_tool_prefixes() {
        assert_eq!(
            target_prefixed_binutils_prefix(Path::new("/tmp/x86_64-linux-gnu-clang")),
            Some("x86_64-linux-gnu".to_string())
        );
        assert_eq!(
            target_prefixed_binutils_prefix(Path::new("/tmp/aarch64-linux-gnu-gcc")),
            Some("aarch64-linux-gnu".to_string())
        );
        assert_eq!(
            target_prefixed_binutils_prefix(Path::new("/tmp/x86_64-linux-gnu-clang-18")),
            Some("x86_64-linux-gnu".to_string())
        );
        assert_eq!(
            target_prefixed_binutils_prefix(Path::new("/tmp/aarch64-linux-gnu-gcc-14.2")),
            Some("aarch64-linux-gnu".to_string())
        );
        assert_eq!(
            target_prefixed_binutils_prefix(Path::new("/tmp/clang")),
            None
        );
    }

    #[test]
    fn extracts_compiler_tool_version_suffixes() {
        assert_eq!(
            compiler_tool_version_suffix(Path::new("/tmp/clang-18")),
            Some("18".to_string())
        );
        assert_eq!(
            compiler_tool_version_suffix(Path::new("/tmp/x86_64-linux-gnu-clang-18")),
            Some("18".to_string())
        );
        assert_eq!(
            compiler_tool_version_suffix(Path::new("/tmp/aarch64-linux-gnu-gcc-14.2")),
            Some("14.2".to_string())
        );
        assert_eq!(compiler_tool_version_suffix(Path::new("/tmp/clang")), None);
    }

    #[test]
    fn linux_strip_program_candidates_include_sibling_tools() {
        let candidates = linux_strip_program_candidates(
            Path::new("/tmp/toolchain/bin/x86_64-linux-gnu-clang"),
            "x86_64-unknown-linux-gnu",
        );

        assert_eq!(
            candidates[0],
            PathBuf::from("/tmp/toolchain/bin/x86_64-linux-gnu-strip")
        );
        assert_eq!(
            candidates[1],
            PathBuf::from("/tmp/toolchain/bin/llvm-strip")
        );
        assert_eq!(candidates[2], PathBuf::from("x86_64-linux-gnu-strip"));
    }

    #[test]
    fn linux_strip_program_candidates_include_versioned_llvm_strip_tools() {
        let candidates = linux_strip_program_candidates(
            Path::new("/tmp/toolchain/bin/clang-18"),
            "x86_64-unknown-linux-gnu",
        );

        assert_eq!(
            candidates[0],
            PathBuf::from("/tmp/toolchain/bin/llvm-strip-18")
        );
        assert_eq!(
            candidates[1],
            PathBuf::from("/tmp/toolchain/bin/llvm-strip")
        );
        assert_eq!(candidates[2], PathBuf::from("x86_64-linux-gnu-strip"));
        assert_eq!(candidates[4], PathBuf::from("llvm-strip-18"));
    }

    #[test]
    fn resolves_linux_strip_program_from_sibling_target_prefixed_tool() {
        let temp_root = temporary_directory("boltffi-linux-strip-test");
        let bin_dir = temp_root.join("bin");
        fs::create_dir_all(&bin_dir).expect("create bin dir");
        let compiler = bin_dir.join("x86_64-linux-gnu-clang");
        let strip_tool = bin_dir.join("x86_64-linux-gnu-strip");
        fs::write(&compiler, []).expect("write fake compiler");
        fs::write(&strip_tool, []).expect("write fake strip tool");
        #[cfg(unix)]
        {
            fs::set_permissions(&strip_tool, fs::Permissions::from_mode(0o755))
                .expect("mark fake strip executable");
        }

        let resolved = resolve_linux_strip_program(
            DesktopJniStripMode::Explicit,
            &compiler,
            "x86_64-unknown-linux-gnu",
        )
        .expect("resolve sibling strip tool")
        .expect("strip tool should be present");

        assert_eq!(resolved, strip_tool);

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

    #[test]
    fn resolves_linux_strip_program_from_sibling_versioned_llvm_strip_tool() {
        let temp_root = temporary_directory("boltffi-linux-llvm-strip-test");
        let bin_dir = temp_root.join("bin");
        fs::create_dir_all(&bin_dir).expect("create bin dir");
        let compiler = bin_dir.join("clang-18");
        let strip_tool = bin_dir.join("llvm-strip-18");
        fs::write(&compiler, []).expect("write fake compiler");
        fs::write(&strip_tool, []).expect("write fake strip tool");
        #[cfg(unix)]
        {
            fs::set_permissions(&strip_tool, fs::Permissions::from_mode(0o755))
                .expect("mark fake strip executable");
        }

        let resolved = resolve_linux_strip_program(
            DesktopJniStripMode::Explicit,
            &compiler,
            "x86_64-unknown-linux-gnu",
        )
        .expect("resolve sibling versioned llvm-strip tool")
        .expect("strip tool should be present");

        assert_eq!(resolved, strip_tool);

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

    #[test]
    fn skips_missing_linux_strip_program_for_automatic_release_stripping() {
        let resolved = handle_missing_linux_strip_program(
            DesktopJniStripMode::Automatic,
            "no supported Linux strip tool found",
        )
        .expect("automatic stripping should not fail without a strip tool");

        assert!(resolved.is_none());
    }

    #[test]
    fn rejects_missing_linux_strip_program_for_explicit_stripping() {
        let error = handle_missing_linux_strip_program(
            DesktopJniStripMode::Explicit,
            "no supported Linux strip tool found",
        )
        .expect_err("explicit stripping should fail without a strip tool");

        assert!(matches!(
            error,
            CliError::CommandFailed { command, status: None }
                if command.contains("no supported Linux strip tool found")
        ));
    }

    #[test]
    fn derives_target_prefixed_strip_tool_candidates_from_linux_target_triple() {
        assert_eq!(
            target_prefixed_strip_tool_candidates("x86_64-unknown-linux-gnu"),
            vec![
                "x86_64-linux-gnu-strip".to_string(),
                "x86_64-unknown-linux-gnu-strip".to_string(),
            ]
        );
        assert_eq!(
            target_prefixed_strip_tool_candidates("aarch64-unknown-linux-gnu"),
            vec![
                "aarch64-linux-gnu-strip".to_string(),
                "aarch64-unknown-linux-gnu-strip".to_string(),
            ]
        );
        assert_eq!(
            target_prefixed_strip_tool_candidates("x86_64-alpine-linux-musl"),
            vec![
                "x86_64-linux-musl-strip".to_string(),
                "x86_64-alpine-linux-musl-strip".to_string(),
            ]
        );
    }

    #[test]
    fn derives_vendorless_linux_target_triples() {
        assert_eq!(
            vendorless_linux_target_triple("x86_64-unknown-linux-gnu"),
            Some("x86_64-linux-gnu".to_string())
        );
        assert_eq!(
            vendorless_linux_target_triple("aarch64-alpine-linux-musl"),
            Some("aarch64-linux-musl".to_string())
        );
        assert_eq!(vendorless_linux_target_triple("x86_64-linux-musl"), None);
    }

    #[test]
    fn skips_release_jni_optimization_flags_for_debug_builds() {
        let flags = clang_release_optimization_flags(JavaHostTarget::DarwinArm64, false);

        assert!(flags.is_empty());
    }

    #[test]
    fn strips_desktop_jni_symbols_for_release_and_opted_in_named_profiles() {
        let default_config = config(false);
        let opted_in_config = config(true);

        assert!(!matches!(
            desktop_jni_strip_mode(&default_config, &CargoBuildProfile::Release),
            DesktopJniStripMode::Disabled
        ));
        assert!(matches!(
            desktop_jni_strip_mode(&default_config, &CargoBuildProfile::Debug),
            DesktopJniStripMode::Disabled
        ));
        assert!(matches!(
            desktop_jni_strip_mode(
                &default_config,
                &CargoBuildProfile::Named("asan".to_string())
            ),
            DesktopJniStripMode::Disabled
        ));
        assert!(!matches!(
            desktop_jni_strip_mode(
                &opted_in_config,
                &CargoBuildProfile::Named("dist".to_string())
            ),
            DesktopJniStripMode::Disabled
        ));
    }

    #[test]
    fn distinguishes_automatic_and_explicit_desktop_jni_strip_modes() {
        assert_eq!(
            desktop_jni_strip_mode(&config(false), &CargoBuildProfile::Release),
            DesktopJniStripMode::Automatic
        );
        assert_eq!(
            desktop_jni_strip_mode(&config(true), &CargoBuildProfile::Release),
            DesktopJniStripMode::Automatic
        );
        assert_eq!(
            desktop_jni_strip_mode(&config(true), &CargoBuildProfile::Named("dist".to_string())),
            DesktopJniStripMode::Explicit
        );
        assert_eq!(
            desktop_jni_strip_mode(&config(false), &CargoBuildProfile::Debug),
            DesktopJniStripMode::Disabled
        );
    }

    #[test]
    fn rejects_explicit_windows_symbol_stripping_opt_in() {
        let error =
            validate_desktop_jni_symbol_stripping(&config(true), JavaHostTarget::WindowsX86_64)
                .expect_err("windows strip opt-in should fail");

        assert!(matches!(
            error,
            CliError::CommandFailed { command, status: None }
                if command.contains("strip_symbols")
                    && command.contains("windows-x86_64")
        ));
    }

    #[test]
    fn allows_automatic_windows_release_behavior_without_strip_opt_in() {
        validate_desktop_jni_symbol_stripping(&config(false), JavaHostTarget::WindowsX86_64)
            .expect("windows release packaging should remain unchanged without explicit opt-in");
    }

    #[test]
    fn extracts_library_filenames_from_print_file_names_output() {
        let filenames = extract_library_filenames(
            "Compiling demo\nlibdemo.a\nlibdemo.dylib\nlibdemo.rlib\nFinished\n",
        );

        assert_eq!(
            filenames,
            vec![
                "libdemo.a".to_string(),
                "libdemo.dylib".to_string(),
                "libdemo.rlib".to_string(),
            ]
        );
    }

    #[test]
    fn selects_windows_static_library_filename_from_reported_outputs() {
        let filename = select_windows_static_library_filename(
            "demo",
            &[
                "demo.lib".to_string(),
                "demo.dll".to_string(),
                "demo.rlib".to_string(),
            ],
        )
        .expect("expected windows staticlib filename");

        assert_eq!(filename, "demo.lib");
    }

    #[test]
    fn selects_windows_gnu_static_library_filename_from_reported_outputs() {
        let filename = select_windows_static_library_filename(
            "demo",
            &[
                "libdemo.a".to_string(),
                "demo.dll".to_string(),
                "demo.rlib".to_string(),
            ],
        )
        .expect("expected windows gnu staticlib filename");

        assert_eq!(filename, "libdemo.a");
    }

    #[test]
    fn builds_target_specific_java_env_keys() {
        assert_eq!(
            target_specific_java_home_env_key("x86_64-unknown-linux-gnu"),
            "BOLTFFI_JAVA_HOME_X86_64_UNKNOWN_LINUX_GNU"
        );
        assert_eq!(
            target_specific_java_include_env_key("x86_64-unknown-linux-gnu"),
            "BOLTFFI_JAVA_INCLUDE_X86_64_UNKNOWN_LINUX_GNU"
        );
    }

    #[test]
    fn rejects_missing_cross_host_jni_headers_during_validation() {
        let temp_root = temporary_directory("boltffi-java-headers-test");
        let java_home = temp_root.join("linux-jdk");
        let shared_include = java_home.join("include");
        fs::create_dir_all(&shared_include).expect("create shared include dir");
        fs::write(shared_include.join("jni.h"), []).expect("write jni.h");

        let error = resolve_jni_include_directories_with_overrides(
            &cargo_context(&temp_root, JavaHostTarget::LinuxX86_64),
            Some(java_home),
            None,
            None,
        )
        .expect_err("expected missing target headers error");

        assert!(matches!(
            error,
            CliError::CommandFailed { command, status: None }
                if command.contains("BOLTFFI_JAVA_INCLUDE_X86_64_UNKNOWN_LINUX_GNU")
                    && command.contains("BOLTFFI_JAVA_HOME_X86_64_UNKNOWN_LINUX_GNU")
        ));

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

    #[test]
    fn rejects_missing_jni_header_files_during_validation() {
        let temp_root = temporary_directory("boltffi-java-header-files-test");
        let shared_include = temp_root.join("include");
        let platform_include = shared_include.join("linux");
        fs::create_dir_all(&platform_include).expect("create include dirs");

        let error = resolve_jni_include_directories_with_overrides(
            &cargo_context(&temp_root, JavaHostTarget::LinuxX86_64),
            None,
            None,
            Some(platform_include),
        )
        .expect_err("expected missing header file error");

        assert!(matches!(error, CliError::FileNotFound(path) if path.ends_with("jni.h")));

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

    #[test]
    fn accepts_target_include_override_without_java_home() {
        let temp_root = temporary_directory("boltffi-java-include-only-test");
        let shared_include = temp_root.join("include");
        let platform_include = shared_include.join("linux");
        fs::create_dir_all(&platform_include).expect("create platform include dir");
        fs::write(shared_include.join("jni.h"), []).expect("write jni.h");
        fs::write(platform_include.join("jni_md.h"), []).expect("write jni_md.h");

        let include_directories = resolve_jni_include_directories_with_overrides(
            &cargo_context(&temp_root, JavaHostTarget::LinuxX86_64),
            None,
            None,
            Some(platform_include.clone()),
        )
        .expect("include override should be sufficient");

        assert_eq!(include_directories.shared, shared_include);
        assert_eq!(include_directories.platform, platform_include);

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

    #[test]
    fn prefers_target_include_override_over_java_home_include() {
        let temp_root = temporary_directory("boltffi-java-include-priority-test");
        let host_java_home = temp_root.join("host-jdk");
        let target_include_root = temp_root.join("target-jdk").join("include");
        let target_platform_include = target_include_root.join("linux");
        fs::create_dir_all(host_java_home.join("include").join("darwin"))
            .expect("create host include dir");
        fs::create_dir_all(&target_platform_include).expect("create target include dir");
        fs::write(host_java_home.join("include").join("jni.h"), []).expect("write host jni.h");
        fs::write(target_include_root.join("jni.h"), []).expect("write target jni.h");
        fs::write(target_platform_include.join("jni_md.h"), []).expect("write target jni_md.h");

        let include_directories = resolve_jni_include_directories_with_overrides(
            &cargo_context(&temp_root, JavaHostTarget::LinuxX86_64),
            Some(host_java_home),
            None,
            Some(target_platform_include.clone()),
        )
        .expect("target include override should take precedence");

        assert_eq!(include_directories.shared, target_include_root);
        assert_eq!(include_directories.platform, target_platform_include);

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

    #[test]
    fn resolves_homebrew_java_home_layout_for_host_headers() {
        let temp_root = temporary_directory("boltffi-java-homebrew-layout-test");
        let homebrew_prefix = temp_root.join("openjdk@17");
        let resolved_home = homebrew_prefix
            .join("libexec")
            .join("openjdk.jdk")
            .join("Contents")
            .join("Home");
        let include_root = resolved_home.join("include");
        let platform_include = include_root.join("darwin");
        fs::create_dir_all(&platform_include).expect("create homebrew include dirs");
        fs::write(include_root.join("jni.h"), []).expect("write jni.h");
        fs::write(platform_include.join("jni_md.h"), []).expect("write jni_md.h");

        let include_directories = resolve_jni_include_directories_with_overrides(
            &cargo_context(&temp_root, JavaHostTarget::DarwinArm64),
            Some(homebrew_prefix),
            None,
            None,
        )
        .expect("homebrew java layout should resolve");

        assert_eq!(include_directories.shared, include_root);
        assert_eq!(include_directories.platform, platform_include);

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

    #[test]
    fn prefers_staticlib_for_jvm_linking_when_available() {
        let temp_root = temporary_directory("boltffi-jvm-link-test");
        let profile_dir = temp_root.join("release");
        fs::create_dir_all(&profile_dir).expect("create profile dir");

        let staticlib = profile_dir.join("libdemo.a");
        let cdylib = profile_dir.join("libdemo.dylib");
        fs::write(&staticlib, []).expect("write staticlib");
        fs::write(&cdylib, []).expect("write cdylib");

        let resolved = resolve_jvm_native_link_input(
            &profile_dir,
            JavaHostTarget::DarwinArm64,
            "demo",
            JvmCrateOutputs {
                builds_staticlib: true,
                builds_cdylib: true,
            },
            Some("libdemo.a"),
        )
        .expect("expected link input");

        assert_eq!(resolved.path(), staticlib.as_path());

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

    #[test]
    fn skips_shared_library_compatibility_copy_when_jni_links_staticlib() {
        let temp_root = temporary_directory("boltffi-jvm-copy-test");
        let profile_dir = temp_root.join("release");
        fs::create_dir_all(&profile_dir).expect("create profile dir");

        let staticlib = profile_dir.join("libdemo.a");
        let cdylib = profile_dir.join("libdemo.dylib");
        fs::write(&staticlib, []).expect("write staticlib");
        fs::write(&cdylib, []).expect("write cdylib");

        let resolved = resolve_jvm_native_link_input(
            &profile_dir,
            JavaHostTarget::DarwinArm64,
            "demo",
            JvmCrateOutputs {
                builds_staticlib: true,
                builds_cdylib: true,
            },
            Some("libdemo.a"),
        )
        .expect("expected link input");
        let compatibility_shared_library = bundled_jvm_shared_library_path(
            &resolved,
            &profile_dir,
            JavaHostTarget::DarwinArm64,
            "demo",
            JvmCrateOutputs {
                builds_staticlib: true,
                builds_cdylib: true,
            },
        );

        assert_eq!(resolved.path(), staticlib.as_path());
        assert!(compatibility_shared_library.is_none());

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

    #[test]
    fn keeps_shared_library_compatibility_copy_when_jni_links_cdylib() {
        let temp_root = temporary_directory("boltffi-jvm-copy-cdylib-test");
        let profile_dir = temp_root.join("release");
        fs::create_dir_all(&profile_dir).expect("create profile dir");

        let cdylib = profile_dir.join("libdemo.dylib");
        fs::write(&cdylib, []).expect("write cdylib");

        let resolved = resolve_jvm_native_link_input(
            &profile_dir,
            JavaHostTarget::DarwinArm64,
            "demo",
            JvmCrateOutputs {
                builds_staticlib: false,
                builds_cdylib: true,
            },
            None,
        )
        .expect("expected link input");
        let compatibility_shared_library = bundled_jvm_shared_library_path(
            &resolved,
            &profile_dir,
            JavaHostTarget::DarwinArm64,
            "demo",
            JvmCrateOutputs {
                builds_staticlib: false,
                builds_cdylib: true,
            },
        )
        .expect("expected shared library compatibility copy");

        assert_eq!(resolved.path(), cdylib.as_path());
        assert_eq!(compatibility_shared_library, cdylib);

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

    #[test]
    fn ignores_stale_staticlib_when_current_crate_is_cdylib_only() {
        let temp_root = temporary_directory("boltffi-jvm-stale-static");
        let profile_dir = temp_root.join("release");
        fs::create_dir_all(&profile_dir).expect("create profile dir");

        let staticlib = profile_dir.join("libdemo.a");
        let cdylib = profile_dir.join("libdemo.dylib");
        fs::write(&staticlib, []).expect("write stale staticlib");
        fs::write(&cdylib, []).expect("write current cdylib");

        let resolved = resolve_jvm_native_link_input(
            &profile_dir,
            JavaHostTarget::DarwinArm64,
            "demo",
            JvmCrateOutputs {
                builds_staticlib: false,
                builds_cdylib: true,
            },
            None,
        )
        .expect("expected link input");

        assert_eq!(resolved.path(), cdylib.as_path());

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

    #[test]
    fn ignores_stale_shared_library_when_current_crate_is_staticlib_only() {
        let temp_root = temporary_directory("boltffi-jvm-stale-cdylib");
        let profile_dir = temp_root.join("release");
        fs::create_dir_all(&profile_dir).expect("create profile dir");

        let cdylib = profile_dir.join("libdemo.dylib");
        fs::write(&cdylib, []).expect("write stale shared library");

        let compatibility_shared_library = existing_jvm_shared_library_path(
            &profile_dir,
            JavaHostTarget::DarwinArm64,
            "demo",
            JvmCrateOutputs {
                builds_staticlib: true,
                builds_cdylib: false,
            },
        );

        assert!(compatibility_shared_library.is_none());

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