mod args;
pub(crate) mod config;
#[cfg(test)]
pub(crate) mod fixture;
mod metadata;

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

use crate::cli::{CliError, Result};
use crate::config::Config;

use self::args::CargoArguments;

#[cfg(test)]
pub(crate) use self::metadata::CargoCrateType;
pub(crate) use self::metadata::CargoMetadata;

#[derive(Debug, Clone)]
pub(crate) struct Cargo {
    working_directory: PathBuf,
    arguments: CargoArguments,
}

impl Cargo {
    pub(crate) fn current(cargo_args: &[String]) -> Result<Self> {
        std::env::current_dir()
            .map(|working_directory| Self::in_working_directory(working_directory, cargo_args))
            .map_err(|source| CliError::CommandFailed {
                command: format!("current_dir: {source}"),
                status: None,
            })
    }

    pub(crate) fn in_working_directory(working_directory: PathBuf, cargo_args: &[String]) -> Self {
        Self {
            working_directory,
            arguments: CargoArguments::new(cargo_args),
        }
    }

    pub(crate) fn metadata(&self) -> Result<CargoMetadata> {
        CargoMetadata::load(self)
    }

    pub(crate) fn manifest_path(&self) -> Result<PathBuf> {
        self.arguments.manifest_path(&self.working_directory)
    }

    pub(crate) fn toolchain_selector(&self) -> Option<&str> {
        self.arguments.toolchain_selector()
    }

    pub(crate) fn package_selector(&self) -> Option<&str> {
        self.arguments.package_selector()
    }

    pub(crate) fn target_selector(&self) -> Option<&str> {
        self.arguments.target_selector()
    }

    pub(crate) fn probe_command_arguments(&self) -> Vec<String> {
        self.arguments.probe_command_arguments()
    }

    pub(crate) fn configured_build_target(&self) -> Option<String> {
        self.arguments
            .configured_build_target(&self.working_directory)
    }

    pub(crate) fn effective_package_selector(
        &self,
        config: &Config,
        metadata: &CargoMetadata,
        manifest_path: &Path,
    ) -> Option<String> {
        if let Some(package_selector) = self.package_selector() {
            return Some(package_selector.to_string());
        }

        let manifest_selects_package = self.arguments.has_explicit_manifest_path()
            && metadata
                .packages
                .iter()
                .any(|package| package.manifest_path == manifest_path);

        (!manifest_selects_package)
            .then(|| self.infer_package_selector(config, metadata, manifest_path))
            .flatten()
    }

    pub(super) fn working_directory(&self) -> &Path {
        &self.working_directory
    }

    pub(super) fn metadata_arguments(&self) -> Vec<String> {
        self.arguments.metadata_arguments()
    }

    #[cfg(test)]
    pub(crate) fn command_arguments(&self) -> &[String] {
        self.arguments.command_arguments()
    }

    #[cfg(test)]
    pub(crate) fn metadata_passthrough_arguments(&self) -> Vec<String> {
        self.arguments.metadata_arguments()
    }

    #[cfg(test)]
    pub(crate) fn command_arguments_without_package_selector(&self) -> Vec<String> {
        self.arguments.command_arguments_without_package_selector()
    }

    #[cfg(test)]
    pub(crate) fn command_arguments_without_manifest_path_selector(&self) -> Vec<String> {
        self.arguments
            .command_arguments_without_manifest_path_selector()
    }

    pub(crate) fn command_arguments_without_target_selector(&self) -> Vec<String> {
        self.arguments.command_arguments_without_target_selector()
    }

