use std::collections::HashMap;

use nu_protocol::engine::Closure;
use nu_protocol::{Record, Span, Spanned, Value};

use crate::commands::{
    Perms, confirmation_prompt, dry_run_command, run_command, run_command_for_stdout,
};
use crate::parser::Engine;
use crate::{CleanCommand, SyncCommand, UpdateCommand};
use crate::{mod_err, nest_errors};

use super::{Backend, Error, Result};

const PACKAGE_LIST_KEY: &str = "packages";
const PACKAGE_KEY: &str = "package";
const FROM_KEY: &str = "from";
const WITH_LFS_KEY: &str = "with_lfs";
const PYTHON_VERSION_KEY: &str = "python_version";
const EXTRA_PACKAGES_KEY: &str = "extra_packages";
const HOOK_KEY: &str = "post_hook";
const EXTRA_NAME_KEY: &str = "name";
const EXTRA_EXECUTABLES_ENABLE_KEY: &str = "enable_executables";

#[derive(Debug)]
struct UvOpts {
    from: Option<String>,
    with_lfs: bool,
    py_version: Option<String>,
    with: Box<[(String, bool)]>,
    post_hook: Option<Closure>,
}

// TODO: Handle toolchains
#[derive(Debug)]
pub struct Uv {
    packages: HashMap<String, UvOpts>,
}

impl Backend for Uv {
    fn new(value: Spanned<&Record>, _config: Spanned<&Record>) -> Result<Self> {
        let packages = value
            .item
            .get(PACKAGE_LIST_KEY)
            .ok_or_else(|| mod_err!(value.span, "Failed to get packages for Uv"))?;

        let packages = packages
            .as_list()
            .map_err(|e| {
                nest_errors!(
                    (value.span, packages.span()),
                    "Packages not a list for Uv",
                    e
                )
            })?
            .iter()
            .map(|value| value_to_pkgspec(value, packages.span()))
            .collect::<Result<_>>()
            .inspect(|_| log::info!("Parsed uv packages from spec"))
            .map_err(|e| {
                nest_errors!(packages.span(), "Failed to parse the packages from uv", e)
            })?;

        Ok(Uv { packages })
    }

    fn install(&self, engine: &mut Engine, opts: &SyncCommand) -> Result<()> {
        let installed_packages = get_installed_packages()
            .map_err(|e| nest_errors!("Failed to find the installed packages in uv", e))?;
        let configured_packages = &self.packages;

        let missing_packages: Box<[_]> = configured_packages
            .iter()
            .filter(|(package, required)| {
                installed_packages
                    .get(*package)
                    .map(|other| extra_opts(required, other))
                    .map(|spec| !spec.with.is_empty())
                    .unwrap_or(true)
            })
            .collect();

        if missing_packages.is_empty() {
            log::info!("No new packages to install in uv");
            return Ok(());
        }

        if !opts.dry_run
            && !opts.no_confirm
            && !confirmation_prompt(
                "Do you want to install the following packages for uv?: ",
                missing_packages.iter().map(|pkg| pkg.0),
            )?
        {
            return Ok(());
        }

        let mut closures = Vec::new();

        missing_packages
            .iter()
            .try_for_each(|(package, spec)| {
                if let Some(hook) = spec.post_hook.as_ref() {
                    closures.push(hook);
                }
                install_package(package, spec, opts.dry_run)
            })
            .inspect(|_| log::info!("Successfully installed missing packages"))
            .map_err(|e| nest_errors!("Failed to install packages in uv", e))?;

        closures
            .into_iter()
            .try_for_each(|hook| {
                if opts.dry_run {
                    engine.dry_run_closure(hook)
                } else {
                    engine.execute_closure(hook)
                }
            })
            .inspect(|_| log::info!("Successfully executed all the closures"))
            .map_err(|e| nest_errors!("Failed to execute closures", e))
    }

