use std::ffi::{OsStr, OsString};
use std::fs;
use std::io::Read;
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::os::unix::fs::{symlink, PermissionsExt};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Output, Stdio};
use std::thread;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

pub struct MountedTimefs {
    mountpoint: PathBuf,
    child: Option<Child>,
}

pub struct MountRunResult {
    pub stderr: String,
}

impl MountedTimefs {
    pub fn mount(repo: &Path, cli_args: &[&str], mount_args: &[&str]) -> Self {
        let mountpoint = create_temp_dir("timefs-it-mount");
        let child = Command::new(timefs_binary())
            .args(cli_args)
            .arg("mount")
            .arg(repo)
            .arg(&mountpoint)
            .args(mount_args)
            .stdout(Stdio::null())
            .stderr(Stdio::piped())
            .spawn()
            .expect("timefs mount should spawn");

        let mut mount = Self {
            mountpoint,
            child: Some(child),
        };
        mount.wait_until_ready(b"now");
        mount
    }

    pub fn path(&self) -> &Path {
        &self.mountpoint
    }

    pub fn finish(mut self) -> MountRunResult {
        self.unmount();
        MountRunResult {
            stderr: self.wait_for_exit("mount process should exit after explicit unmount"),
        }
    }

    fn wait_until_ready(&mut self, expected_name: &[u8]) {
        for _ in 0..120 {
            if let Ok(entries) = fs::read_dir(&self.mountpoint) {
                let names: Vec<Vec<u8>> = entries
                    .filter_map(Result::ok)
                    .map(|entry| entry.file_name().into_vec())
                    .collect();
                if names.iter().any(|name| name == expected_name) {
                    return;
                }
            }

            if let Some(child) = self.child.as_mut() {
                if let Some(status) = child
                    .try_wait()
                    .expect("mount child status should be readable")
                {
                    panic!(
                        "mount process exited early with status {}: {}",
                        status,
                        child_stderr(child)
                    );
                }
            }

            thread::sleep(Duration::from_millis(50));
        }

        panic!(
            "mountpoint did not become ready at {}",
            self.mountpoint.display()
        );
    }

    fn unmount(&self) {
        run_timefs(["unmount", self.mountpoint.to_string_lossy().as_ref()]);
    }

    fn wait_for_exit(&mut self, message: &str) -> String {
        let Some(mut child) = self.child.take() else {
            return String::new();
        };
        let status = child.wait().expect("mount child status should be readable");
        let stderr = child_stderr(&mut child);
        assert!(status.success(), "{message}: {stderr}");
        wait_for_mount_cleared(&self.mountpoint);
        stderr
    }
}

impl Drop for MountedTimefs {
    fn drop(&mut self) {
        if self.child.is_none() {
            return;
        }

        self.unmount();
        let _ = self.wait_for_exit("mount process should exit during drop cleanup");
    }
}

pub fn with_mounted_timefs<T, F>(
    repo: &Path,
    cli_args: &[&str],
    mount_args: &[&str],
    body: F,
) -> (T, MountRunResult)
where
    F: FnOnce(&MountedTimefs) -> T,
{
    let mount = MountedTimefs::mount(repo, cli_args, mount_args);
    let value = body(&mount);
    let result = mount.finish();
    (value, result)
}

pub struct RepositoryFixture {
    root: PathBuf,
}

impl RepositoryFixture {
    pub fn new(prefix: &str) -> Self {
        let root = unique_temp_path(prefix);
        fs::create_dir_all(&root).expect("fixture directory creation should succeed");
        let fixture = Self { root };
        fixture.git(["init", "-b", "main"]);
        fixture.git(["config", "user.name", "Timefs Tests"]);
        fixture.git(["config", "user.email", "timefs-tests@example.com"]);
        fixture
    }

    pub fn path(&self) -> &Path {
        &self.root
    }

