/*
    Copyright: 2020 Rasmus Thomsen <oss@cogitri.dev>
    SPDX-License-Identifier: GPL-3.0-or-later
*/

use crate::apk_package_owned::ApkPackageOwned;
use apk_tools::i18n::i18n_f;
use apk_tools::{ApkDatabase, ApkOwnedPackages, ApkRepository};
use zbus::{self, dbus_interface, fdo, MessageHeader};

pub struct DBusServer {
    allow_untrusted_repos: bool,
    additional_repo_uri: Option<String>,
    root: Option<String>,
}

impl Default for DBusServer {
    fn default() -> Self {
        Self {
            allow_untrusted_repos: false,
            additional_repo_uri: None,
            root: None,
        }
    }
}

/// Wraps a struct, so we can turn (s) into ((s)) in the DBus API.
/// GVariant expects single structs to be wrapped into  another layer of tuples.
#[derive(serde::Serialize, zvariant_derive::Type)]
pub struct ApkPackageOwnedWrapper {
    pub inner: ApkPackageOwned,
}

impl DBusServer {
    #[cfg(test)]
    pub fn new(
        allow_untrusted_repos: bool,
        additional_repo_uri: Option<String>,
        root: Option<String>,
    ) -> Self {
        Self {
            allow_untrusted_repos,
            additional_repo_uri,
            root,
        }
    }

    #[cfg(not(test))]
    fn check_polkit_auth(
        method_name: &str,
        header: &MessageHeader,
    ) -> Result<(), zbus::fdo::Error> {
        use std::collections::HashMap;
        use zbus::Connection;
        use zbus_polkit::policykit1::{AuthorityProxy, Subject};

        let connection = Connection::new_system()?;
        let proxy = AuthorityProxy::new(&connection)?;
        let subject = match Subject::new_for_message_header(header) {
            Ok(o) => o,
            Err(e) => {
                return Err(zbus::fdo::Error::AuthFailed(i18n_f(
                    "Failed to determine subject due to error {}",
                    &[&e.to_string()],
                )))
            }
        };
        let mut check_auth_details = HashMap::new();
        check_auth_details.insert("polkit.gettext_domain", "apk-polkit-rs");
        let res = proxy
            .check_authorization(
                &subject,
                &format!("dev.Cogitri.apkPolkit1.{}", method_name),
                check_auth_details,
                enumflags2::BitFlags::empty(),
                "",
            )
            .map_err(|e| {
                zbus::fdo::Error::AuthFailed(i18n_f(
                    "Failed to query for authentication due to error {}",
                    &[&e.to_string()],
                ))
            })?;
        if !res.is_authorized {
            Err(zbus::fdo::Error::AuthFailed(i18n_f(
                "Access for operation {} was denied",
                &[method_name],
            )))
        } else {
            Ok(())
        }
    }

    #[cfg(test)]
    fn check_polkit_auth(
        method_name: &str,
        header: &MessageHeader,
    ) -> Result<(), zbus::fdo::Error> {
        Ok(())
    }
}

#[dbus_interface(name = "dev.Cogitri.apkPolkit1")]
impl DBusServer {
    fn add_package(&self, #[zbus(header)] header: MessageHeader, pkgname: &str) -> fdo::Result<()> {
        DBusServer::check_polkit_auth("AddPackage", &header)?;
        let mut db = ApkDatabase::new(
            false,
            self.additional_repo_uri.as_deref(),
            self.root.as_deref(),
        )?;
        #[cfg(test)]
        db.no_extract_chown();
        db.add_package(pkgname)?;
        Ok(())
    }

    fn add_repository(&self, #[zbus(header)] header: MessageHeader, url: &str) -> fdo::Result<()> {
        DBusServer::check_polkit_auth("AddRepository", &header)?;
        let mut repos = ApkDatabase::get_repositories(self.root.as_deref())?;
        if repos
            .iter()
            .position(|r| r.url == url && r.enabled)
            .is_none()
        {
            repos.push(ApkRepository::new(true, None, url.to_string()));
            ApkDatabase::set_repositories(&repos, self.root.as_deref())?;
        }
        Ok(())
    }

    fn delete_package(
        &self,
        #[zbus(header)] header: MessageHeader,
        pkgname: &str,
    ) -> fdo::Result<()> {
        DBusServer::check_polkit_auth("DeletePackage", &header)?;
        let mut db = ApkDatabase::new(
            false,
            self.additional_repo_uri.as_deref(),
            self.root.as_deref(),
        )?;
        db.delete_package(pkgname)?;
        Ok(())
    }