    fn remove(&self, opts: &CleanCommand) -> Result<()> {
        let installed_packages = get_installed_packages()
            .map_err(|e| nest_errors!("Failed to get installed packages in uv", e))?;
        let configured_packages = &self.packages;

        remove_extra_packages(&installed_packages, configured_packages, opts)
            .inspect(|_| log::info!("Successfully removed extra packages from uv"))
            .map_err(|e| nest_errors!("Failed to remove extra packages in uv", e))?;

        remove_extra_deps(&installed_packages, configured_packages, opts)
            .inspect(|_| log::info!("Successfully removed extra dependencies"))
            .map_err(|e| nest_errors!("Failed to remove extra dependencies in uv", e))
    }

    fn update(&self, opts: &UpdateCommand) -> Result<()> {
        if !opts.dry_run
            && !opts.no_confirm
            && !confirmation_prompt("Do you want to update uv?: ", &["Using <uv tool upgrade>"])?
        {
            return Ok(());
        }

        let command_action = if opts.dry_run {
            dry_run_command
        } else {
            run_command
        };

        command_action(["uv", "tool", "upgrade", "--all"], Perms::User)
            .inspect(|_| log::info!("Successfully updated uv backend"))
            .map_err(|e| nest_errors!("Failed to update uv", e))
    }

    fn clean_cache(&self, _opts: &crate::CleanCacheCommand) -> Result<()> {
        // nothing to do
        Ok(())
    }
}

fn value_to_pkgspec(value: &Value, outer_span: Span) -> Result<(String, UvOpts)> {
    let record = value
        .as_record()
        .map_err(|e| nest_errors!((outer_span, value.span()), "Failed to parse value", e))?;

    let package = record
        .get(PACKAGE_KEY)
        .ok_or_else(|| mod_err!((outer_span, value.span()), "No package mentioned"))?;

    let package = package
        .as_str()
        .map_err(|e| {
            nest_errors!(
                (value.span(), package.span()),
                "Package name in record is not a string",
                e
            )
        })?
        .to_owned();

    let from = match record.get(FROM_KEY) {
        Some(from) => {
            let from = from.as_str().map_err(|e| {
                nest_errors!(
                    (value.span(), from.span()),
                    format!("'{FROM_KEY}' in {package} is not a string"),
                    e
                )
            })?;
            Some(from.to_owned())
        }
        None => {
            log::debug!("no '{FROM_KEY}' specified in {package}, defaulting to None");
            None
        }
    };

    let with_lfs = match record.get(WITH_LFS_KEY) {
        Some(with_lfs) => {
            let git_prefix = from
                .as_ref()
                .map(|from| from.starts_with("git+"))
                .unwrap_or(false);
            if !git_prefix {
                log::warn!("{WITH_LFS_KEY} specified without a git source, ignoring!");
                false
            } else {
                with_lfs.as_bool().map_err(|e| {
                    nest_errors!(
                        (value.span(), with_lfs.span()),
                        format!("{WITH_LFS_KEY} in {package} is not a boolean"),
                        e
                    )
                })?
            }
        }
        None => {
            log::debug!("no {WITH_LFS_KEY} specified in {package}, defaulting to false");
            false
        }
    };

    let py_version = match record.get(PYTHON_VERSION_KEY) {
        Some(python_version) => {
            let span = python_version.span();
            let python_version = python_version.as_list().map_err(|e| {
                nest_errors!(
                    (value.span(), span),
                    format!("{PYTHON_VERSION_KEY} specified in {package} not a list"),
                    e
                )
            })?;
            let python_version = format_semver(python_version, value.span(), span)
                .map_err(|e| nest_errors!(span, "Failed to get the semver from version", e))?;
            Some(python_version)
        }
        None => {
            log::debug!("no {PYTHON_VERSION_KEY} specified in {package}, defaulting to None");
            None
        }
    };

    let with = match record.get(EXTRA_PACKAGES_KEY) {
        Some(extra_packages_list) => {
            let span = extra_packages_list.span();

            let extra_packages = extra_packages_list.as_list().map_err(|e| {
                nest_errors!(
                    (value.span(), span),
                    format!("{EXTRA_PACKAGES_KEY} specified in {package} not a list"),
                    e
                )
            })?;

            extra_packages
                .iter()
                .map(|extra| value_to_extraspec(extra, span, &package))
                .collect::<Result<Box<[_]>>>()
                .map_err(|e| {
                    nest_errors!(span, format!("Failed to parse the extras for {package}"), e)
                })?
        }
        None => Box::new([]),
    };

    let post_hook = match record.get(HOOK_KEY) {
        Some(closure) => {
            let closure = closure.as_closure().map_err(|e| {
                nest_errors!(
                    (value.span(), closure.span()),
                    format!("closure for {package} not a closure"),
                    e
                )
            })?;

            Some(closure.to_owned())
        }
        None => None,
    };

    Ok((
        package,
        UvOpts {
            from,
            with_lfs,
            py_version,
            with,
            post_hook,
        },
    ))
}

