// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

use std::{
    fs::OpenOptions,
    os::unix::io::AsRawFd,
    panic,
    path::{Path, PathBuf},
};

use loopdev::{LoopControl, LoopDevice};
use tempfile::{self, TempDir};

use crate::testing::test_lib::clean_up;

pub struct LoopTestDev {
    ld: LoopDevice,
}

impl LoopTestDev {
    fn new(lc: &LoopControl, path: &Path) -> LoopTestDev {
        OpenOptions::new()
            .read(true)
            .write(true)
            .open(path)
            .unwrap();

        let ld = lc.next_free().unwrap();
        ld.attach_file(path).unwrap();

        LoopTestDev { ld }
    }

    fn path(&self) -> PathBuf {
        self.ld.path().unwrap()
    }

    fn detach(&self) {
        self.ld.detach().unwrap()
    }
}

impl Drop for LoopTestDev {
    fn drop(&mut self) {
        self.detach()
    }
}

/// Setup count loop backed devices in dir.
/// Make sure each loop device is backed by a sparse 1 GiB file. The entire file will read back
/// as initialized with zero.
fn get_devices(count: u8, dir: &TempDir) -> Vec<LoopTestDev> {
    let lc = LoopControl::open().unwrap();
    let mut loop_devices = Vec::new();

    for index in 0..count {
        let path = dir.path().join(format!("store{}", &index));
        let f = OpenOptions::new()
            .read(true)
            .write(true)
            .create(true)
            .open(&path)
            .unwrap();

        nix::unistd::ftruncate(f.as_raw_fd(), 1_000_000_000).unwrap();
        f.sync_all().unwrap();

        let ltd = LoopTestDev::new(&lc, &path);

        loop_devices.push(ltd);
    }
    loop_devices
}

/// Set up count loopbacked devices.
/// Then, run the designated test.
/// Then, take down the loop devices.
pub fn test_with_spec<F>(count: u8, test: F)
where
    F: Fn(&[&Path]) + panic::RefUnwindSafe,
{
    clean_up().unwrap();

    let tmpdir = tempfile::Builder::new()
        .prefix("btrfsutil-")
        .tempdir()
        .unwrap();
    let loop_devices: Vec<LoopTestDev> = get_devices(count, &tmpdir);
    let device_paths: Vec<PathBuf> = loop_devices.iter().map(|x| x.path()).collect();
    let device_paths: Vec<&Path> = device_paths.iter().map(|x| x.as_path()).collect();

    let result = panic::catch_unwind(|| test(&device_paths));
    let tear_down = clean_up();

    result.unwrap();
    tear_down.unwrap();
}