    fn get_package_details(&self, pkgname: &str) -> fdo::Result<ApkPackageOwnedWrapper> {
        let mut db = ApkDatabase::new(
            false,
            self.additional_repo_uri.as_deref(),
            self.root.as_deref(),
        )?;
        Ok(ApkPackageOwnedWrapper {
            inner: db.get_package_details(pkgname)?.into(),
        })
    }

    fn list_available_packages(&self) -> fdo::Result<ApkOwnedPackages> {
        let db = ApkDatabase::new(
            true,
            self.additional_repo_uri.as_deref(),
            self.root.as_deref(),
        )?;
        let pkgs = db.list_available_packages();
        Ok(pkgs)
    }

    fn list_repositories(&self) -> fdo::Result<Vec<ApkRepository>> {
        Ok(ApkDatabase::get_repositories(self.root.as_deref())?)
    }

    fn list_upgradable_packages(&self) -> fdo::Result<ApkOwnedPackages> {
        let db = ApkDatabase::new(
            true,
            self.additional_repo_uri.as_deref(),
            self.root.as_deref(),
        )?;
        let pkgs = db.list_upgradable_packages()?;
        Ok(pkgs)
    }

    fn remove_repository(
        &self,
        #[zbus(header)] header: MessageHeader,
        url: &str,
    ) -> fdo::Result<()> {
        DBusServer::check_polkit_auth("RemoveRepository", &header)?;
        let mut repos = ApkDatabase::get_repositories(self.root.as_deref())?;
        repos.retain(|r| r.url != url);
        ApkDatabase::set_repositories(&repos, self.root.as_deref())?;
        Ok(())
    }

    fn search_file_owner(&self, path: &str) -> fdo::Result<ApkPackageOwnedWrapper> {
        let mut db = ApkDatabase::new(
            true,
            self.additional_repo_uri.as_deref(),
            self.root.as_deref(),
        )?;
        match db.search_file_owner(path) {
            Some(pkg) => Ok(ApkPackageOwnedWrapper { inner: pkg.into() }),
            None => Err(fdo::Error::Failed(i18n_f(
                "Couldn't find package owning path {}",
                &[path],
            ))),
        }
    }

    fn update_repositories(&self, #[zbus(header)] header: MessageHeader) -> fdo::Result<()> {
        DBusServer::check_polkit_auth("UpdateRepositories", &header)?;
        let mut db = ApkDatabase::new(
            false,
            self.additional_repo_uri.as_deref(),
            self.root.as_deref(),
        )?;
        db.update_repositories(self.allow_untrusted_repos)?;
        Ok(())
    }

    fn upgrade_package(
        &self,
        #[zbus(header)] header: MessageHeader,
        pkgname: &str,
    ) -> fdo::Result<()> {
        DBusServer::check_polkit_auth("UpgradePackage", &header)?;
        let mut db = ApkDatabase::new(
            false,
            self.additional_repo_uri.as_deref(),
            self.root.as_deref(),
        )?;
        #[cfg(test)]
        db.no_extract_chown();
        db.upgrade_package(pkgname)?;
        Ok(())
    }
}

#[cfg(test)]
mod test {
    use super::DBusServer;
    use crate::apk_package_owned::ApkPackageOwned;
    use apk_tools::{ApkRepository, PackageState};
    use std::env::var;
    use std::io::Write;
    use std::path::Path;
    use std::process::Command;
    use std::sync::Once;
    use zbus::*;

    static INIT_LOGGER: Once = Once::new();