    pub fn git<I, S>(&self, args: I) -> Output
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        run_git_in(&self.root, args)
    }

    #[allow(dead_code)]
    pub fn git_with_options<A, B, S1, S2>(&self, options: A, args: B) -> Output
    where
        A: IntoIterator<Item = S1>,
        B: IntoIterator<Item = S2>,
        S1: AsRef<OsStr>,
        S2: AsRef<OsStr>,
    {
        let output = Command::new("git")
            .args(options)
            .args(args)
            .current_dir(&self.root)
            .output()
            .expect("git should be executable in tests");

        if output.status.success() {
            output
        } else {
            panic!(
                "git command failed with status {}: {}",
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
    }

    pub fn git_ls_tree_recursive(&self, rev: &str) -> Vec<GitEntry> {
        parse_ls_tree_entries(&self.git(["ls-tree", "-r", "-z", rev]).stdout)
    }

    pub fn git_show_path(&self, rev: &str, path: &[u8]) -> Vec<u8> {
        let spec = format!("{rev}:{}", String::from_utf8_lossy(path));
        self.git(["show", spec.as_str()]).stdout
    }

    #[allow(dead_code)]
    pub fn git_rev_parse(&self, spec: &str) -> String {
        String::from_utf8(self.git(["rev-parse", spec]).stdout)
            .expect("rev-parse output should be valid UTF-8")
            .trim()
            .to_owned()
    }

    #[allow(dead_code)]
    pub fn git_rev_parse_short(&self, spec: &str) -> String {
        String::from_utf8(self.git(["rev-parse", "--short", spec]).stdout)
            .expect("short rev-parse output should be valid UTF-8")
            .trim()
            .to_owned()
    }

    #[allow(dead_code)]
    pub fn remove_worktree_entries(&self) {
        let entries = fs::read_dir(&self.root).expect("fixture directory should be readable");
        for entry in entries.filter_map(Result::ok) {
            if entry.file_name() == OsStr::new(".git") {
                continue;
            }
            let path = entry.path();
            let file_type = entry
                .file_type()
                .expect("fixture file type should be readable");
            if file_type.is_dir() {
                fs::remove_dir_all(&path).expect("fixture subtree removal should succeed");
            } else {
                fs::remove_file(&path).expect("fixture file removal should succeed");
            }
        }
    }
}

impl Drop for RepositoryFixture {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.root);
    }
}

#[derive(Debug)]
pub struct GitEntry {
    pub mode: u32,
    pub kind: String,
    pub path: Vec<u8>,
}

pub fn assert_revision_tree_matches_git(fixture: &RepositoryFixture, mount_root: &Path, rev: &str) {
    let git_entries = fixture.git_ls_tree_recursive(rev);
    let mount_paths = collect_mount_leaf_paths(mount_root);
    assert_eq!(
        mount_paths,
        git_entries
            .iter()
            .map(|entry| entry.path.clone())
            .collect::<Vec<_>>()
    );

    for entry in &git_entries {
        let mounted_path = mount_root.join(OsString::from_vec(entry.path.clone()));
        match entry.kind.as_str() {
            "blob" if entry.mode == 0o120000 => {
                let target = fs::read_link(&mounted_path).expect("symlink should resolve");
                assert_eq!(
                    target.as_os_str().as_bytes(),
                    fixture.git_show_path(rev, &entry.path)
                );
            }
            "blob" => {
                assert_eq!(
                    fs::read(&mounted_path).expect("file should be readable"),
                    fixture.git_show_path(rev, &entry.path)
                );
            }
            other => panic!("unexpected git entry kind in fixture: {other}"),
        }
    }
}

pub fn collect_mount_leaf_paths(root: &Path) -> Vec<Vec<u8>> {
    let mut paths = Vec::new();
    collect_mount_leaf_paths_recursive(root, root, &mut paths);
    paths.sort();
    paths
}

pub fn create_temp_dir(prefix: &str) -> PathBuf {
    let path = unique_temp_path(prefix);
    fs::create_dir_all(&path).expect("temporary directory creation should succeed");
    path
}

