use std::{
    collections::HashMap,
    fs,
    path::{Path, PathBuf},
    process::Command,
};

use anyhow::{Context, bail};
use ostool::build::config::Cargo;
pub use ostool::build::config::LogLevel;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize, de::DeserializeOwned};

use crate::{
    context::{ResolvedBuildRequest, workspace_manifest_path, workspace_metadata_root_manifest},
    process::ProcessExt,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AxFeaturePrefixFamily {
    AxStd,
    AxFeat,
}

impl AxFeaturePrefixFamily {
    fn prefix(self) -> &'static str {
        match self {
            Self::AxStd => "ax-std/",
            Self::AxFeat => "ax-feat/",
        }
    }
}

#[derive(Debug, Clone, JsonSchema, Deserialize, Serialize, PartialEq)]
pub struct ArceosBuildInfo {
    /// Environment variables to set during the build.
    pub env: HashMap<String, String>,
    /// Cargo features to enable.
    pub features: Vec<String>,
    /// Log level feature to automatically enable.
    pub log: LogLevel,
    /// Maximum number of CPUs to expose to the build.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub max_cpu_num: Option<usize>,
    /// Whether to use the dynamic platform linker flow when supported.
    #[serde(default, skip_serializing_if = "is_false")]
    pub plat_dyn: bool,
}

impl ArceosBuildInfo {
    pub fn with_features<T: AsRef<str>>(mut self, features: impl AsRef<[T]>) -> Self {
        let features = features
            .as_ref()
            .iter()
            .map(|feature| feature.as_ref().to_string())
            .collect();
        self.features = features;
        self
    }

    pub fn default_for_target(target: &str) -> Self {
        Self {
            plat_dyn: supports_platform_dynamic(target),
            ..Self::default()
        }
    }

    pub(crate) fn effective_plat_dyn(&self, target: &str, plat_dyn_override: Option<bool>) -> bool {
        resolve_effective_plat_dyn(target, self.plat_dyn, plat_dyn_override)
    }

    pub(crate) fn resolve_features(&mut self, package: &str, plat_dyn: bool) {
        self.resolve_features_with_manifest_path(package, plat_dyn, None);
    }

    fn resolve_features_with_manifest_path(
        &mut self,
        package: &str,
        plat_dyn: bool,
        manifest_path: Option<&Path>,
    ) {
        let prefix_family = self.resolve_ax_feature_prefix_family(package, manifest_path);
        let has_myplat = self.features.iter().any(|feature| {
            matches!(
                feature.as_str(),
                "myplat" | "ax-std/myplat" | "ax-feat/myplat"
            )
        });

        self.features.retain(|feature| {
            !matches!(
                feature.as_str(),
                "plat-dyn"
                    | "defplat"
                    | "myplat"
                    | "ax-std/plat-dyn"
                    | "ax-std/defplat"
                    | "ax-std/myplat"
                    | "ax-feat/plat-dyn"
                    | "ax-feat/defplat"
                    | "ax-feat/myplat"
            )
        });

        if plat_dyn {
            self.features
                .push(format!("{}plat-dyn", prefix_family.prefix()));
        } else if has_myplat {
            self.features
                .push(format!("{}myplat", prefix_family.prefix()));
        } else {
            self.features
                .push(format!("{}defplat", prefix_family.prefix()));
        }

        if self.max_cpu_num.is_some_and(|max_cpu_num| max_cpu_num > 1) {
            self.features.push(format!("{}smp", prefix_family.prefix()));
        }

        self.features.sort();
        self.features.dedup();
    }

    fn resolve_ax_feature_prefix_family(
        &self,
        package: &str,
        manifest_path: Option<&Path>,
    ) -> AxFeaturePrefixFamily {
        match detect_ax_feature_prefix_family(package, manifest_path) {
            Ok(prefix_family) => prefix_family,
            Err(err) => {
                if let Some(prefix_family) = feature_family_from_existing_features(&self.features) {
                    return prefix_family;
                }
                warn!(
                    "failed to detect direct ax dependency for package {}: {}, defaulting to \
                     ax-std feature prefix",
                    package, err
                );
                AxFeaturePrefixFamily::AxStd
            }
        }
    }

    fn normalize_legacy_feature_aliases(&mut self) -> bool {
        let mut changed = false;

        for feature in &mut self.features {
            let normalized = normalize_legacy_feature_alias(feature);
            if *feature != normalized {
                *feature = normalized;
                changed = true;
            }
        }

        if changed {
            self.features.sort();
            self.features.dedup();
        }

        changed
    }

    pub(crate) fn prepare_log_env(&mut self) {
        self.env
            .insert("AX_LOG".into(), format!("{:?}", self.log).to_lowercase());
    }

    pub(crate) fn prepare_max_cpu_num_env(&mut self) -> anyhow::Result<()> {
        if let Some(max_cpu_num) = self.validated_max_cpu_num()? {
            self.env.insert("SMP".into(), max_cpu_num.to_string());
        }
        Ok(())
    }

