use std::{
    collections::{BTreeMap, HashMap},
    path::{Path, PathBuf},
};
use crate::indexmap::IndexMap;
use base64::{prelude::BASE64_STANDARD, Engine};
use semver::VersionReq;
use serde_cbor::Value;
use sha2::{Digest, Sha256};
use shared_buffer::{MmapError, OwnedBuffer};
use url::Url;
use wasmer_toml::Manifest as WasmerManifest;
use crate::{
    metadata::{
        annotations::{
            Atom as AtomAnnotation, Emscripten, FileSystemMapping, FileSystemMappings,
            VolumeSpecificPath, Wapm, Wasi,
        },
        Atom, Binding, Command, Manifest as WebcManifest, UrlOrManifest, WaiBindings, WitBindings,
    },
    wasmer_package::{strictness::Strictness, Volume},
};
const METADATA_VOLUME: &str = Volume::METADATA;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ManifestError {
    #[error("The dependency, \"{_0}\", isn't in the \"namespace/name\" format")]
    InvalidDependency(String),
    #[error("Unable to serialize the \"{key}\" annotation")]
    SerializeCborAnnotation {
        key: String,
        #[source]
        error: serde_cbor::Error,
    },
    #[error("Unknown atom kind, \"{_0}\"")]
    UnknownAtomKind(String),
    #[error("Duplicate module, \"{_0}\"")]
    DuplicateModule(String),
    #[error("Unable to read the \"{module}\" module's file from \"{}\"", path.display())]
    ReadAtomFile {
        module: String,
        path: PathBuf,
        #[source]
        error: std::io::Error,
    },
    #[error("Duplicate command, \"{_0}\"")]
    DuplicateCommand(String),
    #[error("Unknown runner kind, \"{_0}\"")]
    UnknownRunnerKind(String),
    #[error("Unable to merge in user-defined \"{key}\" annotations for the \"{command}\" command")]
    #[non_exhaustive]
    MergeAnnotations {
        command: String,
        key: String,
    },
    #[error("The \"{command}\" command uses a non-existent module, \"{module}\"")]
    InvalidModuleReference {
        command: String,
        module: String,
    },
    #[error("Unable to deserialize custom annotations from the wasmer.toml manifest")]
    WasmerTomlAnnotations {
        #[source]
        error: Box<dyn std::error::Error + Send + Sync>,
    },
    #[error("\"{}\" is outside of \"{}\"", path.display(), base_dir.display())]
    OutsideBaseDirectory {
        path: PathBuf,
        base_dir: PathBuf,
    },
    #[error("The \"{}\" doesn't exist (base dir: {})", path.display(), base_dir.display())]
    MissingFile {
        path: PathBuf,
        base_dir: PathBuf,
    },
}
pub(crate) fn wasmer_manifest_to_webc(
    manifest: &WasmerManifest,
    base_dir: &Path,
    strictness: Strictness,
) -> Result<(WebcManifest, BTreeMap<String, OwnedBuffer>), ManifestError> {
    let use_map = transform_dependencies(&manifest.dependencies)?;
    let fs: IndexMap<String, PathBuf> = manifest.fs.clone().into_iter().collect();
    let package = transform_package_annotations(&manifest.package, &fs, base_dir, strictness)?;
    let (atoms, atom_files) = transform_atoms(manifest, base_dir)?;
    let commands = transform_commands(manifest, base_dir)?;
    let bindings = transform_bindings(manifest, base_dir)?;
    let manifest = WebcManifest {
        origin: None,
        use_map,
        package,
        atoms,
        commands,
        bindings,
        entrypoint: entrypoint(manifest),
    };
    Ok((manifest, atom_files))
}
fn transform_dependencies(
    original_dependencies: &HashMap<String, VersionReq>,
) -> Result<IndexMap<String, UrlOrManifest>, ManifestError> {
    let mut dependencies = IndexMap::new();
    for (dep, version) in original_dependencies {
        let (namespace, package_name) = extract_dependency_parts(dep)
            .ok_or_else(|| ManifestError::InvalidDependency(dep.clone()))?;
        let dependency_specifier =
            UrlOrManifest::RegistryDependentUrl(format!("{namespace}/{package_name}@{version}"));
        dependencies.insert(dep.clone(), dependency_specifier);
    }
    Ok(dependencies)
}
fn extract_dependency_parts(dep: &str) -> Option<(&str, &str)> {
    let (namespace, package_name) = dep.split_once('/')?;
    fn invalid_char(c: char) -> bool {
        !matches!(c, 'a'..='z' | 'A'..='Z' | '_' | '-' | '0'..='9')
    }
    if namespace.contains(invalid_char) || package_name.contains(invalid_char) {
        None
    } else {
        Some((namespace, package_name))
    }
}
type Atoms = BTreeMap<String, OwnedBuffer>;
fn transform_atoms(
    manifest: &WasmerManifest,
    base_dir: &Path,
) -> Result<(IndexMap<String, Atom>, Atoms), ManifestError> {
    let mut atom_files = BTreeMap::new();
    let mut metadata = IndexMap::new();
    for module in &manifest.modules {
        let name = &module.name;
        let path = base_dir.join(&module.source);
        let file = open_file(&path).map_err(|error| ManifestError::ReadAtomFile {
            module: name.clone(),
            path,
            error,
        })?;
        let atom = Atom {
            kind: atom_kind(module.kind.as_deref())?,
            signature: atom_signature(&file),
        };
        if metadata.contains_key(name) {
            return Err(ManifestError::DuplicateModule(name.clone()));
        }
        metadata.insert(name.clone(), atom);
        atom_files.insert(name.clone(), file);
    }
    Ok((metadata, atom_files))
}
fn atom_signature(atom: &[u8]) -> String {
    let mut hasher = Sha256::new();
    hasher.update(atom);
    let sha256 = hasher.finalize();
    let encoded = BASE64_STANDARD.encode(sha256);
    format!("sha256:{encoded}")
}
fn atom_kind(kind: Option<&str>) -> Result<Url, ManifestError> {
    const WASM_ATOM_KIND: &str = "https://webc.org/kind/wasm";
    const TENSORFLOW_SAVED_MODEL_KIND: &str = "https://webc.org/kind/tensorflow-SavedModel";
    let url = match kind {
        Some("wasm") | None => WASM_ATOM_KIND.parse().expect("Should never fail"),
        Some("tensorflow-SavedModel") => TENSORFLOW_SAVED_MODEL_KIND
            .parse()
            .expect("Should never fail"),
        Some(other) => {
            if let Ok(url) = Url::parse(other) {
                url
            } else {
                return Err(ManifestError::UnknownAtomKind(other.to_string()));
            }
        }
    };
    Ok(url)
}
fn open_file(path: &Path) -> Result<OwnedBuffer, std::io::Error> {
    match OwnedBuffer::mmap(path) {
        Ok(b) => return Ok(b),
        Err(MmapError::Map(_)) => {
            }
        Err(MmapError::FileOpen { error, .. }) => {
            return Err(error);
        }
    }
    let bytes = std::fs::read(path)?;
    Ok(OwnedBuffer::from_bytes(bytes))
}
fn transform_package_annotations(
    package: &wasmer_toml::Package,
    fs: &IndexMap<String, PathBuf>,
    base_dir: &Path,
    strictness: Strictness,
) -> Result<IndexMap<String, Value>, ManifestError> {
    let mut annotations = IndexMap::new();
    let wapm = transform_package_meta_to_annotations(package, base_dir, strictness)?;
    insert_annotation(&mut annotations, Wapm::KEY, wapm)?;
    let fs = get_fs_table(fs);
    if !fs.is_empty() {
        insert_annotation(&mut annotations, FileSystemMappings::KEY, fs)?;
    }
    Ok(annotations)
}
fn insert_annotation(
    annotations: &mut IndexMap<String, serde_cbor::Value>,
    key: impl Into<String>,
    value: impl serde::Serialize,
) -> Result<(), ManifestError> {
    let key = key.into();
    match serde_cbor::value::to_value(value) {
        Ok(value) => {
            annotations.insert(key, value);
            Ok(())
        }
        Err(error) => Err(ManifestError::SerializeCborAnnotation { key, error }),
    }
}
fn get_fs_table(fs: &IndexMap<String, PathBuf>) -> FileSystemMappings {
    if fs.is_empty() {
        return FileSystemMappings::default();
    }
    let mut entries = Vec::new();
    for (guest, host) in fs {
        let mapping = FileSystemMapping {
            from: None,
            volume_name: Volume::ASSET.to_string(),
            original_path: sanitize_path(host.display().to_string()),
            mount_path: sanitize_path(guest),
        };
        entries.push(mapping);
    }
    FileSystemMappings(entries)
}
fn sanitize_path(path: impl AsRef<Path>) -> String {
    let path = path.as_ref();
    let mut segments = Vec::new();
    for component in path.components() {
        match component {
            std::path::Component::Prefix(_) | std::path::Component::RootDir => {}
            std::path::Component::CurDir => {}
            std::path::Component::ParentDir => {
                segments.pop();
            }
            std::path::Component::Normal(segment) => {
                segments.push(segment.to_string_lossy());
            }
        }
    }
    let mut sanitized = String::new();
    for segment in segments {
        sanitized.push('/');
        sanitized.push_str(&segment);
    }
    if sanitized.is_empty() {
        sanitized.push('/');
    }
    sanitized
}
fn transform_package_meta_to_annotations(
    package: &wasmer_toml::Package,
    base_dir: &Path,
    strictness: Strictness,
) -> Result<Wapm, ManifestError> {
    let mut wapm = Wapm::new(
        &package.name,
        package.version.to_string(),
        &package.description,
    );
    fn metadata_file(
        path: Option<&Path>,
        base_dir: &Path,
        strictness: Strictness,
    ) -> Result<Option<VolumeSpecificPath>, ManifestError> {
        let path = match path {
            Some(p) => p,
            None => return Ok(None),
        };
        let absolute_path = base_dir.join(path);
        if !absolute_path.exists() {
            match strictness.missing_file(path, base_dir) {
                Ok(_) => return Ok(None),
                Err(e) => {
                    return Err(e);
                }
            }
        }
        match base_dir.join(path).strip_prefix(base_dir) {
            Ok(without_prefix) => Ok(Some(VolumeSpecificPath {
                volume: METADATA_VOLUME.to_string(),
                path: sanitize_path(without_prefix),
            })),
            Err(_) => match strictness.outside_base_directory(path, base_dir) {
                Ok(_) => Ok(None),
                Err(e) => Err(e),
            },
        }
    }
    wapm.license = package.license.clone();
    wapm.license_file = metadata_file(package.license_file.as_deref(), base_dir, strictness)?;
    wapm.readme = metadata_file(package.readme.as_deref(), base_dir, strictness)?;
    wapm.repository = package.repository.clone();
    wapm.homepage = package.homepage.clone();
    wapm.private = package.private;
    Ok(wapm)
}
fn transform_commands(
    manifest: &WasmerManifest,
    base_dir: &Path,
) -> Result<IndexMap<String, Command>, ManifestError> {
    let mut commands = IndexMap::new();
    for command in &manifest.commands {
        let cmd = match command {
            wasmer_toml::Command::V1(cmd) => transform_command_v1(cmd, manifest)?,
            wasmer_toml::Command::V2(cmd) => transform_command_v2(cmd, base_dir)?,
        };
        match commands.entry(command.get_name().to_string()) {
            indexmap::map::Entry::Occupied(_) => {
                return Err(ManifestError::DuplicateCommand(
                    command.get_name().to_string(),
                ));
            }
            indexmap::map::Entry::Vacant(entry) => {
                entry.insert(cmd);
            }
        }
    }
    Ok(commands)
}
fn transform_command_v1(
    cmd: &wasmer_toml::CommandV1,
    manifest: &WasmerManifest,
) -> Result<Command, ManifestError> {
    let runner = match &cmd.module {
        wasmer_toml::ModuleReference::CurrentPackage { module } => {
            let module = manifest
                .modules
                .iter()
                .find(|m| m.name == module.as_str())
                .ok_or_else(|| ManifestError::InvalidModuleReference {
                    command: cmd.name.clone(),
                    module: cmd.module.to_string(),
                })?;
            RunnerKind::from_name(module.abi.to_str())?
        }
        wasmer_toml::ModuleReference::Dependency { .. } => {
            RunnerKind::Wasi
        }
    };
    let mut annotations = IndexMap::new();
    let main_args = cmd
        .main_args
        .as_deref()
        .map(|args| args.split_whitespace().map(String::from).collect());
    runner.runner_specific_annotations(
        &mut annotations,
        &cmd.module,
        cmd.package.clone(),
        main_args,
    )?;
    Ok(Command {
        runner: runner.uri().to_string(),
        annotations,
    })
}
fn transform_command_v2(
    cmd: &wasmer_toml::CommandV2,
    base_dir: &Path,
) -> Result<Command, ManifestError> {
    let runner = RunnerKind::from_name(&cmd.runner)?;
    let mut annotations = IndexMap::new();
    runner.runner_specific_annotations(&mut annotations, &cmd.module, None, None)?;
    let custom_annotations =
        cmd.get_annotations(base_dir)
            .map_err(|error| ManifestError::WasmerTomlAnnotations {
                error: error.into(),
            })?;
    if let Some(serde_cbor::Value::Map(custom_annotations)) = custom_annotations {
        for (key, value) in custom_annotations {
            if let serde_cbor::Value::Text(key) = key {
                match annotations.entry(key) {
                    indexmap::map::Entry::Occupied(mut entry) => {
                        merge_cbor(entry.get_mut(), value).map_err(|_| {
                            ManifestError::MergeAnnotations {
                                command: cmd.name.clone(),
                                key: entry.key().clone(),
                            }
                        })?;
                    }
                    indexmap::map::Entry::Vacant(entry) => {
                        entry.insert(value);
                    }
                }
            }
        }
    }
    Ok(Command {
        runner: runner.uri().to_string(),
        annotations,
    })
}
fn merge_cbor(original: &mut Value, addition: Value) -> Result<(), ()> {
    match (original, addition) {
        (Value::Map(left), Value::Map(right)) => {
            for (k, v) in right {
                match left.entry(k) {
                    std::collections::btree_map::Entry::Vacant(entry) => {
                        entry.insert(v);
                    }
                    std::collections::btree_map::Entry::Occupied(mut entry) => {
                        merge_cbor(entry.get_mut(), v)?;
                    }
                }
            }
        }
        (Value::Array(left), Value::Array(right)) => {
            left.extend(right);
        }
        (Value::Bool(left), Value::Bool(right)) if *left == right => {}
        (Value::Bytes(left), Value::Bytes(right)) if *left == right => {}
        (Value::Float(left), Value::Float(right)) if *left == right => {}
        (Value::Integer(left), Value::Integer(right)) if *left == right => {}
        (Value::Text(left), Value::Text(right)) if *left == right => {}
        (original @ Value::Null, value) => {
            *original = value;
        }
        (_original, Value::Null) => {}
        (_left, _right) => {
            return Err(());
        }
    }
    Ok(())
}
#[derive(Debug, Clone, PartialEq)]
enum RunnerKind {
    Wasi,
    Wcgi,
    Emscripten,
    Wasm4,
    Other(Url),
}
impl RunnerKind {
    fn from_name(name: &str) -> Result<Self, ManifestError> {
        match name {
            "wasi" | "wasi@unstable_" | crate::metadata::annotations::WASI_RUNNER_URI => {
                Ok(RunnerKind::Wasi)
            }
            "generic" => {
                Ok(RunnerKind::Wasi)
            }
            "wcgi" | crate::metadata::annotations::WCGI_RUNNER_URI => Ok(RunnerKind::Wcgi),
            "emscripten" | crate::metadata::annotations::EMSCRIPTEN_RUNNER_URI => {
                Ok(RunnerKind::Emscripten)
            }
            "wasm4" | crate::metadata::annotations::WASM4_RUNNER_URI => Ok(RunnerKind::Wasm4),
            other => {
                if let Ok(other) = Url::parse(other) {
                    Ok(RunnerKind::Other(other))
                } else if let Ok(other) = format!("https://webc.org/runner/{other}").parse() {
                    Ok(RunnerKind::Other(other))
                } else {
                    Err(ManifestError::UnknownRunnerKind(other.to_string()))
                }
            }
        }
    }
    fn uri(&self) -> &str {
        match self {
            RunnerKind::Wasi => crate::metadata::annotations::WASI_RUNNER_URI,
            RunnerKind::Wcgi => crate::metadata::annotations::WCGI_RUNNER_URI,
            RunnerKind::Emscripten => crate::metadata::annotations::EMSCRIPTEN_RUNNER_URI,
            RunnerKind::Wasm4 => crate::metadata::annotations::WASM4_RUNNER_URI,
            RunnerKind::Other(other) => other.as_str(),
        }
    }
    #[allow(deprecated)]
    fn runner_specific_annotations(
        &self,
        annotations: &mut IndexMap<String, Value>,
        module: &wasmer_toml::ModuleReference,
        package: Option<String>,
        main_args: Option<Vec<String>>,
    ) -> Result<(), ManifestError> {
        let atom_annotation = match module {
            wasmer_toml::ModuleReference::CurrentPackage { module } => {
                AtomAnnotation::new(module, None)
            }
            wasmer_toml::ModuleReference::Dependency { dependency, module } => {
                AtomAnnotation::new(module, dependency.to_string())
            }
        };
        insert_annotation(annotations, AtomAnnotation::KEY, atom_annotation)?;
        match self {
            RunnerKind::Wasi | RunnerKind::Wcgi => {
                let mut wasi = Wasi::new(module.to_string());
                wasi.main_args = main_args;
                wasi.package = package;
                insert_annotation(annotations, Wasi::KEY, wasi)?;
            }
            RunnerKind::Emscripten => {
                let emscripten = Emscripten {
                    atom: Some(module.to_string()),
                    package,
                    env: None,
                    main_args,
                    mount_atom_in_volume: None,
                };
                insert_annotation(annotations, Emscripten::KEY, emscripten)?;
            }
            RunnerKind::Wasm4 | RunnerKind::Other(_) => {
                }
        }
        Ok(())
    }
}
fn entrypoint(manifest: &WasmerManifest) -> Option<String> {
    if let Some(entrypoint) = &manifest.package.entrypoint {
        return Some(entrypoint.clone());
    }
    if let [only_command] = manifest.commands.as_slice() {
        return Some(only_command.get_name().to_string());
    }
    None
}
fn transform_bindings(
    manifest: &WasmerManifest,
    base_dir: &Path,
) -> Result<Vec<Binding>, ManifestError> {
    let mut bindings = Vec::new();
    for module in &manifest.modules {
        let b = match &module.bindings {
            Some(wasmer_toml::Bindings::Wit(wit)) => transform_wit_bindings(wit, module, base_dir)?,
            Some(wasmer_toml::Bindings::Wai(wai)) => transform_wai_bindings(wai, module, base_dir)?,
            None => continue,
        };
        bindings.push(b);
    }
    Ok(bindings)
}
fn transform_wai_bindings(
    wai: &wasmer_toml::WaiBindings,
    module: &wasmer_toml::Module,
    base_dir: &Path,
) -> Result<Binding, ManifestError> {
    let wasmer_toml::WaiBindings {
        wai_version,
        exports,
        imports,
    } = wai;
    let bindings = WaiBindings {
        exports: exports
            .as_deref()
            .map(|path| metadata_volume_uri(path, base_dir))
            .transpose()?,
        module: module.name.clone(),
        imports: imports
            .iter()
            .map(|path| metadata_volume_uri(path, base_dir))
            .collect::<Result<Vec<_>, ManifestError>>()?,
    };
    let mut annotations = IndexMap::new();
    insert_annotation(&mut annotations, "wai", bindings)?;
    Ok(Binding {
        name: "library-bindings".to_string(),
        kind: format!("wai@{wai_version}"),
        annotations: Value::Map(
            annotations
                .into_iter()
                .map(|(k, v)| (Value::Text(k), v))
                .collect(),
        ),
    })
}
fn metadata_volume_uri(path: &Path, base_dir: &Path) -> Result<String, ManifestError> {
    make_relative_path(path, base_dir)
        .map(sanitize_path)
        .map(|p| format!("{METADATA_VOLUME}:/{p}"))
}
fn transform_wit_bindings(
    wit: &wasmer_toml::WitBindings,
    module: &wasmer_toml::Module,
    base_dir: &Path,
) -> Result<Binding, ManifestError> {
    let wasmer_toml::WitBindings {
        wit_bindgen,
        wit_exports,
    } = wit;
    let bindings = WitBindings {
        exports: make_relative_path(wit_exports, base_dir)
            .and_then(|path| metadata_volume_uri(&path, base_dir))?,
        module: module.name.clone(),
    };
    let mut annotations = IndexMap::new();
    insert_annotation(&mut annotations, "wit", bindings)?;
    Ok(Binding {
        name: "library-bindings".to_string(),
        kind: format!("wit@{wit_bindgen}"),
        annotations: Value::Map(
            annotations
                .into_iter()
                .map(|(k, v)| (Value::Text(k), v))
                .collect(),
        ),
    })
}
fn make_relative_path(path: &Path, base_dir: &Path) -> Result<PathBuf, ManifestError> {
    let absolute_path = base_dir.join(path);
    match absolute_path.strip_prefix(base_dir) {
        Ok(p) => Ok(p.into()),
        Err(_) => Err(ManifestError::OutsideBaseDirectory {
            path: absolute_path,
            base_dir: base_dir.to_path_buf(),
        }),
    }
}
#[cfg(test)]
mod tests {
    use crate::metadata::annotations::Wasi;
    use tempfile::TempDir;
    use super::*;
    #[test]
    fn custom_annotations_are_copied_across_verbatim() {
        let temp = TempDir::new().unwrap();
        let wasmer_toml = r#"
        [package]
        name = "test"
        version = "0.0.0"
        description = "asdf"
        [[module]]
        name = "module"
        source = "file.wasm"
        abi = "wasi"
        [[command]]
        name = "command"
        module = "module"
        runner = "asdf"
        annotations = { first = 42, second = ["a", "b"] }
        "#;
        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
        std::fs::write(temp.path().join("file.wasm"), b"\0asm...").unwrap();
        let (transformed, _) =
            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
        let command = &transformed.commands["command"];
        assert_eq!(command.annotation::<u32>("first").unwrap(), Some(42));
        assert_eq!(command.annotation::<String>("non-existent").unwrap(), None);
        insta::with_settings! {
            { description => wasmer_toml },
            { insta::assert_yaml_snapshot!(&transformed); }
        }
    }
    #[test]
    fn transform_empty_manifest() {
        let temp = TempDir::new().unwrap();
        let wasmer_toml = r#"
            [package]
            name = "some/package"
            version = "0.0.0"
            description = "My awesome package"
        "#;
        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
        let (transformed, atoms) =
            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
        assert!(atoms.is_empty());
        insta::with_settings! {
            { description => wasmer_toml },
            { insta::assert_yaml_snapshot!(&transformed); }
        }
    }
    #[test]
    fn transform_manifest_with_single_atom() {
        let temp = TempDir::new().unwrap();
        let wasmer_toml = r#"
            [package]
            name = "some/package"
            version = "0.0.0"
            description = "My awesome package"
            [[module]]
            name = "first"
            source = "./path/to/file.wasm"
            abi = "wasi"
        "#;
        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
        let dir = temp.path().join("path").join("to");
        std::fs::create_dir_all(&dir).unwrap();
        std::fs::write(dir.join("file.wasm"), b"\0asm...").unwrap();
        let (transformed, atoms) =
            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
        assert_eq!(atoms.len(), 1);
        assert_eq!(atoms["first"].as_slice(), b"\0asm...");
        insta::with_settings! {
            { description => wasmer_toml },
            { insta::assert_yaml_snapshot!(&transformed); }
        }
    }
    #[test]
    fn transform_manifest_with_atom_and_command() {
        let temp = TempDir::new().unwrap();
        let wasmer_toml = r#"
            [package]
            name = "some/package"
            version = "0.0.0"
            description = "My awesome package"
            [[module]]
            name = "cpython"
            source = "python.wasm"
            abi = "wasi"
            [[command]]
            name = "python"
            module = "cpython"
            runner = "wasi"
        "#;
        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
        std::fs::write(temp.path().join("python.wasm"), b"\0asm...").unwrap();
        let (transformed, _) =
            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
        assert_eq!(transformed.commands.len(), 1);
        let python = &transformed.commands["python"];
        assert_eq!(
            &python.runner,
            crate::metadata::annotations::WASI_RUNNER_URI
        );
        assert_eq!(python.wasi().unwrap().unwrap(), Wasi::new("cpython"));
        insta::with_settings! {
            { description => wasmer_toml },
            { insta::assert_yaml_snapshot!(&transformed); }
        }
    }
    #[test]
    fn transform_manifest_with_multiple_commands() {
        let temp = TempDir::new().unwrap();
        let wasmer_toml = r#"
            [package]
            name = "some/package"
            version = "0.0.0"
            description = "My awesome package"
            [[module]]
            name = "cpython"
            source = "python.wasm"
            abi = "wasi"
            [[command]]
            name = "first"
            module = "cpython"
            runner = "wasi"
            [[command]]
            name = "second"
            module = "cpython"
            runner = "wasi"
            [[command]]
            name = "third"
            module = "cpython"
            runner = "wasi"
        "#;
        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
        std::fs::write(temp.path().join("python.wasm"), b"\0asm...").unwrap();
        let (transformed, _) =
            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
        assert_eq!(transformed.commands.len(), 3);
        assert!(transformed.commands.contains_key("first"));
        assert!(transformed.commands.contains_key("second"));
        assert!(transformed.commands.contains_key("third"));
        insta::with_settings! {
            { description => wasmer_toml },
            { insta::assert_yaml_snapshot!(&transformed); }
        }
    }
    #[test]
    fn merge_custom_attributes_with_builtin_ones() {
        let temp = TempDir::new().unwrap();
        let wasmer_toml = r#"
            [package]
            name = "some/package"
            version = "0.0.0"
            description = "My awesome package"
            [[module]]
            name = "cpython"
            source = "python.wasm"
            abi = "wasi"
            [[command]]
            name = "python"
            module = "cpython"
            runner = "wasi"
            annotations = { wasi = { env = ["KEY=val"]} }
        "#;
        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
        std::fs::write(temp.path().join("python.wasm"), b"\0asm...").unwrap();
        let (transformed, _) =
            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
        assert_eq!(transformed.commands.len(), 1);
        let cmd = &transformed.commands["python"];
        assert_eq!(
            &cmd.wasi().unwrap().unwrap(),
            Wasi::new("cpython").with_env("KEY", "val")
        );
        insta::with_settings! {
            { description => wasmer_toml },
            { insta::assert_yaml_snapshot!(&transformed); }
        }
    }
    #[test]
    fn transform_bash_manifest() {
        let temp = TempDir::new().unwrap();
        let wasmer_toml = r#"
            [package]
            name = "sharrattj/bash"
            version = "1.0.17"
            description = "Bash is a modern POSIX-compliant implementation of /bin/sh."
            license = "GNU"
            wasmer-extra-flags = "--enable-threads --enable-bulk-memory"
            [dependencies]
            "sharrattj/coreutils" = "1.0.16"
            [[module]]
            name = "bash"
            source = "bash.wasm"
            abi = "wasi"
            [[command]]
            name = "bash"
            module = "bash"
            runner = "wasi@unstable_"
        "#;
        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
        std::fs::write(temp.path().join("bash.wasm"), b"\0asm...").unwrap();
        let (transformed, _) =
            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
        insta::with_settings! {
            { description => wasmer_toml },
            { insta::assert_yaml_snapshot!(&transformed); }
        }
    }
    #[test]
    fn transform_wasmer_pack_manifest() {
        let temp = TempDir::new().unwrap();
        let wasmer_toml = r#"
            [package]
            name = "wasmer/wasmer-pack"
            version = "0.7.0"
            description = "The WebAssembly interface to wasmer-pack."
            license = "MIT"
            readme = "README.md"
            repository = "https://github.com/wasmerio/wasmer-pack"
            homepage = "https://wasmer.io/"
            [[module]]
            name = "wasmer-pack-wasm"
            source = "wasmer_pack_wasm.wasm"
            [module.bindings]
            wai-version = "0.2.0"
            exports = "wasmer-pack.exports.wai"
            imports = []
        "#;
        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
        std::fs::write(temp.path().join("wasmer_pack_wasm.wasm"), b"\0asm...").unwrap();
        std::fs::write(temp.path().join("wasmer-pack.exports.wai"), b"").unwrap();
        std::fs::write(temp.path().join("README.md"), b"").unwrap();
        let (transformed, _) =
            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
        insta::with_settings! {
            { description => wasmer_toml },
            { insta::assert_yaml_snapshot!(&transformed); }
        }
    }
    #[test]
    fn transform_python_manifest() {
        let temp = TempDir::new().unwrap();
        let wasmer_toml = r#"
            [package]
            name = "python"
            version = "0.1.0"
            description = "Python is an interpreted, high-level, general-purpose programming language"
            license = "ISC"
            repository = "https://github.com/wapm-packages/python"
            [[module]]
            name = "python"
            source = "bin/python.wasm"
            abi = "wasi"
            [module.interfaces]
            wasi = "0.0.0-unstable"
            [[command]]
            name = "python"
            module = "python"
            [fs]
            lib = "lib"
        "#;
        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
        let bin = temp.path().join("bin");
        std::fs::create_dir_all(&bin).unwrap();
        std::fs::write(bin.join("python.wasm"), b"\0asm...").unwrap();
        let (transformed, _) =
            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
        insta::with_settings! {
            { description => wasmer_toml },
            { insta::assert_yaml_snapshot!(&transformed); }
        }
    }
    #[test]
    fn transform_manifest_with_fs_table() {
        let temp = TempDir::new().unwrap();
        let wasmer_toml = r#"
            [package]
            name = "some/package"
            version = "0.0.0"
            description = "This is a package"
            [fs]
            lib = "lib"
            "/public" = "./out"
        "#;
        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
        std::fs::write(temp.path().join("python.wasm"), b"\0asm...").unwrap();
        let (transformed, _) =
            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
        let fs = transformed.filesystem().unwrap().unwrap();
        assert_eq!(
            fs,
            [
                FileSystemMapping {
                    from: None,
                    volume_name: "atom".to_string(),
                    original_path: "/lib".to_string(),
                    mount_path: "/lib".to_string(),
                },
                FileSystemMapping {
                    from: None,
                    volume_name: "atom".to_string(),
                    original_path: "/out".to_string(),
                    mount_path: "/public".to_string(),
                }
            ]
        );
        insta::with_settings! {
            { description => wasmer_toml },
            { insta::assert_yaml_snapshot!(&transformed); }
        }
    }
    #[test]
    fn issue_124_command_runner_is_swallowed() {
        let temp = TempDir::new().unwrap();
        let wasmer_toml = r#"
            [package]
            name = "wasmer-tests/wcgi-always-panic"
            version = "0.1.0"
            description = "wasmer-tests/wcgi-always-panic website"
            [[module]]
            name = "wcgi-always-panic"
            source = "./wcgi-always-panic.wasm"
            abi = "wasi"
            [[command]]
            name = "wcgi"
            module = "wcgi-always-panic"
            runner = "https://webc.org/runner/wcgi"
        "#;
        let manifest: WasmerManifest = toml::from_str(wasmer_toml).unwrap();
        std::fs::write(temp.path().join("wcgi-always-panic.wasm"), b"\0asm...").unwrap();
        let (transformed, _) =
            wasmer_manifest_to_webc(&manifest, temp.path(), Strictness::Strict).unwrap();
        let cmd = &transformed.commands["wcgi"];
        assert_eq!(cmd.runner, crate::metadata::annotations::WCGI_RUNNER_URI);
        assert_eq!(cmd.wasi().unwrap().unwrap(), Wasi::new("wcgi-always-panic"));
        insta::with_settings! {
            { description => wasmer_toml },
            { insta::assert_yaml_snapshot!(&transformed); }
        }
    }
}