//! Bundle your distribution as a ZIP archive.

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

use cicero_core::error::Context;

use crate::fs;

use super::{BundleMetadata, Bundler};


/// Provides a ZIP archive of the distribution files,
/// typically compressed as `.zip` file.
///
/// To be used with [`Distribution::bundle`](crate::Distribution::bundle).
#[derive(Default)]
pub struct ZipBundler;

impl Bundler for ZipBundler {
    fn file_extension(&self) -> &str {
        ".zip"
    }

    fn bundle(&self, in_dir: &Path, out_path: &Path, metadata: &BundleMetadata) -> cicero_core::Result<()> {

        let mut zip_file = {
            let out_file = fs::File::create(out_path)?;

            zip::ZipWriter::new(out_file)
        };

        let options = zip::write::SimpleFileOptions::default();


        for entry in walkdir::WalkDir::new(in_dir) {
            let entry = entry
                .context("Error while opening file when walking input directory {in_dir:?}.")?;

            let in_path = entry.path();

            let out_path = {
                let path = entry.path().strip_prefix(in_dir)?; //strip absolute path, because `zip` library will add the entire directory structure into the ZIP file
                PathBuf::from(&metadata.distribution_name).join(path) //nest files into folder with distribution name, to avoid spilling files everywhere when user unpacks
            };

            if in_path.is_file() {
                let mut in_file = fs::File::open(in_path)?;

                #[cfg(unix)]
                let options = { //keep original Unix permissions
                    use std::os::unix::fs::PermissionsExt;

                    let permissions = in_file.metadata()?.permissions();

                    options.unix_permissions(permissions.mode())
                };

                zip_file.start_file_from_path(out_path, options)?;

                io::copy(&mut in_file, &mut zip_file)?;
            } else {
                zip_file.add_directory_from_path(out_path, options)?;
            }
        }

        zip_file.finish()?;
        Ok(())
    }
}


#[cfg(test)]
mod tests {
    use crate::{distribution::tests::DistributionFixture, DistributionOptions};

    use super::*;


    #[test]
    fn should_bundle_as_zip() -> cicero_core::Result<()> {
        let fixture = DistributionFixture::create()?;
        let distribution = fixture.testee(DistributionOptions::default())?;

        distribution.root
            .add_file("file1", |file| fs::write(file, "Hello").context("Adding file 'file1'"))?;

        let zip_file = distribution.bundle(ZipBundler)?;

        assert!(zip_file.to_string_lossy().ends_with(".zip"), "Expected file path to end with '.zip', but got: {zip_file:?}");
        assert!(fs::metadata(zip_file)?.len() > 0);

        Ok(())
    }
}