    pub(crate) fn into_base_cargo_config(
        self,
        package: String,
        target: String,
        args: Vec<String>,
    ) -> Cargo {
        let to_bin = default_to_bin_for_target(&target);
        Cargo {
            env: self.env,
            target,
            package,
            features: self.features,
            log: Some(self.log),
            extra_config: None,
            args,
            pre_build_cmds: vec![],
            post_build_cmds: vec![],
            to_bin,
        }
    }

    pub(crate) fn into_base_cargo_config_with_log(
        mut self,
        package: String,
        target: String,
        args: Vec<String>,
    ) -> Cargo {
        self.prepare_log_env();
        self.prepare_max_cpu_num_env()
            .expect("max_cpu_num validation should run before cargo config generation");
        self.into_base_cargo_config(package, target, args)
    }

    pub(crate) fn into_prepared_base_cargo_config(
        mut self,
        package: &str,
        target: &str,
        plat_dyn_override: Option<bool>,
    ) -> anyhow::Result<Cargo> {
        let plat_dyn = self.effective_plat_dyn(target, plat_dyn_override);
        self.validated_max_cpu_num()?;
        self.prepare_non_dynamic_platform_for(package, target, plat_dyn)?;
        self.resolve_features(package, plat_dyn);
        let args = Self::build_cargo_args(target, plat_dyn);

        Ok(self.into_base_cargo_config_with_log(package.to_string(), target.to_string(), args))
    }

    pub(crate) fn prepare_non_dynamic_platform_for(
        &mut self,
        package: &str,
        target: &str,
        plat_dyn: bool,
    ) -> anyhow::Result<()> {
        if plat_dyn {
            return Ok(());
        }

        ensure_arceos_tooling_installed()?;

        let package_manifest = resolve_package_manifest_path(package, None)?;
        let app_dir = package_manifest
            .parent()
            .context("package manifest path has no parent directory")?;
        let platform_package = resolve_platform_package(package, target, &self.features)?;
        let platform_config = resolve_platform_config_path(app_dir, &platform_package)?;
        let platform_name = read_platform_name(&platform_config)
            .unwrap_or_else(|| linker_platform_name(&platform_package).to_string());
        let out_config = app_dir.join(".axconfig.toml");

        generate_axconfig(
            &workspace_root_path()?,
            target,
            &platform_name,
            &platform_config,
            &out_config,
            self.validated_max_cpu_num()?,
        )?;

        self.env.insert(
            "AX_CONFIG_PATH".to_string(),
            out_config.display().to_string(),
        );
        self.env
            .insert("AX_PLATFORM".to_string(), platform_name.to_string());

        Ok(())
    }

    fn validated_max_cpu_num(&self) -> anyhow::Result<Option<usize>> {
        match self.max_cpu_num {
            Some(0) => bail!("max_cpu_num must be greater than 0"),
            Some(max_cpu_num) => Ok(Some(max_cpu_num)),
            None => Ok(None),
        }
    }

    pub(crate) fn build_cargo_args(target: &str, plat_dyn: bool) -> Vec<String> {
        let mut args = Vec::new();
        args.push("--config".to_string());
        args.push(if plat_dyn {
            format!("target.{target}.rustflags=[\"-Clink-arg=-Taxplat.x\"]")
        } else {
            format!(
                "target.{target}.rustflags=[\"-Clink-arg=-Tlinker.x\",\"-Clink-arg=-no-pie\",\"\
                 -Clink-arg=-znostart-stop-gc\"]"
            )
        });
        args
    }

    pub fn into_cargo_config(self, request: &ResolvedBuildRequest) -> anyhow::Result<Cargo> {
        self.into_prepared_base_cargo_config(&request.package, &request.target, request.plat_dyn)
    }
}

impl Default for ArceosBuildInfo {
    fn default() -> Self {
        let mut env = HashMap::new();
        env.insert("AX_IP".to_string(), "10.0.2.15".to_string());
        env.insert("AX_GW".to_string(), "10.0.2.2".to_string());

        Self {
            env,
            log: LogLevel::Warn,
            features: vec!["ax-std".to_string()],
            max_cpu_num: None,
            plat_dyn: false,
        }
    }
}

pub(crate) fn resolve_build_info_path(
    package: &str,
    target: &str,
    explicit_path: Option<PathBuf>,
) -> anyhow::Result<PathBuf> {
    if let Some(path) = explicit_path {
        return Ok(path);
    }

    let package_manifest = resolve_package_manifest_path(package, None)?;
    let app_dir = package_manifest
        .parent()
        .context("package manifest path has no parent directory")?;
    Ok(resolve_build_info_path_in_dir(app_dir, target))
}

pub(crate) fn load_build_info(request: &ResolvedBuildRequest) -> anyhow::Result<ArceosBuildInfo> {
    let mut build_info = load_or_create_build_info(&request.build_info_path, || {
        ArceosBuildInfo::default_for_target(&request.target)
    })?;

    if build_info.normalize_legacy_feature_aliases() {
        warn!(
            "normalizing legacy feature aliases in build config {}",
            request.build_info_path.display()
        );
        fs::write(
            &request.build_info_path,
            toml::to_string_pretty(&build_info)?,
        )
        .with_context(|| {
            format!(
                "failed to rewrite normalized build info {}",
                request.build_info_path.display()
            )
        })?;
    }

    Ok(build_info)
}