fn get_installed_packages() -> Result<HashMap<String, UvOpts>> {
    let res = run_command_for_stdout(
        ["uv", "tool", "list", "--color", "never", "--show-with"],
        Perms::User,
        true,
    )
    .map_err(|e| nest_errors!("Failed to get installed packages for uv", e))?
    .lines()
    .filter(|line| !line.starts_with("-"))
    .map(parse_uv_package)
    .collect();

    Ok(res)
}

fn remove_extra_packages(
    installed_packages: &HashMap<String, UvOpts>,
    configured_packages: &HashMap<String, UvOpts>,
    opts: &CleanCommand,
) -> Result<()> {
    let extra_packages: Box<[_]> = installed_packages
        .iter()
        .filter(|package| !configured_packages.contains_key(package.0))
        .map(|package| package.0)
        .collect();

    if extra_packages.is_empty() {
        log::info!("No extra packages to remove in uv");
        return Ok(());
    }

    if !opts.dry_run
        && !opts.no_confirm
        && !confirmation_prompt(
            "Do you want to remove the following packages for uv?: ",
            &extra_packages,
        )?
    {
        return Ok(());
    }

    let command = ["uv", "tool", "uninstall"]
        .into_iter()
        .chain(extra_packages.into_iter().map(String::as_str));
    if opts.dry_run {
        dry_run_command(command, Perms::User)
    } else {
        run_command(command, Perms::User)
    }
}

fn remove_extra_deps(
    installed_packages: &HashMap<String, UvOpts>,
    configured_packages: &HashMap<String, UvOpts>,
    opts: &CleanCommand,
) -> Result<()> {
    let extra_deps: Box<[_]> = installed_packages
        .iter()
        .filter_map(|(package, spec)| {
            configured_packages
                .get(package)
                .map(|other| extra_opts(spec, other))
                .filter(|diff| !diff.with.is_empty())
                .map(|_| (package, configured_packages.get(package).unwrap()))
        })
        .collect();

    if extra_deps.is_empty() {
        log::info!("No extra deps from existing packages to remove in uv");
        return Ok(());
    }

    if !opts.dry_run
        && !opts.no_confirm
        && !confirmation_prompt(
            "Do you want to remove the extra dependencies from the following uv packages?: ",
            extra_deps.iter().map(|(pkg, _)| pkg),
        )?
    {
        return Ok(());
    }

    // We install it with the correct set of extra deps and uv takes care of removing the others itself
    extra_deps
        .iter()
        .try_for_each(|(package, spec)| install_package(package, spec, opts.dry_run))
}

// TODO: What else can we compare across the two options? Python versions? There doesn't seem to be a way that
// we can compare installation options. Seems like the way to go forward would be to maintain behaviour similar
// to cargo
/// Checks if [current] has different/"extra" opts than required. The meaning of extra is subject to change
fn extra_opts(current: &UvOpts, required: &UvOpts) -> UvOpts {
    let with = current
        .with
        .iter()
        .filter(|(pkg, _)| required.with.iter().all(|(other, _)| other != pkg))
        .cloned()
        .collect();

    UvOpts {
        from: None,
        with_lfs: false,
        py_version: None,
        with,
        post_hook: None,
    }
}

