// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use anyhow::{format_err, Context, Error};
use fdio::{create_fd, device_get_topo_path};
use fidl::endpoints::ClientEnd;
use fidl_fuchsia_io::{DirectoryMarker, OPEN_RIGHT_READABLE, OPEN_RIGHT_WRITABLE};
use fidl_fuchsia_paver::{PaverMarker, PaverProxy};
use fs_management as fs;
use fuchsia_component::client::connect_to_protocol;
use fuchsia_zircon as zx;

/// Calls the paver service to format the system volume, and returns the path
/// to the newly created blobfs and minfs partitions.
async fn format_disk(paver: PaverProxy) -> Result<(String, String), Error> {
    let (data_sink, data_sink_server_end) = fidl::endpoints::create_proxy()?;
    paver.find_data_sink(data_sink_server_end)?;

    let server_end = match data_sink.wipe_volume().await? {
        Ok(server) => server,
        Err(err) => return Err(format_err!("failed to wipe volume: {}", err)),
    };

    let file = create_fd(server_end.into_channel().into())?;

    let base_path = device_get_topo_path(&file)?;

    Ok((format!("{}/blobfs-p-1/block", base_path), format!("{}/minfs-p-2/block", base_path)))
}

/// Required functionality from an fs::Filesystem.
/// See fs_management for the documentation.
trait Filesystem {
    fn format(&mut self) -> Result<(), Error>;
    fn mount(&mut self, mount_point: &str) -> Result<(), Error>;
}

/// Forwards calls to the fs_management implementation.
impl Filesystem for fs::Filesystem<fs::Blobfs> {
    fn format(&mut self) -> Result<(), Error> {
        self.format()
    }

    fn mount(&mut self, mount_point: &str) -> Result<(), Error> {
        self.mount(mount_point)
    }
}

/// The object that controls the lifetime of the newly minted blobfs.
/// The filesystem is accessible through the "/b" path on the current namespace,
/// as long as this object is alive.
pub struct Storage {
    _blobfs: fs::Filesystem<fs::Blobfs>,
    minfs: fs::Filesystem<fs::Minfs>,
}

impl Storage {
    /// Re-initializes the storage stack on this device, returning an object that
    /// encapsulates the freshly minted blobfs.
    pub async fn new() -> Result<Storage, Error> {
        println!("About to Initialize storage");
        let paver = connect_to_protocol::<PaverMarker>()?;
        let (blobfs_path, minfs_path) = format_disk(paver.clone()).await?;

        let blobfs = create_blobfs(blobfs_path)?;
        let minfs = create_minfs(minfs_path)?;

        println!("Storage initialized");
        Ok(Storage { _blobfs: blobfs, minfs })
    }

    /// Mounts the encapsulated minfs at "/m".
    pub fn mount_minfs(&mut self) -> Result<(), Error> {
        self.minfs.mount("/m")
    }

    pub fn get_blobfs(&self) -> Result<ClientEnd<DirectoryMarker>, Error> {
        let (blobfs_root, remote) = fidl::endpoints::create_endpoints::<DirectoryMarker>()?;
        fdio::open("/b", OPEN_RIGHT_READABLE | OPEN_RIGHT_WRITABLE, remote.into_channel())?;
        Ok(blobfs_root)
    }
}

/// Creates and mounts the provided filesystem at "/b".
fn mount_filesystem(blobfs: &mut dyn Filesystem) -> Result<(), Error> {
    blobfs.format()?;
    blobfs.mount("/b")?;
    Ok(())
}

/// Uses the provide block device path to format a new blobfs partition.
fn create_blobfs(block_device_path: String) -> Result<fs::Filesystem<fs::Blobfs>, Error> {
    let mut blobfs = fs::Blobfs::new(block_device_path.as_str())?;

    mount_filesystem(&mut blobfs)?;
    Ok(blobfs)
}

/// Uses the provide block device path to format a new minfs partition.
/// Does not mount the partition, but leaves it blank.
fn create_minfs(block_device_path: String) -> Result<fs::Filesystem<fs::Minfs>, Error> {
    let (block_device, server_chan) = zx::Channel::create()?;
    fdio::service_connect(&block_device_path, server_chan).context("connecting to block device")?;
    create_minfs_from_channel(block_device)
}

fn create_minfs_from_channel(
    block_device_channel: zx::Channel,
) -> Result<fs::Filesystem<fs::Minfs>, Error> {
    let mut minfs = fs::Minfs::from_channel(block_device_channel)?;
    minfs.format()?;
    Ok(minfs)
}

#[cfg(test)]
mod tests {
    use super::{create_minfs_from_channel, format_disk, mount_filesystem, Filesystem};
    use anyhow::Error;
    use fidl::endpoints::ClientEnd;
    use fidl_fuchsia_hardware_block_volume::VolumeManagerMarker;
    use fidl_fuchsia_paver::{
        DataSinkRequest, DataSinkRequestStream, PaverMarker, PaverRequest, PaverRequestStream,
    };
    use fuchsia_component::server::{NestedEnvironment, ServiceFs};
    use fuchsia_zircon as zx;
    use fuchsia_zircon::Status;
    use futures::prelude::*;
    use ramdevice_client::RamdiskClientBuilder;
    use std::sync::Arc;

    // Mock for a Filesystem.
    struct FilesystemMock {
        format_called: bool,
        mount_called: bool,
    }

    impl FilesystemMock {
        fn new() -> FilesystemMock {
            FilesystemMock { format_called: false, mount_called: false }
        }
    }

    impl Filesystem for FilesystemMock {
        fn format(&mut self) -> Result<(), Error> {
            assert!(!self.format_called);
            self.format_called = true;
            Ok(())
        }