pub(crate) fn load_cargo_config(request: &ResolvedBuildRequest) -> anyhow::Result<Cargo> {
    load_build_info(request)?.into_cargo_config(request)
}

pub(crate) fn load_or_create_build_info<T>(
    path: &Path,
    default: impl FnOnce() -> T,
) -> anyhow::Result<T>
where
    T: Serialize + DeserializeOwned,
{
    println!("Using build config: {}", path.display());

    if path.exists() {
        info!("Found build config at {}", path.display());
    } else {
        info!(
            "Build config not found at {}, writing default config",
            path.display()
        );
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }
        let default = default();
        std::fs::write(path, toml::to_string_pretty(&default)?)?;
    }

    toml::from_str::<T>(&std::fs::read_to_string(path)?)
        .with_context(|| format!("failed to parse build info {}", path.display()))
}

fn resolve_effective_plat_dyn(
    target: &str,
    configured_plat_dyn: bool,
    plat_dyn_override: Option<bool>,
) -> bool {
    plat_dyn_override.unwrap_or(configured_plat_dyn) && supports_platform_dynamic(target)
}

fn supports_platform_dynamic(target: &str) -> bool {
    target.starts_with("aarch64-")
}

fn default_to_bin_for_target(target: &str) -> bool {
    !target.starts_with("x86_64-")
}

fn is_false(value: &bool) -> bool {
    !*value
}

fn normalize_legacy_feature_alias(feature: &str) -> String {
    if feature == "axstd" {
        "ax-std".to_string()
    } else if let Some(rest) = feature.strip_prefix("axstd/") {
        format!("ax-std/{rest}")
    } else if feature == "axfeat" {
        "ax-feat".to_string()
    } else if let Some(rest) = feature.strip_prefix("axfeat/") {
        format!("ax-feat/{rest}")
    } else {
        feature.to_string()
    }
}

pub(crate) fn resolve_build_info_path_in_dir(dir: &Path, target: &str) -> PathBuf {
    let bare_path = dir.join(format!("build-{target}.toml"));
    if bare_path.exists() {
        return bare_path;
    }

    let dotted_path = dir.join(format!(".build-{target}.toml"));
    if dotted_path.exists() {
        return dotted_path;
    }

    dotted_path
}

fn feature_family_from_existing_features(features: &[String]) -> Option<AxFeaturePrefixFamily> {
    if features
        .iter()
        .any(|feature| feature.starts_with("ax-std/"))
    {
        return Some(AxFeaturePrefixFamily::AxStd);
    }
    if features
        .iter()
        .any(|feature| feature.starts_with("ax-feat/"))
    {
        return Some(AxFeaturePrefixFamily::AxFeat);
    }
    None
}

fn detect_ax_feature_prefix_family(
    package: &str,
    workspace_manifest: Option<&Path>,
) -> anyhow::Result<AxFeaturePrefixFamily> {
    let manifest_path = workspace_manifest
        .map(Path::to_path_buf)
        .map(Ok)
        .unwrap_or_else(workspace_manifest_path)?;
    let metadata = workspace_metadata_root_manifest(&manifest_path)?;
    let workspace_members: std::collections::HashSet<_> =
        metadata.workspace_members.iter().cloned().collect();
    let package_info = metadata
        .packages
        .iter()
        .find(|pkg| workspace_members.contains(&pkg.id) && pkg.name == package)
        .ok_or_else(|| anyhow::anyhow!("workspace package `{package}` not found"))?;

    let has_axstd = package_info
        .dependencies
        .iter()
        .any(|dep| dep.name == "ax-std" || dep.rename.as_deref() == Some("ax-std"));
    let has_axfeat = package_info
        .dependencies
        .iter()
        .any(|dep| dep.name == "ax-feat" || dep.rename.as_deref() == Some("ax-feat"));

    match (has_axstd, has_axfeat) {
        (true, true) | (true, false) => Ok(AxFeaturePrefixFamily::AxStd),
        (false, true) => Ok(AxFeaturePrefixFamily::AxFeat),
        (false, false) => Err(anyhow::anyhow!(
            "package `{package}` must directly depend on `ax-std` or `ax-feat`"
        )),
    }
}

pub(crate) fn resolve_package_manifest_path(
    package: &str,
    workspace_manifest: Option<&Path>,
) -> anyhow::Result<PathBuf> {
    let manifest_path = workspace_manifest
        .map(Path::to_path_buf)
        .map(Ok)
        .unwrap_or_else(workspace_manifest_path)?;
    let metadata = workspace_metadata_root_manifest(&manifest_path)?;
    let workspace_members: std::collections::HashSet<_> =
        metadata.workspace_members.iter().cloned().collect();
    metadata
        .packages
        .iter()
        .find(|pkg| workspace_members.contains(&pkg.id) && pkg.name == package)
        .map(|pkg| pkg.manifest_path.clone().into_std_path_buf())
        .ok_or_else(|| anyhow::anyhow!("workspace package `{package}` not found"))
}