pub fn run_git_in<I, S>(root: &Path, args: I) -> Output
where
    I: IntoIterator<Item = S>,
    S: AsRef<OsStr>,
{
    let output = Command::new("git")
        .args(args)
        .current_dir(root)
        .output()
        .expect("git should be executable in tests");

    if output.status.success() {
        output
    } else {
        panic!(
            "git command failed with status {}: {}",
            output.status,
            String::from_utf8_lossy(&output.stderr)
        );
    }
}

pub fn make_executable(path: &Path) {
    let mut permissions = fs::metadata(path)
        .expect("script metadata should be readable")
        .permissions();
    permissions.set_mode(0o755);
    fs::set_permissions(path, permissions).expect("script permissions should be writable");
}

pub fn write_symlink(target: &str, path: &Path) {
    symlink(target, path).expect("symlink creation should succeed");
}

pub fn parse_ls_tree_entries(output: &[u8]) -> Vec<GitEntry> {
    output
        .split(|byte| *byte == 0)
        .filter(|record| !record.is_empty())
        .map(parse_ls_tree_entry)
        .collect()
}

fn parse_ls_tree_entry(record: &[u8]) -> GitEntry {
    let tab = record
        .iter()
        .position(|byte| *byte == b'\t')
        .expect("ls-tree records should contain a tab");
    let (header, path) = record.split_at(tab);
    let header = std::str::from_utf8(header).expect("ls-tree header should be UTF-8");
    let mut parts = header.split_whitespace();
    let mode = u32::from_str_radix(parts.next().expect("mode should exist"), 8)
        .expect("mode should be valid octal");
    let kind = parts.next().expect("kind should exist").to_owned();
    let _oid = parts.next().expect("oid should exist");

    GitEntry {
        mode,
        kind,
        path: path.get(1..).unwrap_or_default().to_vec(),
    }
}

fn run_timefs<I, S>(args: I)
where
    I: IntoIterator<Item = S>,
    S: AsRef<OsStr>,
{
    let output = Command::new(timefs_binary())
        .args(args)
        .output()
        .expect("timefs command should execute");

    if !output.status.success() {
        panic!(
            "timefs command failed with status {}: {}",
            output.status,
            String::from_utf8_lossy(&output.stderr)
        );
    }
}

fn wait_for_mount_cleared(mountpoint: &Path) {
    for _ in 0..120 {
        if let Ok(entries) = fs::read_dir(mountpoint) {
            if entries.count() == 0 {
                return;
            }
        }
        thread::sleep(Duration::from_millis(50));
    }

    panic!(
        "mountpoint did not clear after unmount: {}",
        mountpoint.display()
    );
}

fn child_stderr(child: &mut Child) -> String {
    let mut stderr = String::new();
    if let Some(mut pipe) = child.stderr.take() {
        let _ = pipe.read_to_string(&mut stderr);
    }
    stderr
}

fn collect_mount_leaf_paths_recursive(root: &Path, dir: &Path, out: &mut Vec<Vec<u8>>) {
    let mut entries: Vec<_> = fs::read_dir(dir)
        .expect("mounted directory should be readable")
        .filter_map(Result::ok)
        .collect();
    entries.sort_by(|left, right| left.file_name().cmp(&right.file_name()));

    for entry in entries {
        let path = entry.path();
        let file_type = entry.file_type().expect("entry type should be readable");
        if file_type.is_dir() {
            collect_mount_leaf_paths_recursive(root, &path, out);
        } else {
            let relative = path
                .strip_prefix(root)
                .expect("path should remain under root")
                .as_os_str()
                .as_bytes()
                .to_vec();
            out.push(relative);
        }
    }
}

fn unique_temp_path(prefix: &str) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("system time should be after the Unix epoch")
        .as_nanos();
    let mut path = std::env::temp_dir();
    path.push(OsString::from_vec(format!("{prefix}-{nanos}").into_bytes()));
    path
}

fn timefs_binary() -> OsString {
    OsString::from(env!("CARGO_BIN_EXE_timefs"))
}