        fn mount(&mut self, mount_point: &str) -> Result<(), Error> {
            assert!(self.format_called);
            assert_eq!(mount_point, "/b");
            self.mount_called = true;
            Ok(())
        }
    }

    /// Tests that mount_filesystem calls the expected Filesystem functions.
    #[test]
    fn test_mount() {
        let mut blobfs: FilesystemMock = FilesystemMock::new();
        let result = match mount_filesystem(&mut blobfs) {
            Ok(()) => true,
            _ => false,
        };
        assert!(result);
        assert!(blobfs.mount_called);
    }

    /// Mock for the paver service.
    struct MockPaver {
        /// Desired response.
        response: Status,
    }

    impl MockPaver {
        fn new(response: Status) -> Self {
            Self { response }
        }

        /// FindDataSink implementation.
        async fn run_service(self: Arc<Self>, mut stream: PaverRequestStream) -> Result<(), Error> {
            while let Some(req) = stream.try_next().await? {
                match req {
                    PaverRequest::FindDataSink { data_sink, .. } => {
                        let mock_paver_clone = self.clone();
                        fuchsia_async::Task::spawn(
                            mock_paver_clone
                                .run_data_sink_service(data_sink.into_stream()?)
                                .unwrap_or_else(|e| panic!("error running paver service: {:?}", e)),
                        )
                        .detach();
                    }
                    req => println!("mock Paver ignoring request: {:?}", req),
                }
            }
            Ok(())
        }

        /// WipeVolume implementation.
        async fn run_data_sink_service(
            self: Arc<Self>,
            mut stream: DataSinkRequestStream,
        ) -> Result<(), Error> {
            while let Some(req) = stream.try_next().await? {
                match req {
                    DataSinkRequest::WipeVolume { responder } => {
                        if self.response == Status::OK {
                            responder.send(&mut Ok(grab_volume())).expect("send ok");
                        } else {
                            responder.send(&mut Err(self.response.into_raw())).expect("send ok");
                        }
                    }
                    req => println!("mock paver ignoring request: {:?}", req),
                }
            }
            Ok(())
        }
    }

    /// Provides a suitable channel for a faked successful WipeVolume call.
    fn grab_volume() -> ClientEnd<VolumeManagerMarker> {
        // We need a channel to a device supported by device_get_topo_path.
        // Grab the system's fist block device.
        let (client_channel, server_channel) = zx::Channel::create().unwrap();
        fdio::service_connect("/dev/class/block/000", server_channel).expect("Open block device");
        ClientEnd::<VolumeManagerMarker>::new(client_channel)
    }

    struct TestEnv {
        env: NestedEnvironment,
        _paver: Arc<MockPaver>,
    }

    impl TestEnv {
        fn new(paver: MockPaver) -> Self {
            let mut fs = ServiceFs::new();
            let paver = Arc::new(paver);
            let paver_clone = Arc::clone(&paver);
            fs.add_fidl_service(move |stream: PaverRequestStream| {
                let paver_clone = Arc::clone(&paver_clone);
                fuchsia_async::Task::local(
                    paver_clone
                        .run_service(stream)
                        .unwrap_or_else(|e| panic!("error running paver service: {:?}", e)),
                )
                .detach()
            });

            let env = fs
                .create_salted_nested_environment("recovery_test_env")
                .expect("nested environment to create successfully");
            fuchsia_async::Task::spawn(fs.collect()).detach();

            Self { env, _paver: paver }
        }
    }

    /// Tests that format_disk fails when the paver service fails the request.
    #[fuchsia_async::run_singlethreaded(test)]
    async fn test_calls_paver_service_with_errors() {
        let env = TestEnv::new(MockPaver::new(Status::NOT_SUPPORTED));
        let paver = env.env.connect_to_protocol::<PaverMarker>().unwrap();

        let path = format_disk(paver.clone()).await;
        assert_eq!(
            (String::new(), String::new()),
            match path {
                Ok(path) => path,
                Err(err) => {
                    println!("{:?}", err);
                    (String::new(), String::new())
                }
            }
        );
    }

    /// Tests that a successful call to the paver service results in a path with
    /// the expected form.
    #[fuchsia_async::run_singlethreaded(test)]
    async fn test_calls_paver_service() {
        let env = TestEnv::new(MockPaver::new(Status::OK));
        let paver = env.env.connect_to_protocol::<PaverMarker>().unwrap();

        let result = format_disk(paver.clone()).await;
        let (blobfs_path, minfs_path) = match result {
            Ok(paths) => paths,
            Err(err) => {
                println!("{:?}", err);
                (String::new(), String::new())
            }
        };
        assert!(blobfs_path.ends_with("/blobfs-p-1/block"));
        assert!(minfs_path.ends_with("/minfs-p-2/block"));
    }

    /// Tests that creating minfs works.
    #[fuchsia_async::run_singlethreaded(test)]
    async fn test_minfs_create_works() {
        const BLOCK_SIZE: u64 = 512;
        const BLOCK_COUNT: u64 = 262144; // 128 MB
        let block_device = RamdiskClientBuilder::new(BLOCK_SIZE, BLOCK_COUNT)
            .isolated_dev_root()
            .build()
            .expect("Ramdisk creation succeeds");

        let ramdisk = block_device.open().expect("Open ramdisk");
        let mut minfs = create_minfs_from_channel(ramdisk).expect("creating minfs to succeed");
        minfs.fsck().expect("Fsck to succeed on the new minfs");
        minfs.mount("/minfs").expect("minfs mount to succeed");
    }
}