fn resolve_platform_package(
    package: &str,
    target: &str,
    features: &[String],
) -> anyhow::Result<String> {
    let arch = target_arch_name(target)?;
    let workspace_manifest = workspace_manifest_path()?;
    let metadata = workspace_metadata_root_manifest(&workspace_manifest)?;
    let package_info = metadata
        .packages
        .iter()
        .find(|pkg| metadata.workspace_members.contains(&pkg.id) && pkg.name == package)
        .ok_or_else(|| anyhow!("workspace package `{package}` not found"))?;

    let explicit_platform_features: Vec<_> = features
        .iter()
        .map(|feature| {
            feature
                .strip_prefix("ax-feat/")
                .or_else(|| feature.strip_prefix("ax-std/"))
                .unwrap_or(feature.as_str())
        })
        .filter(|feature| {
            !matches!(
                *feature,
                "ax-std" | "ax-feat" | "plat-dyn" | "defplat" | "myplat"
            )
        })
        .collect();

    if let Some(dep) = package_info.dependencies.iter().find(|dep| {
        (dep.name.starts_with("axplat-") || dep.name.starts_with("ax-plat-"))
            && explicit_platform_features
                .iter()
                .any(|feature| *feature == linker_platform_name(&dep.name))
    }) {
        return Ok(dep.name.clone());
    }

    if features.iter().any(|feature| {
        matches!(
            feature.as_str(),
            "myplat" | "ax-std/myplat" | "ax-feat/myplat"
        )
    }) && let Some(dep) = package_info
        .dependencies
        .iter()
        .find(|dep| dep.name.starts_with(&format!("axplat-{arch}")))
    {
        return Ok(dep.name.clone());
    }

    Ok(default_platform_package(arch).to_string())
}

fn target_arch_name(target: &str) -> anyhow::Result<&'static str> {
    if target.starts_with("aarch64-") {
        Ok("aarch64")
    } else if target.starts_with("x86_64-") {
        Ok("x86_64")
    } else if target.starts_with("riscv64") {
        Ok("riscv64")
    } else if target.starts_with("loongarch64-") {
        Ok("loongarch64")
    } else {
        Err(anyhow!("unsupported target triple `{target}`"))
    }
}

fn default_platform_package(arch: &str) -> &'static str {
    match arch {
        "x86_64" => "ax-plat-x86-pc",
        "aarch64" => "ax-plat-aarch64-qemu-virt",
        "riscv64" => "ax-plat-riscv64-qemu-virt",
        "loongarch64" => "ax-plat-loongarch64-qemu-virt",
        _ => unreachable!("unsupported arch"),
    }
}

fn linker_platform_name(platform_package: &str) -> &str {
    platform_package
        .strip_prefix("axplat-")
        .or_else(|| platform_package.strip_prefix("ax-plat-"))
        .unwrap_or(platform_package)
}

fn resolve_platform_config_path(app_dir: &Path, platform_package: &str) -> anyhow::Result<PathBuf> {
    if let Some(local_path) = find_local_platform_config_path(platform_package)? {
        return Ok(local_path);
    }

    let workspace_root = workspace_root_path()?;
    let root_manifest = workspace_root.join("Cargo.toml");
    let output = Command::new("cargo")
        .arg("axplat")
        .arg("info")
        .arg("--manifest-path")
        .arg(&root_manifest)
        .arg("-C")
        .arg(app_dir)
        .arg("-c")
        .arg(platform_package)
        .exec_capture()
        .with_context(|| format!("failed to run cargo axplat info for `{platform_package}`"))?;

    let config_path = String::from_utf8_lossy(&output.stdout).trim().to_string();
    if config_path.is_empty() {
        bail!(
            "cargo axplat info returned empty config path for package `{}`",
            platform_package
        );
    }

    let config_path = PathBuf::from(config_path);
    if !config_path.exists() {
        bail!(
            "platform config path does not exist: {}",
            config_path.display()
        );
    }

    Ok(config_path)
}

fn find_local_platform_config_path(platform_package: &str) -> anyhow::Result<Option<PathBuf>> {
    let workspace_root = workspace_root_path()?;
    let platform_dir_name = platform_package
        .strip_prefix("ax-plat-")
        .map(|suffix| format!("axplat-{suffix}"))
        .unwrap_or_else(|| platform_package.to_string());
    let candidate = workspace_root
        .join("components/axplat_crates/platforms")
        .join(platform_dir_name)
        .join("axconfig.toml");

    Ok(candidate.exists().then_some(candidate))
}

fn ensure_arceos_tooling_installed() -> anyhow::Result<()> {
    ensure_cargo_axplat_installed()?;
    ensure_ax_config_gen_installed()?;
    Ok(())
}

fn ensure_cargo_axplat_installed() -> anyhow::Result<()> {
    if Command::new("cargo")
        .arg("axplat")
        .arg("--version")
        .exec_capture()
        .is_ok()
    {
        return Ok(());
    }

    warn!("`cargo axplat` not found, installing `cargo-axplat` via cargo");
    Command::new("cargo")
        .arg("install")
        .arg("cargo-axplat")
        .exec()
        .context("failed to install cargo-axplat")?;
    Ok(())
}