// https://users.rust-lang.org/t/borrow-checker-lifetime-error-in-iterator/137260/2?u=innocentzer0
fn install_package(package: &str, opts: &UvOpts, dry_run: bool) -> Result<()> {
    let mut with_enabled_commands = Vec::new();
    let mut with_disabled_commands = Vec::new();
    opts.with.iter().for_each(|(key, opt)| {
        if *opt {
            with_enabled_commands.push(key.clone());
        } else {
            with_disabled_commands.push(key.clone());
        }
    });

    let with_enabled = with_enabled_commands.join(",");
    let with_disabled = with_disabled_commands.join(",");

    let with_enabled_opts = ["--with-executables-from", with_enabled.as_str()]
        .into_iter()
        .filter(|_| !with_enabled_commands.is_empty());

    let with_disabled_opts = ["--with", with_disabled.as_str()]
        .into_iter()
        .filter(|_| !with_disabled_commands.is_empty());

    let from = ["--from"]
        .into_iter()
        .chain(opts.from.as_deref())
        .filter(|_| opts.from.is_some());

    let git_lfs = ["--lfs"].into_iter().filter(|_| opts.with_lfs);

    let py_version = ["--python"]
        .into_iter()
        .chain(opts.py_version.as_deref())
        .filter(|_| opts.py_version.is_some());

    let command = ["uv", "tool", "install", package]
        .into_iter()
        .chain(from)
        .chain(git_lfs)
        .chain(py_version)
        .chain(with_enabled_opts)
        .chain(with_disabled_opts);

    if dry_run {
        dry_run_command(command, Perms::User)
    } else {
        run_command(command, Perms::User)
    }
}

fn format_semver(python_version: &[Value], outer_span: Span, span: Span) -> Result<String> {
    let result = match python_version {
        [Value::Int { val: major, .. }] => major.to_string(),
        [Value::Int { val: major, .. }, Value::Int { val: minor, .. }] => {
            format!("{major}.{minor}")
        }
        [
            Value::Int { val: major, .. },
            Value::Int { val: minor, .. },
            Value::Int { val: patch, .. },
        ] => format!("{major}.{minor}.{patch}"),
        _ => {
            return Err(mod_err!(
                (outer_span, span),
                format!("Improperly formatted version")
            ));
        }
    };

    Ok(result)
}

fn value_to_extraspec(value: &Value, outer_span: Span, package: &str) -> Result<(String, bool)> {
    let record = value.as_record().map_err(|e| {
        nest_errors!(
            (outer_span, value.span()),
            format!("Value provided in {package} not a record"),
            e
        )
    })?;

    let name = record
        .get(EXTRA_NAME_KEY)
        .ok_or_else(|| mod_err!(value.span(), "No name provided for record in extras"))?;

    let name = name
        .as_str()
        .map_err(|e| {
            nest_errors!(
                (value.span(), name.span()),
                format!("Name provided in {package}'s extra not a record"),
                e
            )
        })?
        .to_owned();

    let enable_executables = match record.get(EXTRA_EXECUTABLES_ENABLE_KEY) {
        Some(enabled) => enabled.as_bool().map_err(|e| {
            nest_errors!(
                (value.span(), enabled.span()),
                format!("{EXTRA_EXECUTABLES_ENABLE_KEY} not a boolean for {name}"),
                e
            )
        })?,
        None => {
            log::debug!(
                "{EXTRA_EXECUTABLES_ENABLE_KEY} for {EXTRA_NAME_KEY} not set, defaulting to false"
            );
            false
        }
    };

    Ok((name, enable_executables))
}