    fn run_fs_test<T>(test_fn: T)
    where
        T: FnOnce(String, String) -> () + std::panic::UnwindSafe,
    {
        INIT_LOGGER.call_once(|| {
            fern::Dispatch::new()
                .format(move |out, message, record| {
                    out.finish(format_args!("[{}] {}", record.level(), message,))
                })
                .chain(std::io::stdout())
                .apply()
                .unwrap();
        });

        let temp_dir = tempfile::tempdir().unwrap();
        let repo_build_script_path = var("REPO_BUILD_SCRIPT_PATH")
            .expect("REPO_BUILD_SCRIPT_PATH must be set! Are you running tests via meson?..");
        let apk_bin_path = var("APK_BIN_PATH")
            .expect("APK_BIN_PATH must be set! Are you running tests via meson?...");
        let apk_root_dir = temp_dir.as_ref().join("apk_root");
        let abuild_build_dir = temp_dir.as_ref().join("abuild_root");

        let abuild_cmd = Command::new(repo_build_script_path)
            .args(&[&apk_root_dir, &abuild_build_dir])
            .env("APK", apk_bin_path)
            .output()
            .expect("Failed to execute abuild!");

        if !abuild_cmd.status.success() {
            println!("Running abuild failed!");
            std::io::stdout().write(&abuild_cmd.stdout).unwrap();
            std::io::stdout().write(&abuild_cmd.stderr).unwrap();
            panic!("Running abuild failed!");
        }

        unsafe { apk_tools_sys::apk_flags = apk_tools_sys::APK_ALLOW_UNTRUSTED };

        let result = std::panic::catch_unwind(move || {
            test_fn(
                apk_root_dir.to_str().unwrap().to_string(),
                abuild_build_dir
                    .join("abuilds")
                    .to_str()
                    .unwrap()
                    .to_string(),
            );
        });

        temp_dir.close().unwrap();

        assert!(result.is_ok());
    }

    fn build_additional_package(pkgname: &str, repo_dir: &str, root_dir: &str) {
        let repo_path = Path::new(repo_dir).join(pkgname);

        let apk_bin_path = var("APK_BIN_PATH")
            .expect("APK_BIN_PATH must be set! Are you running tests via meson?...");
        //    APK="$APK --allow-untrusted --root $1" SUDO_APK="abuild-apk --root $1" REPODEST="$2"
        let abuild_cmd = Command::new("abuild")
            .args(&[
                "-F",
                "clean",
                "unpack",
                "prepare",
                "build",
                "rootpkg",
                "update_abuildrepo_index",
            ])
            .env(
                "APK",
                format!("{} --allow-untrusted --root {}", apk_bin_path, root_dir),
            )
            .env("SUDO_APK", format!("abuild-apk --root {}", root_dir))
            .env("REPODEST", &Path::new(repo_dir).parent().unwrap())
            .env(
                "ABUILD_USERDIR",
                &Path::new(repo_dir).parent().unwrap().join("abuildUserDir"),
            )
            .current_dir(repo_path)
            .output()
            .expect("Failed to execute abuild!");

        if !abuild_cmd.status.success() {
            println!("Running abuild failed!");
            std::io::stdout().write_all(&abuild_cmd.stdout).unwrap();
            std::io::stdout().write_all(&abuild_cmd.stderr).unwrap();
            panic!("Running abuild failed!");
        }
    }