fn ensure_ax_config_gen_installed() -> anyhow::Result<()> {
    if Command::new("ax-config-gen")
        .arg("--version")
        .exec_capture()
        .is_ok()
    {
        return Ok(());
    }

    let workspace_root = workspace_root_path()?;
    let ax_config_gen_dir = workspace_root.join("components/axconfig-gen/axconfig-gen");

    warn!(
        "`ax-config-gen` not found, installing from local path {}",
        ax_config_gen_dir.display()
    );
    Command::new("cargo")
        .arg("install")
        .arg("--path")
        .arg(&ax_config_gen_dir)
        .exec()
        .with_context(|| {
            format!(
                "failed to install ax-config-gen from {}",
                ax_config_gen_dir.display()
            )
        })?;
    Ok(())
}

fn read_platform_name(platform_config: &Path) -> Option<String> {
    let contents = fs::read_to_string(platform_config).ok()?;
    let value: toml::Value = toml::from_str(&contents).ok()?;
    value
        .get("platform")
        .and_then(|value| value.as_str())
        .map(|value| value.to_string())
}

fn generate_axconfig(
    workspace_root: &Path,
    target: &str,
    platform_name: &str,
    platform_config: &Path,
    out_config: &Path,
    max_cpu_num: Option<usize>,
) -> anyhow::Result<()> {
    let defconfig = resolve_defconfig_path(workspace_root)?;
    let arch = target_arch_name(target)?;
    let mut command = Command::new("ax-config-gen");
    command
        .arg(defconfig)
        .arg(platform_config)
        .arg("-w")
        .arg(format!("arch=\"{arch}\""))
        .arg("-w")
        .arg(format!("platform=\"{platform_name}\""));
    if let Some(max_cpu_num) = max_cpu_num {
        command
            .arg("-w")
            .arg(format!("plat.max-cpu-num={max_cpu_num}"));
    }
    command
        .arg("-o")
        .arg(out_config)
        .exec()
        .context("failed to run ax-config-gen")?;

    Ok(())
}

fn workspace_root_path() -> anyhow::Result<PathBuf> {
    let root = Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .and_then(Path::parent)
        .context("failed to locate workspace root from axbuild crate")?;
    Ok(root.to_path_buf())
}

fn resolve_defconfig_path(workspace_root: &Path) -> anyhow::Result<PathBuf> {
    let path = workspace_root.join("os/arceos/configs/defconfig.toml");
    if path.exists() {
        Ok(path)
    } else {
        Err(anyhow::anyhow!(
            "defconfig.toml not found at {}",
            path.display()
        ))
    }
}