fn parse_uv_package(line: &str) -> (String, UvOpts) {
    let (package_name, rest) = line.split_once(" ").expect("Failed to split line");

    let with: Box<[_]> = rest
        .split_once("[with: ")
        .map(|elem| elem.1)
        .map(|rest| {
            let with = rest
                .strip_suffix(']')
                .expect("Cannot find closing delimiter for [with:");

            with.split(',')
                .map(|elem| elem.trim().to_owned())
                .map(|elem| (elem, false))
                .collect()
        })
        .unwrap_or(Box::new([]));

    (
        package_name.to_owned(),
        UvOpts {
            from: None,
            with_lfs: false,
            with,
            py_version: None,
            post_hook: None,
        },
    )
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn value_to_pkgspec_only_package() {
        let record = Record::from_raw_cols_vals(
            vec!["package".to_owned()],
            vec![Value::string("foo", Span::test_data())],
            Span::test_data(),
            Span::test_data(),
        )
        .unwrap();

        let res = value_to_pkgspec(&Value::record(record, Span::test_data()), Span::test_data());
        assert!(res.is_ok());

        let res = res.unwrap();
        assert_eq!(res.0, "foo".to_string());
        let with: [(String, bool); 0] = [];
        assert_eq!(*res.1.with, with);
        assert!(!res.1.with_lfs);
        assert_eq!(res.1.from, None);
        assert_eq!(res.1.py_version, None);
        assert!(res.1.post_hook.is_none());
    }

    #[test]
    fn value_to_pkgspec_package_from() {
        let record = Record::from_raw_cols_vals(
            ["package", "from"]
                .into_iter()
                .map(ToOwned::to_owned)
                .collect(),
            vec![
                Value::string("foo", Span::test_data()),
                Value::string("bar", Span::test_data()),
            ],
            Span::test_data(),
            Span::test_data(),
        )
        .unwrap();

        let res = value_to_pkgspec(&Value::record(record, Span::test_data()), Span::test_data());
        assert!(res.is_ok());

        let res = res.unwrap();
        assert_eq!(res.0, "foo".to_string());
        let with: [(String, bool); 0] = [];
        assert_eq!(*res.1.with, with);
        assert!(!res.1.with_lfs);
        assert_eq!(res.1.from, Some("bar".to_owned()));
        assert_eq!(res.1.py_version, None);
        assert!(res.1.post_hook.is_none());
    }

    #[test]
    fn value_to_pkgspec_package_wrong_from() {
        let record = Record::from_raw_cols_vals(
            ["package", "from"]
                .into_iter()
                .map(ToOwned::to_owned)
                .collect(),
            vec![
                Value::string("foo", Span::test_data()),
                Value::bool(false, Span::test_data()),
            ],
            Span::test_data(),
            Span::test_data(),
        )
        .unwrap();

        let res = value_to_pkgspec(&Value::record(record, Span::test_data()), Span::test_data());
        assert!(res.is_err());
    }

    #[test]
    fn value_to_pkgspec_package_with_lfs_without_from() {
        let record = Record::from_raw_cols_vals(
            ["package", "with_lfs"]
                .into_iter()
                .map(ToOwned::to_owned)
                .collect(),
            vec![
                Value::string("foo", Span::test_data()),
                Value::bool(true, Span::test_data()),
            ],
            Span::test_data(),
            Span::test_data(),
        )
        .unwrap();

        let res = value_to_pkgspec(&Value::record(record, Span::test_data()), Span::test_data());
        assert!(res.is_ok());

        let res = res.unwrap();
        assert_eq!(res.0, "foo".to_string());
        let with: [(String, bool); 0] = [];
        assert_eq!(*res.1.with, with);
        assert!(!res.1.with_lfs);
        assert_eq!(res.1.from, None);
        assert_eq!(res.1.py_version, None);
        assert!(res.1.post_hook.is_none());
    }

    #[test]
    fn value_to_pkgspec_package_wrong_lfs_without_from() {
        let record = Record::from_raw_cols_vals(
            ["package", "with_lfs"]
                .into_iter()
                .map(ToOwned::to_owned)
                .collect(),
            vec![
                Value::string("foo", Span::test_data()),
                Value::string("false", Span::test_data()),
            ],
            Span::test_data(),
            Span::test_data(),
        )
        .unwrap();

        let res = value_to_pkgspec(&Value::record(record, Span::test_data()), Span::test_data());
        assert!(res.is_ok());

        let res = res.unwrap();
        assert_eq!(res.0, "foo".to_string());
        let with: [(String, bool); 0] = [];
        assert_eq!(*res.1.with, with);
        assert!(!res.1.with_lfs);
        assert_eq!(res.1.from, None);
        assert_eq!(res.1.py_version, None);
        assert!(res.1.post_hook.is_none());
    }

    #[test]
    fn value_to_pkgspec_package_with_lfs_wrong_from() {
        let record = Record::from_raw_cols_vals(
            ["package", "with_lfs", "from"]
                .into_iter()
                .map(ToOwned::to_owned)
                .collect(),
            vec![
                Value::string("foo", Span::test_data()),
                Value::bool(true, Span::test_data()),
                Value::string("bar", Span::test_data()),
            ],
            Span::test_data(),
            Span::test_data(),
        )
        .unwrap();

        let res = value_to_pkgspec(&Value::record(record, Span::test_data()), Span::test_data());
        assert!(res.is_ok());

        let res = res.unwrap();
        assert_eq!(res.0, "foo".to_string());
        let with: [(String, bool); 0] = [];
        assert_eq!(*res.1.with, with);
        assert!(!res.1.with_lfs);
        assert_eq!(res.1.from, Some("bar".to_string()));
        assert_eq!(res.1.py_version, None);
        assert!(res.1.post_hook.is_none());
    }

    #[test]
    fn value_to_pkgspec_package_with_lfs_with_from() {
        let record = Record::from_raw_cols_vals(
            ["package", "with_lfs", "from"]
                .into_iter()
                .map(ToOwned::to_owned)
                .collect(),
            vec![
                Value::string("foo", Span::test_data()),
                Value::bool(true, Span::test_data()),
                Value::string("git+https://github.com/httpie/cli", Span::test_data()),
            ],
            Span::test_data(),
            Span::test_data(),
        )
        .unwrap();

        let res = value_to_pkgspec(&Value::record(record, Span::test_data()), Span::test_data());
        assert!(res.is_ok());

        let res = res.unwrap();
        assert_eq!(res.0, "foo".to_string());
        let with: [(String, bool); 0] = [];
        assert_eq!(*res.1.with, with);
        assert!(res.1.with_lfs);
        assert!(res.1.from.is_some());
        let from = res.1.from.unwrap();
        assert_eq!(from, "git+https://github.com/httpie/cli");
        assert_eq!(res.1.py_version, None);
        assert!(res.1.post_hook.is_none());
    }

    #[test]
    fn value_to_pkgspec_package_with_pyver3() {
        let pyver: Vec<_> = [1, 2, 3]
            .into_iter()
            .map(|i| Value::int(i, Span::test_data()))
            .collect();

        let record = Record::from_raw_cols_vals(
            ["package", "python_version"]
                .into_iter()
                .map(ToOwned::to_owned)
                .collect(),
            vec![
                Value::string("foo", Span::test_data()),
                Value::list(pyver, Span::test_data()),
            ],
            Span::test_data(),
            Span::test_data(),
        )
        .unwrap();

        let res = value_to_pkgspec(&Value::record(record, Span::test_data()), Span::test_data());
        assert!(res.is_ok());

        let res = res.unwrap();
        assert_eq!(res.0, "foo".to_string());
        let with: [(String, bool); 0] = [];
        assert_eq!(*res.1.with, with);
        assert!(!res.1.with_lfs);
        assert!(res.1.from.is_none());
        assert_eq!(res.1.py_version, Some("1.2.3".to_owned()));
        assert!(res.1.post_hook.is_none());
    }

    #[test]
    fn format_semver2() {
        let pyver: Vec<_> = [1, 2]
            .into_iter()
            .map(|i| Value::int(i, Span::test_data()))
            .collect();

        let res = format_semver(pyver.as_slice(), Span::test_data(), Span::test_data());
        assert!(res.is_ok());

        let res = res.unwrap();
        assert_eq!(res, "1.2".to_string());
    }

    #[test]
    fn format_semver1() {
        let pyver: Vec<_> = [12]
            .into_iter()
            .map(|i| Value::int(i, Span::test_data()))
            .collect();

        let res = format_semver(pyver.as_slice(), Span::test_data(), Span::test_data());
        assert!(res.is_ok());

        let res = res.unwrap();
        assert_eq!(res, "12".to_string());
    }

    #[test]
    fn format_semver4() {
        let pyver: Vec<_> = [1, 2, 3, 4]
            .into_iter()
            .map(|i| Value::int(i, Span::test_data()))
            .collect();

        let res = format_semver(pyver.as_slice(), Span::test_data(), Span::test_data());
        assert!(res.is_err());
    }

    #[test]
    fn format_semver0() {
        let pyver: Vec<_> = []
            .into_iter()
            .map(|i| Value::int(i, Span::test_data()))
            .collect();

        let res = format_semver(pyver.as_slice(), Span::test_data(), Span::test_data());
        assert!(res.is_err());
    }

    #[test]
    fn value_to_extraspec_ok() {
        let record = Record::from_raw_cols_vals(
            vec!["name".to_owned()],
            vec![Value::string("foo", Span::test_data())],
            Span::test_data(),
            Span::test_data(),
        )
        .unwrap();

        let res = value_to_extraspec(
            &Value::record(record, Span::test_data()),
            Span::test_data(),
            "",
        );

        assert!(res.is_ok());
        let res = res.unwrap();
        assert_eq!(res.0, "foo".to_string());
        assert!(!res.1);
    }

    #[test]
    fn value_to_extraspec_with_bool() {
        let record = Record::from_raw_cols_vals(
            ["name", "enable_executables"]
                .into_iter()
                .map(ToOwned::to_owned)
                .collect(),
            vec![
                Value::string("foo", Span::test_data()),
                Value::bool(true, Span::test_data()),
            ],
            Span::test_data(),
            Span::test_data(),
        )
        .unwrap();

        let res = value_to_extraspec(
            &Value::record(record, Span::test_data()),
            Span::test_data(),
            "",
        );

        assert!(res.is_ok());
        let res = res.unwrap();
        assert_eq!(res.0, "foo".to_string());
        assert!(res.1);
    }

    #[test]
    fn value_to_extraspec_not_string() {
        let record = Record::from_raw_cols_vals(
            ["name", "enable_executables"]
                .into_iter()
                .map(ToOwned::to_owned)
                .collect(),
            vec![
                Value::bool(true, Span::test_data()),
                Value::bool(true, Span::test_data()),
            ],
            Span::test_data(),
            Span::test_data(),
        )
        .unwrap();

        let res = value_to_extraspec(
            &Value::record(record, Span::test_data()),
            Span::test_data(),
            "",
        );

        assert!(res.is_err());
    }

    #[test]
    fn value_to_extraspec_not_bool() {
        let record = Record::from_raw_cols_vals(
            ["name", "enable_executables"]
                .into_iter()
                .map(ToOwned::to_owned)
                .collect(),
            vec![
                Value::string("foo", Span::test_data()),
                Value::string("foo", Span::test_data()),
            ],
            Span::test_data(),
            Span::test_data(),
        )
        .unwrap();

        let res = value_to_extraspec(
            &Value::record(record, Span::test_data()),
            Span::test_data(),
            "",
        );

        assert!(res.is_err());
    }

    #[test]
    fn parse_uv_package_with() {
        let uv_out = "ansible v13.1.0 [with: ansible-core, ansible-lint]";
        let res = parse_uv_package(uv_out);
        assert_eq!(res.0, "ansible");
        assert_eq!(res.1.with.len(), 2);
        assert_eq!(res.1.with[0].0, "ansible-core");
        assert_eq!(res.1.with[1].0, "ansible-lint");
    }

    #[test]
    fn parse_uv_package_without() {
        let uv_out = "ansible v13.1.0";
        let res = parse_uv_package(uv_out);
        assert_eq!(res.0, "ansible");
        assert_eq!(res.1.with.len(), 0);
    }

    #[test]
    fn unequal_opts_ok() {
        let opts1 = UvOpts {
            from: None,
            with_lfs: false,
            py_version: None,
            with: ["a", "b", "c", "d"]
                .into_iter()
                .map(ToOwned::to_owned)
                .map(|a| (a, false))
                .collect(),
            post_hook: None,
        };

        let opts2 = UvOpts {
            from: None,
            with_lfs: false,
            py_version: None,
            with: ["a", "b", "c"]
                .into_iter()
                .map(ToOwned::to_owned)
                .map(|a| (a, false))
                .collect(),
            post_hook: None,
        };

        assert!(!extra_opts(&opts1, &opts2).with.is_empty());
    }
}