    fn get_message_header() -> MessageHeader<'static> {
        MessageHeader::new(
            MessagePrimaryHeader::new(MessageType::MethodCall, 0),
            MessageFields::new(),
        )
    }

    #[test]
    fn test_add() {
        run_fs_test(|root, repo| {
            let server = DBusServer::new(true, Some(repo), Some(root.clone()));
            server.add_package(get_message_header(), "test-a").unwrap();
            let output = Command::new(format!("{}/usr/bin/test-a", root)).output().expect("Running test-a executable failed; apparently installing the test-a package didn't work!");
            assert!(output.status.success());
            assert_eq!(output.stdout, b"hello from test-a-1.0\n");
        });
    }

    #[test]
    fn test_add_unknown_package() {
        run_fs_test(|root, repo| {
            let server = DBusServer::new(true, Some(repo), Some(root.clone()));
            assert!(server.add_package(get_message_header(), "unknown").is_err());
        });
    }

    #[test]
    fn test_delete() {
        run_fs_test(|root, repo| {
            let server = DBusServer::new(true, Some(repo), Some(root.clone()));
            server.add_package(get_message_header(), "test-a").unwrap();
            let exe_path = std::path::Path::new(&root)
                .join("usr")
                .join("bin")
                .join("test-a");
            assert!(exe_path.exists());
            server
                .delete_package(get_message_header(), "test-a")
                .unwrap();
            assert!(!exe_path.exists());
        });
    }

    #[test]
    fn test_delete_unknown_package() {
        run_fs_test(|root, repo| {
            let server = DBusServer::new(true, Some(repo), Some(root.clone()));
            server
                .delete_package(get_message_header(), "test-a")
                .unwrap();
            let exe_path = std::path::Path::new(&root)
                .join("usr")
                .join("bin")
                .join("test-a");
            assert!(!exe_path.exists());
        });
    }

    #[test]
    fn test_repo_add() {
        use std::fs::{create_dir_all, File};
        use std::io::Write;
        use std::path::Path;

        run_fs_test(|root, repo| {
            let server = DBusServer::new(true, Some(repo), Some(root.clone()));
            let repo_file_path = Path::new(&root)
                .join("etc")
                .join("apk")
                .join("repositories");
            create_dir_all(repo_file_path.parent().unwrap()).unwrap();
            let mut repo_file = File::create(&repo_file_path).unwrap();
            repo_file.write(b"https://alpine.global.ssl.fastly.net/alpine/edge/main\n#https://alpine.global.ssl.fastly.net/alpine/edge/testing\nhttps://alpine.global.ssl.fastly.net/alpine/edge/community\n").unwrap();
            server
                .add_repository(
                    get_message_header(),
                    "https://alpine.global.ssl.fastly.net/alpine/edge/testing",
                )
                .unwrap();
            let repos = server.list_repositories().unwrap();
            let repo_file_path_s = repo_file_path.to_str().map(|s| s.to_string());
            assert!(
                repos[0]
                    == ApkRepository::new(
                        true,
                        repo_file_path_s.clone(),
                        "https://alpine.global.ssl.fastly.net/alpine/edge/main".to_string(),
                    )
            );
            assert!(
                repos[1]
                    == ApkRepository::new(
                        false,
                        repo_file_path_s.clone(),
                        "https://alpine.global.ssl.fastly.net/alpine/edge/testing".to_string(),
                    ),
            );
            assert!(
                repos[2]
                    == ApkRepository::new(
                        true,
                        repo_file_path_s.clone(),
                        "https://alpine.global.ssl.fastly.net/alpine/edge/community".to_string(),
                    ),
            );
            assert!(
                repos[3]
                    == ApkRepository::new(
                        true,
                        repo_file_path_s.clone(),
                        "https://alpine.global.ssl.fastly.net/alpine/edge/testing".to_string(),
                    ),
            );
        });
    }

    #[test]
    fn test_list_available_packages() {
        run_fs_test(|root, repo| {
            let server = DBusServer::new(true, Some(repo), Some(root.clone()));
            let mut packages: Vec<ApkPackageOwned> = server
                .list_available_packages()
                .unwrap()
                .packages
                .into_iter()
                .map(|p| p.into())
                .collect();
            packages.sort();
            assert_eq!(
                packages[0],
                ApkPackageOwned {
                    name: "test-a".to_string(),
                    new_version: "1.0-r0".to_string(),
                    description: "Package A for apk-tools testsuite".to_string(),
                    license: "GPL".to_string(),
                    old_version: "".to_string(),
                    url: "http://alpinelinux.org".to_string(),
                    installed_size: 4096,
                    size: 1250,
                    package_state: PackageState::Available,
                },
            );
            assert_eq!(
                packages[1],
                ApkPackageOwned {
                    name: "test-a-sub".to_string(),
                    new_version: "1.0-r0".to_string(),
                    description: "Package A for apk-tools testsuite".to_string(),
                    license: "GPL".to_string(),
                    old_version: "".to_string(),
                    url: "http://alpinelinux.org".to_string(),
                    installed_size: 4096,
                    size: 1250,
                    package_state: PackageState::Available,
                },
            );
            assert_eq!(
                packages[2],
                ApkPackageOwned {
                    name: "test-b".to_string(),
                    new_version: "1.0-r0".to_string(),
                    description: "Package A for apk-tools testsuite".to_string(),
                    license: "GPL".to_string(),
                    old_version: "".to_string(),
                    url: "http://alpinelinux.org".to_string(),
                    installed_size: 4096,
                    size: 1252,
                    package_state: PackageState::Available,
                },
            );
            assert_eq!(
                packages[3],
                ApkPackageOwned {
                    name: "test-d".to_string(),
                    new_version: "1.0-r0".to_string(),
                    description: "Package D for apk-tools testsuite - post-install".to_string(),
                    license: "GPL".to_string(),
                    old_version: "".to_string(),
                    url: "http://alpinelinux.org".to_string(),
                    installed_size: 4096,
                    size: 1406,
                    package_state: PackageState::Available,
                }
            );
            assert_eq!(
                packages[4],
                ApkPackageOwned {
                    name: "test-e".to_string(),
                    new_version: "1.0-r0".to_string(),
                    description: "Package E for apk-tools testsuite".to_string(),
                    license: "GPL".to_string(),
                    old_version: "".to_string(),
                    url: "http://alpinelinux.org".to_string(),
                    installed_size: 4096,
                    size: 1254,
                    package_state: PackageState::Available,
                },
            );
        });
    }

    #[test]
    fn test_repo_remove() {
        use std::fs::{create_dir_all, File};
        use std::io::Write;
        use std::path::Path;

        run_fs_test(|root, repo| {
            let server = DBusServer::new(true, Some(repo), Some(root.clone()));
            let repo_file_path = Path::new(&root)
                .join("etc")
                .join("apk")
                .join("repositories");
            create_dir_all(repo_file_path.parent().unwrap()).unwrap();
            let mut repo_file = File::create(&repo_file_path).unwrap();
            repo_file.write(b"https://alpine.global.ssl.fastly.net/alpine/edge/main\n#https://alpine.global.ssl.fastly.net/alpine/edge/testing\nhttps://alpine.global.ssl.fastly.net/alpine/edge/community\n").unwrap();
            server
                .remove_repository(
                    get_message_header(),
                    "https://alpine.global.ssl.fastly.net/alpine/edge/community",
                )
                .unwrap();
            let repos = server.list_repositories().unwrap();
            let repo_file_path_s = repo_file_path.to_str().map(|s| s.to_string());
            assert!(
                repos[0]
                    == ApkRepository::new(
                        true,
                        repo_file_path_s.clone(),
                        "https://alpine.global.ssl.fastly.net/alpine/edge/main".to_string(),
                    )
            );
            assert!(
                repos[1]
                    == ApkRepository::new(
                        false,
                        repo_file_path_s.clone(),
                        "https://alpine.global.ssl.fastly.net/alpine/edge/testing".to_string(),
                    ),
            );
            assert!(repos.len() == 2);
        });
    }

    #[test]
    fn test_update_repo() {
        run_fs_test(|root, repo| {
            let server = DBusServer::new(true, Some(repo), Some(root.clone()));
            server.update_repositories(get_message_header()).unwrap();
        });
    }

    #[test]
    fn test_search_file_owner() {
        run_fs_test(|root, repo| {
            let server = DBusServer::new(true, Some(repo), Some(root.clone()));
            server.add_package(get_message_header(), "test-e").unwrap();
            assert_eq!(
                server.search_file_owner("/usr/bin/test-e").unwrap().inner,
                ApkPackageOwned {
                    name: "test-e".to_string(),
                    new_version: "1.0-r0".to_string(),
                    description: "Package E for apk-tools testsuite".to_string(),
                    license: "GPL".to_string(),
                    old_version: "".to_string(),
                    url: "http://alpinelinux.org".to_string(),
                    installed_size: 4096,
                    size: 1254,
                    package_state: PackageState::Installed,
                },
            );
        });
    }

    #[test]
    fn test_list_upgradable() {
        run_fs_test(|root, repo| {
            let server = DBusServer::new(true, Some(repo.clone()), Some(root.clone()));
            server.add_package(get_message_header(), "test-a").unwrap();
            server.add_package(get_message_header(), "test-b").unwrap();
            server.add_package(get_message_header(), "test-e").unwrap();

            build_additional_package("test-a-new", &repo, &root);

            let packages: Vec<ApkPackageOwned> = server
                .list_upgradable_packages()
                .unwrap()
                .packages
                .into_iter()
                .map(|p| p.into())
                .collect();
            assert_eq!(
                packages[0],
                ApkPackageOwned {
                    name: "test-a".to_string(),
                    new_version: "1.1-r0".to_string(),
                    description: "Package A for apk-tools testsuite".to_string(),
                    license: "GPL".to_string(),
                    old_version: "1.0-r0".to_string(),
                    url: "http://alpinelinux.org".to_string(),
                    installed_size: 4096,
                    size: 1250,
                    package_state: PackageState::Upgradable,
                },
            );
            assert_eq!(packages.len(), 1);
        });
    }

    #[test]
    fn test_upgrade_package() {
        run_fs_test(|root, repo| {
            let server = DBusServer::new(true, Some(repo.clone()), Some(root.clone()));
            server.add_package(get_message_header(), "test-a").unwrap();
            server.add_package(get_message_header(), "test-e").unwrap();
            let output = Command::new(format!("{}/usr/bin/test-a", root)).output().expect("Running test-a executable failed; apparently installing the test-a package didn't work!");
            assert!(output.status.success());
            assert_eq!(output.stdout, b"hello from test-a-1.0\n");

            build_additional_package("test-a-new", &repo, &root);

            // No change should happen here
            server
                .upgrade_package(get_message_header(), "test-e")
                .unwrap();
            let output = Command::new(format!("{}/usr/bin/test-a", root)).output().expect("Running test-a executable failed; apparently installing the test-a package didn't work!");
            assert!(output.status.success());
            assert_eq!(output.stdout, b"hello from test-a-1.0\n");

            // Actually upgrade, now it should be 1.1 not 1.0
            server
                .upgrade_package(get_message_header(), "test-a")
                .unwrap();
            let output = Command::new(format!("{}/usr/bin/test-a", root)).output().expect("Running test-a executable failed; apparently installing the test-a package didn't work!");
            assert!(output.status.success());
            assert_eq!(output.stdout, b"hello from test-a-1.1\n");
        });
    }

    #[test]
    fn test_get_package_details() {
        run_fs_test(|root, repo| {
            let server = DBusServer::new(true, Some(repo), Some(root.clone()));
            server.add_package(get_message_header(), "test-a");
            assert_eq!(
                server.get_package_details("test-a").unwrap().inner,
                ApkPackageOwned {
                    name: "test-a".to_string(),
                    new_version: "1.0-r0".to_string(),
                    description: "Package A for apk-tools testsuite".to_string(),
                    license: "GPL".to_string(),
                    old_version: "".to_string(),
                    url: "http://alpinelinux.org".to_string(),
                    installed_size: 4096,
                    size: 1250,
                    package_state: PackageState::Installed,
                },
            );
            assert_eq!(
                server.get_package_details("test-e").unwrap().inner,
                ApkPackageOwned {
                    name: "test-e".to_string(),
                    new_version: "1.0-r0".to_string(),
                    description: "Package E for apk-tools testsuite".to_string(),
                    license: "GPL".to_string(),
                    old_version: "".to_string(),
                    url: "http://alpinelinux.org".to_string(),
                    installed_size: 4096,
                    size: 1254,
                    package_state: PackageState::Available,
                },
            );
            assert!(server.get_package_details("Unknown!").is_err());
        });
    }

    #[test]
    fn test_delete_required_package() {
        run_fs_test(|root, repo| {
            let server = DBusServer::new(true, Some(repo), Some(root.clone()));
            server.add_package(get_message_header(), "test-e").unwrap();
            let testa_exe_path = Path::new(&root).join("usr").join("bin").join("test-a");
            assert!(testa_exe_path.exists());
            let e = server.delete_package(get_message_header(), "test-a");
            assert!(e.is_err());
            assert_eq!(e.err().unwrap().to_string(), "org.freedesktop.DBus.Error.Failed: Package test-a still required by the following packages: test-b test-e");
            assert!(testa_exe_path.exists());
        });
    }

    #[test]
    fn test_upgrade_subpackage() {
        run_fs_test(|root, repo| {
            let server = DBusServer::new(true, Some(repo.clone()), Some(root.clone()));
            server
                .add_package(get_message_header(), "test-a-sub")
                .unwrap();
            server.add_package(get_message_header(), "test-e").unwrap();
            let output = Command::new(format!("{}/usr/bin/test-a", root)).output().expect("Running test-a executable failed; apparently installing the test-a package didn't work!");
            assert!(output.status.success());
            assert_eq!(output.stdout, b"hello from test-a-1.0\n");

            build_additional_package("test-a-new", &repo, &root);

            // No change should happen here
            server
                .upgrade_package(get_message_header(), "test-e")
                .unwrap();
            let output = Command::new(format!("{}/usr/bin/test-a", root)).output().expect("Running test-a executable failed; apparently installing the test-a package didn't work!");
            assert!(output.status.success());
            assert_eq!(output.stdout, b"hello from test-a-1.0\n");

            // Upgrade the subpackage, that should also upgrade the main pkg
            server
                .upgrade_package(get_message_header(), "test-a-sub")
                .unwrap();
            let output = Command::new(format!("{}/usr/bin/test-a", root)).output().expect("Running test-a executable failed; apparently installing the test-a package didn't work!");
            assert!(output.status.success());
            assert_eq!(output.stdout, b"hello from test-a-1.1\n");
        });
    }

    #[test]
    fn test_replace_upgrade_package() {
        run_fs_test(|root, repo| {
            let server = DBusServer::new(true, Some(repo.clone()), Some(root.clone()));
            server.add_package(get_message_header(), "test-a").unwrap();

            build_additional_package("test-c", &repo, &root);

            let upgrade_list = server.list_upgradable_packages().unwrap();
            assert_eq!(upgrade_list.packages.len(), 2);
            let pkg: Vec<ApkPackageOwned> = upgrade_list
                .packages
                .into_iter()
                .take(2)
                .map(|p| {
                    let pkg: ApkPackageOwned = p.into();
                    pkg
                })
                .collect();
            assert_eq!(
                pkg[0],
                ApkPackageOwned {
                    name: "test-a".to_string(),
                    new_version: "1.0-r0".to_string(),
                    description: "Package A for apk-tools testsuite".to_string(),
                    license: "GPL".to_string(),
                    old_version: "".to_string(),
                    url: "http://alpinelinux.org".to_string(),
                    installed_size: 4096,
                    size: 1250,
                    package_state: PackageState::PendingRemoval,
                }
            );
            assert_eq!(
                pkg[1],
                ApkPackageOwned {
                    name: "test-c".to_string(),
                    new_version: "1.0-r1".to_string(),
                    description: "Package C for apk-tools testsuite".to_string(),
                    license: "GPL".to_string(),
                    old_version: "".to_string(),
                    url: "http://alpinelinux.org".to_string(),
                    installed_size: 4096,
                    size: 1250,
                    package_state: PackageState::PendingInstall,
                }
            );
        });
    }

    #[test]
    fn test_serialize_apk_package_owned() {
        use serde_test::{assert_ser_tokens, Token};

        run_fs_test(|root, repo| {
            let server = DBusServer::new(true, Some(repo), Some(root.clone()));
            let mut packages: Vec<ApkPackageOwned> = server
                .list_available_packages()
                .unwrap()
                .packages
                .into_iter()
                .map(|p| p.into())
                .collect();
            packages.sort();
            packages[0].installed_size = 4096;
            packages[0].size = 1250;
            assert_ser_tokens(
                &packages[0],
                &[
                    Token::Struct {
                        name: "ApkPackageOwned",
                        len: 9,
                    },
                    Token::Str("name"),
                    Token::Str("test-a"),
                    Token::Str("new_version"),
                    Token::Str("1.0-r0"),
                    Token::Str("description"),
                    Token::Str("Package A for apk-tools testsuite"),
                    Token::Str("license"),
                    Token::Str("GPL"),
                    Token::Str("old_version"),
                    Token::Str(""),
                    Token::Str("url"),
                    Token::Str("http://alpinelinux.org"),
                    Token::Str("installed_size"),
                    Token::U64(4096),
                    Token::Str("size"),
                    Token::U64(1250),
                    Token::Str("package_state"),
                    Token::U32(0),
                    Token::StructEnd,
                ],
            )
        });
    }

    #[test]
    fn test_serialize_apk_package() {
        use serde_test::{assert_ser_tokens, Token};
        use std::convert::TryFrom;

        run_fs_test(|root, repo| {
            let server = DBusServer::new(true, Some(repo), Some(root.clone()));
            let op = server.list_available_packages().unwrap();
            let pos = op
                .packages
                .iter()
                .position(|p| p.name() == "test-d")
                .unwrap();
            // This can vary between different runs
            let size = u64::try_from(op.packages[pos].size()).unwrap();
            let installed_size = u64::try_from(op.packages[pos].installed_size()).unwrap();
            assert_ser_tokens(
                &op.packages[pos],
                &[
                    Token::Struct {
                        name: "ApkPackage",
                        len: 9,
                    },
                    Token::Str("name"),
                    Token::Str("test-d"),
                    Token::Str("new_version"),
                    Token::Str("1.0-r0"),
                    Token::Str("description"),
                    Token::Str("Package D for apk-tools testsuite - post-install"),
                    Token::Str("license"),
                    Token::Str("GPL"),
                    Token::Str("old_version"),
                    Token::Str(""),
                    Token::Str("url"),
                    Token::Str("http://alpinelinux.org"),
                    Token::Str("installed_size"),
                    Token::U64(installed_size),
                    Token::Str("size"),
                    Token::U64(size),
                    Token::Str("package_state"),
                    Token::U32(0),
                    Token::StructEnd,
                ],
            );
        });
    }

    #[test]
    fn test_add_bad_package() {
        run_fs_test(|root, repo| {
            let server = DBusServer::new(true, Some(repo), Some(root.clone()));
            let e = server.add_package(get_message_header(), "test-a 1.0").err();
            assert_eq!(
                e,
                Some(apk_tools::Error::Add("Failed to add package test-a 1.0 due to error test-a 1.0 is not a correctly formated world dependency, the format should be: name(@tag)([<>~=]version)".to_string()).into())
            );
        });
    }

    #[test]
    fn test_delete_bad_package() {
        run_fs_test(|root, repo| {
            let server = DBusServer::new(true, Some(repo), Some(root.clone()));
            let e = server
                .delete_package(get_message_header(), "test-a 1.0")
                .err();
            assert_eq!(
                e,
                Some(apk_tools::Error::Delete("Failed to delete package test-a 1.0 due to error test-a 1.0 is not a correctly formated world dependency, the format should be: name(@tag)([<>~=]version)".to_string()).into())
            );
        });
    }

    #[test]
    fn test_upgrade_bad_package() {
        run_fs_test(|root, repo| {
            let server = DBusServer::new(true, Some(repo), Some(root.clone()));
            let e = server
                .upgrade_package(get_message_header(), "test-a 1.0")
                .err();
            assert_eq!(
                e,
                Some(apk_tools::Error::Upgrade("Failed to upgrade package test-a 1.0 due to error test-a 1.0 is not a correctly formated world dependency, the format should be: name(@tag)([<>~=]version)".to_string()).into())
            );
        });
    }

    #[test]
    fn test_get_repositories_nonexistent_file() {
        run_fs_test(|root, repo| {
            let server = DBusServer::new(true, Some(repo), Some(root.clone()));
            let e = server.list_repositories().err();
            assert_eq!(
                e,
                Some(
                    apk_tools::Error::GetRepositories(format!(
                        "Failed to get repositories from file {}/etc/apk/repositories due to error No such file or directory (os error 2)",
                        &root
                    ))
                    .into()
                )
            );
        });
    }

    #[test]
    fn test_update_repositories_nonexistent_repo() {
        run_fs_test(|root, _| {
            let server = DBusServer::new(
                true,
                Some("THISFILEDOESNTEXIST".to_string()),
                Some(root.clone()),
            );
            let e = server.update_repositories(get_message_header()).err();
            assert_eq!(format!("{}", e.unwrap()), "org.freedesktop.DBus.Error.Failed: An error occured: Fetching repository THISFILEDOESNTEXIST failed due to error No such file or directory".to_string());
        });
    }

    #[test]
    fn test_database_open_nonexistent_dir() {
        run_fs_test(|_, _| {
            let server = DBusServer::new(true, None, Some("THISFILEDOESNTEXIST".to_string()));
            let e = server.list_available_packages().err();
            assert_eq!(
                e,
                Some(
                    apk_tools::Error::DatabaseOpen(
                        "Failed to open the apk database due to error No such file or directory"
                            .to_string()
                    )
                    .into()
                )
            );
        });
    }

    #[test]
    fn test_get_package_details_not_installed_pkg() {
        run_fs_test(|root, repo| {
            let server = DBusServer::new(true, Some(repo), Some(root));
            let e = server.get_package_details("unknown_package").err();
            assert_eq!(
                e,
                Some(
                    apk_tools::Error::Details(
                        "Couldn\'t retrieve details for unknown package unknown_package"
                            .to_string()
                    )
                    .into()
                )
            );
        });
    }

    #[test]
    fn test_search_file_owner_nonexistent_file() {
        run_fs_test(|root, repo| {
            let server = DBusServer::new(true, Some(repo), Some(root.clone()));
            server.add_package(get_message_header(), "test-e").unwrap();
            assert_eq!(
                server.search_file_owner("/usr/bin/test").err(),
                Some(zbus::fdo::Error::Failed(
                    "Couldn't find package owning path /usr/bin/test".to_string(),
                ))
            );
        });
    }
}