#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::tempdir;

    use super::*;

    fn request(
        package: &str,
        target: &str,
        plat_dyn: Option<bool>,
        build_info_path: PathBuf,
    ) -> ResolvedBuildRequest {
        ResolvedBuildRequest {
            package: package.to_string(),
            arch: if target.starts_with("x86_64") {
                "x86_64".to_string()
            } else if target.starts_with("aarch64") {
                "aarch64".to_string()
            } else if target.starts_with("riscv64") {
                "riscv64".to_string()
            } else if target.starts_with("loongarch64") {
                "loongarch64".to_string()
            } else {
                "unknown".to_string()
            },
            target: target.to_string(),
            plat_dyn,
            debug: false,
            build_info_path,
            qemu_config: None,
            uboot_config: None,
        }
    }

    fn base_build_info() -> ArceosBuildInfo {
        ArceosBuildInfo::default_for_target("aarch64-unknown-none-softfloat")
    }

    #[test]
    fn resolves_dynamic_platform_features_and_args() {
        let mut build_info = base_build_info();
        build_info.resolve_features("ax-helloworld", true);

        assert!(build_info.features.contains(&"ax-std/plat-dyn".to_string()));
        assert!(!build_info.features.contains(&"ax-std/defplat".to_string()));

        let args = ArceosBuildInfo::build_cargo_args("aarch64-unknown-none-softfloat", true);
        assert!(args.iter().any(|arg| arg.contains("-Taxplat.x")));
    }

    #[test]
    fn resolves_non_dynamic_platform_features_and_args() {
        let mut build_info = base_build_info();
        build_info.resolve_features("ax-helloworld", false);

        assert!(build_info.features.contains(&"ax-std/defplat".to_string()));
        assert!(!build_info.features.contains(&"ax-std/plat-dyn".to_string()));

        let args = ArceosBuildInfo::build_cargo_args("aarch64-unknown-none-softfloat", false);
        assert!(args.iter().any(|arg| arg.contains("-Tlinker.x")));
    }

    #[test]
    fn max_cpu_num_adds_axstd_smp_feature() {
        let mut build_info = ArceosBuildInfo {
            max_cpu_num: Some(4),
            ..ArceosBuildInfo::default()
        };

        build_info.resolve_features("ax-helloworld", false);

        assert!(build_info.features.contains(&"ax-std/smp".to_string()));
    }

    #[test]
    fn preserves_axstd_myplat_for_non_dynamic_platforms() {
        let mut build_info = ArceosBuildInfo {
            features: vec!["ax-std".to_string(), "ax-std/myplat".to_string()],
            ..ArceosBuildInfo::default()
        };
        build_info.resolve_features("ax-helloworld", false);

        assert!(build_info.features.contains(&"ax-std/myplat".to_string()));
        assert!(!build_info.features.contains(&"ax-std/defplat".to_string()));
    }

    #[test]
    fn normalizes_myplat_to_axfeat_when_package_depends_on_axfeat() {
        let workspace = temp_workspace("ax-feat-app", "ax-feat = \"0.1.0\"\n").unwrap();
        let mut build_info = ArceosBuildInfo {
            features: vec!["ax-std/myplat".to_string()],
            ..ArceosBuildInfo::default()
        };

        let family =
            detect_ax_feature_prefix_family("ax-feat-app", Some(&workspace.join("Cargo.toml")))
                .unwrap();
        assert_eq!(family, AxFeaturePrefixFamily::AxFeat);

        build_info.features.retain(|feature| feature != "ax-std");
        build_info.resolve_features_with_manifest_path(
            "ax-feat-app",
            false,
            Some(&workspace.join("Cargo.toml")),
        );

        assert!(build_info.features.contains(&"ax-feat/myplat".to_string()));
        assert!(!build_info.features.contains(&"ax-std/myplat".to_string()));
        assert!(!build_info.features.contains(&"ax-feat/defplat".to_string()));
    }

    #[test]
    fn detects_axfeat_direct_dependency_via_metadata() {
        let workspace = temp_workspace("ax-feat-app", "ax-feat = \"0.1.0\"\n").unwrap();

        let family =
            detect_ax_feature_prefix_family("ax-feat-app", Some(&workspace.join("Cargo.toml")))
                .unwrap();

        assert_eq!(family, AxFeaturePrefixFamily::AxFeat);
    }

    #[test]
    fn max_cpu_num_adds_axfeat_smp_feature() {
        let workspace = temp_workspace("ax-feat-app", "ax-feat = \"0.1.0\"\n").unwrap();
        let mut build_info = ArceosBuildInfo {
            features: vec!["ax-feat/net".to_string()],
            max_cpu_num: Some(4),
            ..ArceosBuildInfo::default()
        };

        build_info.features.retain(|feature| feature != "ax-std");
        build_info.resolve_features_with_manifest_path(
            "ax-feat-app",
            false,
            Some(&workspace.join("Cargo.toml")),
        );

        assert!(build_info.features.contains(&"ax-feat/smp".to_string()));
    }

    #[test]
    fn max_cpu_num_does_not_duplicate_existing_smp_feature() {
        let mut build_info = ArceosBuildInfo {
            features: vec!["ax-std".to_string(), "ax-std/smp".to_string()],
            max_cpu_num: Some(4),
            ..ArceosBuildInfo::default()
        };

        build_info.resolve_features("ax-helloworld", false);

        assert_eq!(
            build_info
                .features
                .iter()
                .filter(|feature| feature.as_str() == "ax-std/smp")
                .count(),
            1
        );
    }

    #[test]
    fn resolve_build_info_path_uses_package_directory() {
        let path = resolve_build_info_path("ax-helloworld", "aarch64-unknown-none-softfloat", None)
            .unwrap();

        assert!(
            path.ends_with(
                "os/arceos/examples/helloworld/.build-aarch64-unknown-none-softfloat.toml"
            )
        );
    }

    #[test]
    fn resolve_build_info_path_prefers_explicit_path() {
        let path = resolve_build_info_path(
            "ax-helloworld",
            "aarch64-unknown-none-softfloat",
            Some(PathBuf::from("/tmp/custom-build.toml")),
        )
        .unwrap();

        assert_eq!(path, PathBuf::from("/tmp/custom-build.toml"));
    }

    #[test]
    fn resolve_build_info_path_in_dir_prefers_existing_bare_name() {
        let root = tempdir().unwrap();
        let bare = root
            .path()
            .join("build-aarch64-unknown-none-softfloat.toml");
        let dotted = root
            .path()
            .join(".build-aarch64-unknown-none-softfloat.toml");
        fs::write(&bare, "").unwrap();
        fs::write(&dotted, "").unwrap();

        let path = resolve_build_info_path_in_dir(root.path(), "aarch64-unknown-none-softfloat");

        assert_eq!(path, bare);
    }

    #[test]
    fn resolve_build_info_path_in_dir_falls_back_to_dotted_default() {
        let root = tempdir().unwrap();

        let path = resolve_build_info_path_in_dir(root.path(), "aarch64-unknown-none-softfloat");

        assert_eq!(
            path,
            root.path()
                .join(".build-aarch64-unknown-none-softfloat.toml")
        );
    }

    #[test]
    fn load_build_info_creates_missing_default_file() {
        let root = tempdir().unwrap();
        let path = root.path().join(".build-target.toml");
        let request = request("ax-helloworld", "target", None, path.clone());

        let build_info = load_build_info(&request).unwrap();

        assert_eq!(build_info, ArceosBuildInfo::default_for_target("target"));
        assert!(path.exists());
        assert!(
            fs::read_to_string(path)
                .unwrap()
                .contains("features = [\"ax-std\"]")
        );
    }

    #[test]
    fn load_build_info_creates_aarch64_default_with_dynamic_platform_enabled() {
        let root = tempdir().unwrap();
        let path = root.path().join(".build-aarch64.toml");
        let request = request(
            "ax-helloworld",
            "aarch64-unknown-none-softfloat",
            None,
            path,
        );

        let build_info = load_build_info(&request).unwrap();

        assert!(build_info.plat_dyn);
    }

    #[test]
    fn load_build_info_reads_existing_file() {
        let root = tempdir().unwrap();
        let path = root.path().join(".build-target.toml");
        fs::write(
            &path,
            r#"
features = ["ax-std", "net"]
log = "Debug"
max_cpu_num = 4

[env]
AX_IP = "127.0.0.1"
"#,
        )
        .unwrap();
        let request = request("ax-helloworld", "target", None, path);

        let build_info = load_build_info(&request).unwrap();

        assert_eq!(build_info.log, LogLevel::Debug);
        assert_eq!(build_info.max_cpu_num, Some(4));
        assert!(build_info.features.contains(&"net".to_string()));
        assert_eq!(build_info.env.get("AX_IP"), Some(&"127.0.0.1".to_string()));
    }

    #[test]
    fn load_build_info_normalizes_legacy_feature_aliases() {
        let root = tempdir().unwrap();
        let path = root.path().join(".build-target.toml");
        fs::write(
            &path,
            r#"
features = ["axstd", "axstd/smp", "axfeat/net"]
log = "Warn"

[env]
AX_IP = "10.0.2.15"
"#,
        )
        .unwrap();
        let request = request("ax-helloworld", "target", None, path.clone());

        let build_info = load_build_info(&request).unwrap();

        assert!(build_info.features.contains(&"ax-std".to_string()));
        assert!(build_info.features.contains(&"ax-std/smp".to_string()));
        assert!(build_info.features.contains(&"ax-feat/net".to_string()));
        assert!(!build_info.features.contains(&"axstd".to_string()));
        assert!(!build_info.features.contains(&"axstd/smp".to_string()));
        assert!(!build_info.features.contains(&"axfeat/net".to_string()));

        let rewritten = fs::read_to_string(path).unwrap();
        assert!(rewritten.contains("ax-std"));
        assert!(rewritten.contains("ax-std/smp"));
        assert!(rewritten.contains("ax-feat/net"));
        assert!(!rewritten.contains("axstd"));
        assert!(!rewritten.contains("axfeat/net"));
    }

    #[test]
    fn load_build_info_rejects_zero_max_cpu_num() {
        let err = ArceosBuildInfo {
            max_cpu_num: Some(0),
            ..ArceosBuildInfo::default()
        }
        .validated_max_cpu_num()
        .unwrap_err()
        .to_string();

        assert!(err.contains("max_cpu_num must be greater than 0"));
    }

    #[test]
    fn to_cargo_config_includes_ax_log_env() {
        let root = tempdir().unwrap();
        let request = request(
            "ax-helloworld",
            "aarch64-unknown-none-softfloat",
            None,
            root.path().join(".build.toml"),
        );

        let cargo = ArceosBuildInfo::default_for_target("aarch64-unknown-none-softfloat")
            .into_cargo_config(&request)
            .unwrap();

        assert_eq!(cargo.env.get("AX_LOG"), Some(&"warn".to_string()));
    }

    #[test]
    fn to_cargo_config_maps_max_cpu_num_to_smp_env_for_dynamic_platforms() {
        let root = tempdir().unwrap();
        let request = request(
            "ax-helloworld",
            "aarch64-unknown-none-softfloat",
            Some(true),
            root.path().join(".build.toml"),
        );

        let cargo = ArceosBuildInfo {
            max_cpu_num: Some(4),
            ..ArceosBuildInfo::default_for_target("aarch64-unknown-none-softfloat")
        }
        .into_cargo_config(&request)
        .unwrap();

        assert_eq!(cargo.env.get("SMP"), Some(&"4".to_string()));
        assert!(cargo.features.contains(&"ax-std/smp".to_string()));
    }

    #[test]
    fn to_cargo_config_maps_single_cpu_to_smp_env_without_forcing_smp_feature() {
        let root = tempdir().unwrap();
        let request = request(
            "ax-helloworld",
            "aarch64-unknown-none-softfloat",
            Some(true),
            root.path().join(".build.toml"),
        );

        let cargo = ArceosBuildInfo {
            max_cpu_num: Some(1),
            ..ArceosBuildInfo::default_for_target("aarch64-unknown-none-softfloat")
        }
        .into_cargo_config(&request)
        .unwrap();

        assert_eq!(cargo.env.get("SMP"), Some(&"1".to_string()));
        assert!(!cargo.features.contains(&"ax-std/smp".to_string()));
    }

    #[test]
    fn base_cargo_config_defaults_to_bin_false_for_x86_64_targets() {
        let cargo = ArceosBuildInfo::default_for_target("x86_64-unknown-none")
            .into_base_cargo_config_with_log(
                "ax-helloworld".to_string(),
                "x86_64-unknown-none".to_string(),
                vec![],
            );

        assert!(!cargo.to_bin);
    }

    #[test]
    fn base_cargo_config_keeps_to_bin_true_for_non_x86_64_targets() {
        let cargo = ArceosBuildInfo::default_for_target("aarch64-unknown-none-softfloat")
            .into_base_cargo_config_with_log(
                "ax-helloworld".to_string(),
                "aarch64-unknown-none-softfloat".to_string(),
                vec![],
            );

        assert!(cargo.to_bin);
    }

    #[test]
    fn resolve_platform_package_prefers_matching_explicit_platform_dependency() {
        let platform = resolve_platform_package(
            "ax-helloworld-myplat",
            "aarch64-unknown-none-softfloat",
            &["aarch64-qemu-virt".to_string()],
        )
        .unwrap();

        assert_eq!(platform, "ax-plat-aarch64-qemu-virt");
    }

    #[test]
    fn find_local_platform_config_path_resolves_repo_platforms() {
        let path = find_local_platform_config_path("ax-plat-aarch64-qemu-virt")
            .unwrap()
            .expect("repo-local platform config should exist");

        assert!(path.ends_with(
            "components/axplat_crates/platforms/axplat-aarch64-qemu-virt/axconfig.toml"
        ));
    }

    #[test]
    fn build_info_toml_equivalent_config_converts_to_non_dynamic_cargo() {
        let toml = r#"
features = ["ax-std"]
log = "Info"
plat_dyn = true
max_cpu_num = 4

[env]
AX_IP = "10.0.2.15"
AX_GW = "10.0.2.2"
"#;

        let build_info: ArceosBuildInfo =
            toml::from_str(toml).expect("build info should deserialize");
        let app_dir = resolve_package_manifest_path("ax-helloworld", None)
            .unwrap()
            .parent()
            .unwrap()
            .to_path_buf();
        let generated_config = app_dir.join(".axconfig.toml");
        let existed = generated_config.exists();
        let request = request(
            "ax-helloworld",
            "aarch64-unknown-none-softfloat",
            Some(false),
            app_dir.join(".build-aarch64-unknown-none-softfloat.toml"),
        );

        let cargo = build_info.into_cargo_config(&request).unwrap();

        assert!(cargo.features.contains(&"ax-std/defplat".to_string()));
        assert!(cargo.features.contains(&"ax-std/smp".to_string()));
        assert!(!cargo.features.contains(&"ax-std/plat-dyn".to_string()));
        assert!(cargo.args.iter().any(|arg| arg.contains("-Tlinker.x")));
        assert_eq!(cargo.env.get("SMP"), Some(&"4".to_string()));
        assert_eq!(
            cargo.env.get("AX_CONFIG_PATH"),
            Some(&generated_config.display().to_string())
        );
        assert_eq!(
            cargo.env.get("AX_PLATFORM"),
            Some(&"aarch64-qemu-virt".to_string())
        );
        assert!(
            fs::read_to_string(&generated_config)
                .unwrap()
                .contains("max-cpu-num = 4")
        );

        if !existed && generated_config.exists() {
            fs::remove_file(generated_config).unwrap();
        }
    }

    #[test]
    fn resolve_effective_plat_dyn_uses_override_and_target_support() {
        assert!(resolve_effective_plat_dyn(
            "aarch64-unknown-none-softfloat",
            true,
            None
        ));
        assert!(!resolve_effective_plat_dyn(
            "aarch64-unknown-none-softfloat",
            true,
            Some(false)
        ));
        assert!(resolve_effective_plat_dyn(
            "aarch64-unknown-none-softfloat",
            false,
            Some(true)
        ));
        assert!(!resolve_effective_plat_dyn(
            "x86_64-unknown-none",
            true,
            Some(true)
        ));
    }

    fn temp_workspace(
        package_name: &str,
        dependency_block: &str,
    ) -> anyhow::Result<std::path::PathBuf> {
        let root = tempdir()?.keep();

        fs::write(
            root.join("Cargo.toml"),
            "[workspace]\nmembers = [\"app\"]\nresolver = \"3\"\n\n[workspace.package]\nedition = \
             \"2024\"\n",
        )?;

        let app_dir = root.join("app");
        fs::create_dir_all(&app_dir)?;
        fs::write(
            app_dir.join("Cargo.toml"),
            format!(
                "[package]\nname = \"{package_name}\"\nversion = \"0.1.0\"\nedition = \
                 \"2024\"\n\n[dependencies]\n{dependency_block}"
            ),
        )?;
        fs::create_dir_all(app_dir.join("src"))?;
        fs::write(app_dir.join("src/lib.rs"), "pub fn smoke() {}\n")?;

        Ok(root)
    }

    #[test]
    fn resolve_platform_package_ignores_unselected_axplat_dependency() {
        let package = resolve_platform_package(
            "starryos",
            "riscv64gc-unknown-none-elf",
            &["qemu".to_string()],
        )
        .unwrap();

        assert_eq!(package, "ax-plat-riscv64-qemu-virt");
    }
}