    fn infer_package_selector(
        &self,
        config: &Config,
        metadata: &CargoMetadata,
        manifest_path: &Path,
    ) -> Option<String> {
        metadata
            .packages
            .iter()
            .find(|package| package.manifest_path == manifest_path)
            .map(|package| package.name.clone())
            .or_else(|| {
                let mut matching_packages = metadata
                    .packages
                    .iter()
                    .filter(|package| package.name == config.package.name);
                let package = matching_packages.next()?;
                matching_packages
                    .next()
                    .is_none()
                    .then(|| package.name.clone())
            })
            .or_else(|| {
                let crate_artifact_name = config.crate_artifact_name();
                let mut matching_packages = metadata
                    .packages
                    .iter()
                    .filter(|package| package.has_target(&crate_artifact_name));
                let package = matching_packages.next()?;
                matching_packages
                    .next()
                    .is_none()
                    .then(|| package.name.clone())
            })
    }
}

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

    use super::Cargo;
    use crate::cargo::fixture::{CargoMetadataFixture, CargoPackageFixture, CargoTargetFixture};
    use crate::cargo::metadata::CargoCrateType;
    use crate::config::{CargoConfig, Config, PackageConfig, TargetsConfig};

    fn cargo(arguments: &[&str]) -> Cargo {
        Cargo::in_working_directory(
            std::env::current_dir().unwrap_or_default(),
            &arguments
                .iter()
                .map(|argument| argument.to_string())
                .collect::<Vec<_>>(),
        )
    }

    fn config(crate_name: Option<&str>) -> Config {
        Config {
            experimental: Vec::new(),
            cargo: CargoConfig::default(),
            package: PackageConfig {
                name: "workspace-member".to_string(),
                crate_name: crate_name.map(str::to_string),
                version: None,
                description: None,
                license: None,
                repository: None,
            },
            targets: TargetsConfig::default(),
        }
    }

    fn metadata_fixture() -> CargoMetadataFixture {
        CargoMetadataFixture::new("/tmp/boltffi-target")
    }

    #[cfg(windows)]
    fn expected_canonical_manifest_path() -> PathBuf {
        dunce::canonicalize(
            std::env::current_dir()
                .expect("current dir")
                .join("Cargo.toml"),
        )
        .expect("canonical manifest path")
    }

    #[cfg(not(windows))]
    fn expected_canonical_manifest_path() -> PathBuf {
        std::env::current_dir()
            .expect("current dir")
            .join("Cargo.toml")
            .canonicalize()
            .expect("canonical manifest path")
    }

    #[test]
    fn splits_toolchain_selector_from_cargo_args() {
        let cargo = cargo(&["--features", "demo", "+nightly", "--locked"]);

        assert_eq!(cargo.toolchain_selector(), Some("+nightly"));
        assert_eq!(
            cargo.command_arguments(),
            vec![
                "--features".to_string(),
                "demo".to_string(),
                "--locked".to_string()
            ]
        );
    }

    #[test]
    fn keeps_metadata_relevant_cargo_args() {
        let cargo = cargo(&[
            "+nightly",
            "--target-dir",
            "out/target",
            "--config=build.target-dir=\"other-target\"",
            "--locked",
            "--features",
            "demo",
            "--manifest-path",
            "examples/demo/Cargo.toml",
            "-Zunstable-options",
        ]);
        let metadata_args = cargo.metadata_passthrough_arguments();

        assert_eq!(cargo.toolchain_selector(), Some("+nightly"));
        assert_eq!(
            metadata_args,
            vec![
                "--target-dir".to_string(),
                "out/target".to_string(),
                "--config=build.target-dir=\"other-target\"".to_string(),
                "--locked".to_string(),
                "--manifest-path".to_string(),
                "examples/demo/Cargo.toml".to_string(),
                "-Zunstable-options".to_string(),
            ]
        );
    }

    #[test]
    fn canonicalizes_manifest_path_from_split_cargo_args() {
        let expected = expected_canonical_manifest_path();

        let manifest_path = cargo(&["--manifest-path", "Cargo.toml"])
            .manifest_path()
            .expect("manifest path");

        assert_eq!(manifest_path, expected);
    }

    #[test]
    fn canonicalizes_manifest_path_from_equals_cargo_arg() {
        let expected = expected_canonical_manifest_path();

        let manifest_path = cargo(&["--manifest-path=Cargo.toml"])
            .manifest_path()
            .expect("manifest path");

        assert_eq!(manifest_path, expected);
    }

    #[test]
    fn canonicalizes_implicit_manifest_path() {
        let expected = expected_canonical_manifest_path();

        let manifest_path = cargo(&[]).manifest_path().expect("manifest path");

        assert_eq!(manifest_path, expected);
    }

    #[cfg(windows)]
    #[test]
    fn canonical_manifest_path_omits_windows_verbatim_prefix() {
        use std::path::{Component, Prefix};

        let manifest_path = cargo(&[]).manifest_path().expect("manifest path");
        let has_verbatim_prefix = matches!(
            manifest_path.components().next(),
            Some(Component::Prefix(prefix)) if matches!(
                prefix.kind(),
                Prefix::Verbatim(_) | Prefix::VerbatimDisk(_) | Prefix::VerbatimUNC(_, _)
            )
        );

        assert!(
            !has_verbatim_prefix,
            "{} should not use a Windows verbatim prefix",
            manifest_path.display()
        );
    }

    #[test]
    fn extracts_last_package_selector_from_cargo_args() {
        let package_selector = cargo(&[
            "--manifest-path",
            "Cargo.toml",
            "-p",
            "first",
            "--package=second",
        ])
        .package_selector()
        .map(str::to_owned);

        assert_eq!(package_selector.as_deref(), Some("second"));
    }

    #[test]
    fn extracts_package_spec_selector_from_cargo_args() {
        let package_selector = cargo(&["--locked", "-p", "workspace-member@1.2.3"])
            .package_selector()
            .map(str::to_owned);

        assert_eq!(package_selector.as_deref(), Some("workspace-member@1.2.3"));
    }

    #[test]
    fn extracts_last_target_selector_from_cargo_args() {
        let target_selector = cargo(&[
            "--target",
            "aarch64-apple-darwin",
            "--target=x86_64-unknown-linux-gnu",
        ])
        .target_selector()
        .map(str::to_owned);

        assert_eq!(target_selector.as_deref(), Some("x86_64-unknown-linux-gnu"));
    }

    #[test]
    fn strips_package_selectors_from_probe_cargo_args() {
        let cargo_args = cargo(&[
            "+nightly",
            "--package",
            "member-a",
            "-pmember-b",
            "-p",
            "member-c@1.2.3",
            "--features",
            "demo",
            "--package=member-d",
            "--release",
        ])
        .command_arguments_without_package_selector();

        assert_eq!(
            cargo_args,
            vec![
                "+nightly".to_string(),
                "--features".to_string(),
                "demo".to_string(),
                "--release".to_string(),
            ]
        );
    }

    #[test]
    fn strips_manifest_path_from_probe_cargo_args() {
        let cargo_args = cargo(&[
            "--locked",
            "--manifest-path",
            "workspace/Cargo.toml",
            "--manifest-path=member/Cargo.toml",
            "--frozen",
        ])
        .command_arguments_without_manifest_path_selector();

        assert_eq!(
            cargo_args,
            vec!["--locked".to_string(), "--frozen".to_string()]
        );
    }

    #[test]
    fn strips_target_selectors_from_probe_cargo_args() {
        let cargo_args = cargo(&[
            "+nightly",
            "--target",
            "aarch64-apple-darwin",
            "--features",
            "demo",
            "--target=x86_64-unknown-linux-gnu",
            "--release",
        ])
        .command_arguments_without_target_selector();

        assert_eq!(
            cargo_args,
            vec![
                "+nightly".to_string(),
                "--features".to_string(),
                "demo".to_string(),
                "--release".to_string(),
            ]
        );
    }

    #[test]
    fn probe_command_arguments_strip_rustup_toolchain_selectors() {
        let cargo_args = cargo(&[
            "--package",
            "member-a",
            "+nightly",
            "--manifest-path",
            "workspace/Cargo.toml",
            "--target",
            "aarch64-apple-darwin",
            "--features",
            "demo",
            "--release",
        ])
        .probe_command_arguments();

        assert_eq!(
            cargo_args,
            vec![
                "--features".to_string(),
                "demo".to_string(),
                "--release".to_string(),
            ]
        );
    }

    #[test]
    fn falls_back_to_current_manifest_package_for_effective_package_selector() {
        let metadata = metadata_fixture()
            .package(
                CargoPackageFixture::manifest_package(
                    "workspace-member",
                    "/tmp/workspace/Cargo.toml",
                    "0.1.0",
                )
                .target(CargoTargetFixture::library(
                    "workspace_member",
                    [CargoCrateType::Cdylib],
                )),
            )
            .metadata();

        let package_selector = cargo(&[]).effective_package_selector(
            &config(None),
            &metadata,
            Path::new("/tmp/workspace/Cargo.toml"),
        );

        assert_eq!(package_selector.as_deref(), Some("workspace-member"));
    }

    #[test]
    fn falls_back_to_cargo_package_name_when_crate_name_differs() {
        let metadata = metadata_fixture()
            .package(
                CargoPackageFixture::manifest_package(
                    "workspace-member",
                    "/tmp/workspace/Cargo.toml",
                    "0.1.0",
                )
                .target(CargoTargetFixture::library(
                    "ffi_member",
                    [CargoCrateType::Cdylib],
                )),
            )
            .metadata();

        let package_selector = cargo(&[]).effective_package_selector(
            &config(Some("ffi_member")),
            &metadata,
            Path::new("/tmp/workspace/Cargo.toml"),
        );

        assert_eq!(package_selector.as_deref(), Some("workspace-member"));
    }

    #[test]
    fn returns_none_for_effective_package_selector_when_manifest_path_selects_package() {
        let metadata = metadata_fixture()
            .package(CargoPackageFixture::manifest_package(
                "workspace-member",
                "/tmp/workspace/member/Cargo.toml",
                "0.1.0",
            ))
            .metadata();

        let package_selector = cargo(&["--manifest-path", "member/Cargo.toml"])
            .effective_package_selector(
                &config(None),
                &metadata,
                Path::new("/tmp/workspace/member/Cargo.toml"),
            );

        assert_eq!(package_selector, None);
    }

    #[test]
    fn falls_back_to_package_name_for_virtual_workspace_manifest_path() {
        let metadata = metadata_fixture()
            .package(
                CargoPackageFixture::manifest_package(
                    "workspace-member",
                    "/tmp/workspace/member/Cargo.toml",
                    "0.1.0",
                )
                .target(CargoTargetFixture::library(
                    "workspace_member",
                    [CargoCrateType::Cdylib],
                )),
            )
            .metadata();

        let package_selector = cargo(&["--manifest-path", "/tmp/workspace/Cargo.toml"])
            .effective_package_selector(
                &config(None),
                &metadata,
                Path::new("/tmp/workspace/Cargo.toml"),
            );

        assert_eq!(package_selector.as_deref(), Some("workspace-member"));
    }

    #[test]
    fn falls_back_to_package_name_when_crate_name_differs_for_virtual_workspace_manifest_path() {
        let metadata = metadata_fixture()
            .package(
                CargoPackageFixture::manifest_package(
                    "workspace-member",
                    "/tmp/workspace/member/Cargo.toml",
                    "0.1.0",
                )
                .target(CargoTargetFixture::library(
                    "ffi_member",
                    [CargoCrateType::Cdylib],
                )),
            )
            .metadata();

        let package_selector = cargo(&["--manifest-path", "/tmp/workspace/Cargo.toml"])
            .effective_package_selector(
                &config(Some("ffi_member")),
                &metadata,
                Path::new("/tmp/workspace/Cargo.toml"),
            );

        assert_eq!(package_selector.as_deref(), Some("workspace-member"));
    }

    #[test]
    fn prefers_explicit_package_selector_over_config_package_name() {
        let package_selector = cargo(&["--package=selected-member"]).effective_package_selector(
            &config(None),
            &metadata_fixture().metadata(),
            Path::new("/tmp/workspace/Cargo.toml"),
        );

        assert_eq!(package_selector.as_deref(), Some("selected-member"));
    }

    #[test]
    fn prefers_configured_package_name_over_unique_library_target_match() {
        let metadata = metadata_fixture()
            .package(
                CargoPackageFixture::manifest_package(
                    "other-member",
                    "/tmp/workspace/other/Cargo.toml",
                    "0.1.0",
                )
                .target(CargoTargetFixture::library(
                    "ffi_member",
                    [CargoCrateType::Cdylib],
                )),
            )
            .package(
                CargoPackageFixture::manifest_package(
                    "workspace-member",
                    "/tmp/workspace/member/Cargo.toml",
                    "0.1.0",
                )
                .target(CargoTargetFixture::library(
                    "workspace_member_lib",
                    [CargoCrateType::Cdylib],
                )),
            )
            .metadata();

        let package_selector = cargo(&["--manifest-path", "/tmp/workspace/Cargo.toml"])
            .effective_package_selector(
                &config(Some("ffi_member")),
                &metadata,
                Path::new("/tmp/workspace/Cargo.toml"),
            );

        assert_eq!(package_selector.as_deref(), Some("workspace-member"));
    }
}
