use std::ffi::{CString, OsStr, OsString};
use std::fs;
use std::io::Read;
use std::mem::MaybeUninit;
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};

#[test]
fn revision_resolvers_match_git_and_unmount_cleanly() {
    let fixture = FixtureRepository::new();
    let mountpoint = create_temp_dir("timefs-task4-mount");

    let mut mount = spawn_mount(fixture.path(), &mountpoint);
    wait_for_mount_ready(&mut mount, &mountpoint, b"now");

    assert_eq!(
        sorted_dir_entries(&mountpoint),
        vec![
            b"at".to_vec(),
            b"commits".to_vec(),
            b"history".to_vec(),
            b"now".to_vec(),
            b"refs".to_vec(),
        ]
    );
    assert!(
        fs::read_dir(mountpoint.join("at"))
            .expect("at/ should be readable")
            .next()
            .is_none(),
        "at/ should not enumerate revisions"
    );

    assert_revision_tree_matches(&fixture, &mountpoint.join("now"), "HEAD");
    assert_revision_tree_matches(&fixture, &mountpoint.join("at").join("HEAD~1"), "HEAD~1");
    assert_revision_tree_matches(&fixture, &mountpoint.join("at").join("topic"), "topic");
    assert_revision_tree_matches(&fixture, &mountpoint.join("at").join("v1.0"), "v1.0");

    let raw_sha = fixture.git_rev_parse("HEAD~1");
    assert_revision_tree_matches(
        &fixture,
        &mountpoint.join("at").join(raw_sha.as_str()),
        "HEAD~1",
    );

    let short_sha = fixture.git_rev_parse_short("HEAD~1");
    assert_revision_tree_matches(
        &fixture,
        &mountpoint.join("commits").join(short_sha.as_str()),
        "HEAD~1",
    );

    let exec_mode = fs::symlink_metadata(mountpoint.join("now").join("run.sh"))
        .expect("executable file metadata should be readable")
        .permissions()
        .mode();
    let regular_mode = fs::symlink_metadata(mountpoint.join("now").join("README.md"))
        .expect("regular file metadata should be readable")
        .permissions()
        .mode();
    assert_eq!(exec_mode & 0o111, 0o111);
    assert_eq!(regular_mode & 0o111, 0);

    assert_eq!(
        sorted_dir_entries(&mountpoint.join("refs").join("heads")),
        vec![b"feature".to_vec(), b"main".to_vec(), b"topic".to_vec()]
    );
    assert_revision_tree_matches(
        &fixture,
        &mountpoint
            .join("refs")
            .join("heads")
            .join("feature")
            .join("x"),
        "feature/x",
    );
    assert_eq!(
        fs::read(
            mountpoint
                .join("refs")
                .join("heads")
                .join("feature")
                .join("x")
                .join("topic.txt"),
        )
        .expect("feature/x topic.txt should be readable"),
        fixture.git_show_path("feature/x", b"topic.txt"),
    );

    let missing_revision = fs::metadata(mountpoint.join("at").join("does-not-exist"))
        .expect_err("missing revisions should return ENOENT");
    assert_eq!(missing_revision.raw_os_error(), Some(libc::ENOENT));

    run_timefs(["unmount", mountpoint.to_string_lossy().as_ref()]);
    wait_for_process_exit(
        &mut mount,
        "mount process should exit after explicit unmount",
    );
    wait_for_mount_cleared(&mountpoint);

    let sigint_mountpoint = create_temp_dir("timefs-task4-sigint");
    let mut sigint_mount = spawn_mount(fixture.path(), &sigint_mountpoint);
    wait_for_mount_ready(&mut sigint_mount, &sigint_mountpoint, b"now");
    send_sigint(sigint_mount.id());
    wait_for_process_exit(
        &mut sigint_mount,
        "mount process should exit cleanly on SIGINT",
    );
    wait_for_mount_cleared(&sigint_mountpoint);
}

#[test]
fn metadata_and_corrupt_objects_match_spec() {
    let fixture = MetadataFixtureRepository::new();
    let mountpoint = create_temp_dir("timefs-task5-mount");

    let mut mount = spawn_mount(fixture.path(), &mountpoint);
    wait_for_mount_ready(&mut mount, &mountpoint, b"now");

    let now_root = mountpoint.join("now");
    let head_root = mountpoint.join("at").join("HEAD");
    let readme = head_root.join("README.md");
    let run_script = now_root.join("run.sh");
    let link = now_root.join("link-to-script");
    let gitlink_dir = now_root.join("vendor").join("submodule");
    let gitlink_file = gitlink_dir.join(".gitlink");

    assert_eq!(statvfs_free_blocks(&mountpoint), (0, 0));

    let exec_mode = fs::symlink_metadata(&run_script)
        .expect("executable file metadata should be readable")
        .permissions()
        .mode();
    let regular_mode = fs::symlink_metadata(&readme)
        .expect("regular file metadata should be readable")
        .permissions()
        .mode();
    let symlink_mode = fs::symlink_metadata(&link)
        .expect("symlink metadata should be readable")
        .permissions()
        .mode();
    let gitlink_dir_mode = fs::symlink_metadata(&gitlink_dir)
        .expect("gitlink directory metadata should be readable")
        .permissions()
        .mode();
    let gitlink_file_mode = fs::symlink_metadata(&gitlink_file)
        .expect("gitlink placeholder metadata should be readable")
        .permissions()
        .mode();
    assert_eq!(exec_mode & 0o777, 0o555);
    assert_eq!(regular_mode & 0o777, 0o444);
    assert_eq!(symlink_mode & 0o777, 0o777);
    assert_eq!(gitlink_dir_mode & 0o777, 0o555);
    assert_eq!(gitlink_file_mode & 0o777, 0o444);

    assert_eq!(
        list_xattrs(&readme),
        vec![
            b"user.timefs.commit".to_vec(),
            b"user.timefs.mode".to_vec(),
            b"user.timefs.oid".to_vec(),
            b"user.timefs.type".to_vec(),
        ]
    );
    assert_eq!(
        get_xattr(&readme, "user.timefs.oid"),
        fixture.git_rev_parse("HEAD:README.md").into_bytes()
    );
    assert_eq!(get_xattr(&readme, "user.timefs.mode"), b"100644");
    assert_eq!(get_xattr(&readme, "user.timefs.type"), b"blob");
    assert_eq!(
        get_xattr(&readme, "user.timefs.commit"),
        fixture.git_rev_parse("HEAD^{commit}").into_bytes()
    );
    assert_eq!(
        get_xattr_error(&readme, "user.timefs.unknown"),
        libc::ENODATA
    );
    assert_eq!(
        get_xattr_small_buffer_error(&readme, "user.timefs.oid", 8),
        libc::ERANGE
    );

    assert_eq!(get_xattr(&gitlink_dir, "user.timefs.type"), b"gitlink");
    assert_eq!(
        get_xattr(&gitlink_dir, "user.timefs.oid"),
        fixture.submodule_head().as_bytes()
    );
    assert_eq!(
        fs::read(&gitlink_file).expect(".gitlink file should be readable"),
        format!("{}\n", fixture.submodule_head()).into_bytes()
    );

    let broken_error = fs::read(head_root.join("broken.txt"))
        .expect_err("corrupt blob reads should return an error");
    assert_eq!(broken_error.raw_os_error(), Some(libc::EIO));
    assert_eq!(
        fs::read(&readme).expect("healthy nodes should stay readable after EIO"),
        b"timefs metadata fixture\n"
    );

    run_timefs(["unmount", mountpoint.to_string_lossy().as_ref()]);
    wait_for_process_exit(
        &mut mount,
        "mount process should exit after explicit unmount",
    );
    wait_for_mount_cleared(&mountpoint);
}

#[test]
fn large_fixture_mount_walks_with_small_cache() {
    let fixture = LargeFixtureRepository::new();
    let mountpoint = create_temp_dir("timefs-task6-mount");

    let mut mount =
        spawn_mount_with_args(fixture.path(), &mountpoint, &["-f", "--cache-size", "1"]);
    wait_for_mount_ready(&mut mount, &mountpoint, b"now");

    let mounted_now = mountpoint.join("now");
    let first_walk = collect_mount_leaf_paths(&mounted_now);
    let second_walk = collect_mount_leaf_paths(&mounted_now);
    assert_eq!(first_walk.len(), fixture.file_count());
    assert_eq!(first_walk, second_walk);

    for path in fixture.sample_paths() {
        let mounted_path = mounted_now.join(OsString::from_vec(path.clone()));
        assert_eq!(
            fs::read(&mounted_path).expect("sample file should be readable"),
            fixture.git_show_path("HEAD", &path),
        );
    }

    run_timefs(["unmount", mountpoint.to_string_lossy().as_ref()]);
    wait_for_process_exit(
        &mut mount,
        "mount process should exit after explicit unmount",
    );
    wait_for_mount_cleared(&mountpoint);
}

#[test]
fn shallow_clone_missing_revisions_log_the_cause() {
    let fixture = ShallowCloneFixtureRepository::new();
    let mountpoint = create_temp_dir("timefs-task7-shallow");

    let mut mount =
        spawn_mount_with_cli_and_mount_args(fixture.path(), &mountpoint, &["-v"], &["-f"]);
    wait_for_mount_ready(&mut mount, &mountpoint, b"now");

    let error = fs::metadata(mountpoint.join("at").join("HEAD~1"))
        .expect_err("missing shallow revisions should return ENOENT");
    assert_eq!(error.raw_os_error(), Some(libc::ENOENT));

    run_timefs(["unmount", mountpoint.to_string_lossy().as_ref()]);
    let status = mount
        .wait()
        .expect("shallow-clone mount child status should be readable");
    assert!(status.success(), "shallow-clone mount should exit cleanly");
    wait_for_mount_cleared(&mountpoint);

    let stderr = child_stderr(&mut mount);
    assert!(
        stderr.contains("repository is shallow"),
        "expected shallow-clone log line in stderr, got: {stderr}"
    );
}

#[test]
fn lfs_mount_serves_pointer_or_local_content() {
    let fixture = LfsFixtureRepository::new();
    let mountpoint = create_temp_dir("timefs-task7-lfs");

    let mut pointer_mount = spawn_mount_with_args(fixture.path(), &mountpoint, &["-f"]);
    wait_for_mount_ready(&mut pointer_mount, &mountpoint, b"now");

    let pointer_asset = mountpoint.join("now").join("asset.bin");
    let missing_asset = mountpoint.join("now").join("missing.bin");
    assert_eq!(
        fs::read(&pointer_asset).expect("pointer asset should be readable"),
        fixture.pointer_contents("asset.bin")
    );
    assert_eq!(get_xattr(&pointer_asset, "user.timefs.lfs"), b"pointer");
    assert_eq!(
        fs::read(&missing_asset).expect("missing LFS object should still expose the pointer"),
        fixture.pointer_contents("missing.bin")
    );
    assert_eq!(get_xattr(&missing_asset, "user.timefs.lfs"), b"pointer");

    run_timefs(["unmount", mountpoint.to_string_lossy().as_ref()]);
    wait_for_process_exit(
        &mut pointer_mount,
        "pointer-mode mount should exit after unmount",
    );
    wait_for_mount_cleared(&mountpoint);

    let mut resolved_mount = spawn_mount_with_args(fixture.path(), &mountpoint, &["-f", "--lfs"]);
    wait_for_mount_ready(&mut resolved_mount, &mountpoint, b"now");

    let resolved_asset = mountpoint.join("now").join("asset.bin");
    assert_eq!(
        fs::read(&resolved_asset).expect("resolved LFS asset should be readable"),
        fixture.resolved_contents()
    );
    assert_eq!(get_xattr(&resolved_asset, "user.timefs.lfs"), b"resolved");
    assert_eq!(
        fs::read(&missing_asset).expect("missing LFS object should still expose the pointer"),
        fixture.pointer_contents("missing.bin")
    );
    assert_eq!(get_xattr(&missing_asset, "user.timefs.lfs"), b"pointer");

    run_timefs(["unmount", mountpoint.to_string_lossy().as_ref()]);
    wait_for_process_exit(
        &mut resolved_mount,
        "resolved-mode mount should exit after unmount",
    );
    wait_for_mount_cleared(&mountpoint);
}

#[test]
fn submodules_recurse_mounts_local_checkouts() {
    let fixture = MetadataFixtureRepository::new();
    let mountpoint = create_temp_dir("timefs-task7-submodules");

    let mut mount = spawn_mount_with_args(
        fixture.path(),
        &mountpoint,
        &["-f", "--submodules", "recurse"],
    );
    wait_for_mount_ready(&mut mount, &mountpoint, b"now");

    let submodule_root = mountpoint.join("now").join("vendor").join("submodule");
    let submodule_file = submodule_root.join("submodule.txt");
    assert_eq!(
        fs::read(&submodule_file).expect("recursed submodule file should be readable"),
        b"pinned submodule contents\n"
    );
    assert_eq!(get_xattr(&submodule_root, "user.timefs.type"), b"tree");
    assert_eq!(
        get_xattr(&submodule_file, "user.timefs.commit"),
        fixture.submodule_head().as_bytes()
    );

    let placeholder_error = fs::metadata(submodule_root.join(".gitlink"))
        .expect_err("recursed submodules should not expose the placeholder file");
    assert_eq!(placeholder_error.raw_os_error(), Some(libc::ENOENT));

    run_timefs(["unmount", mountpoint.to_string_lossy().as_ref()]);
    wait_for_process_exit(
        &mut mount,
        "submodule recurse mount should exit after unmount",
    );
    wait_for_mount_cleared(&mountpoint);
}

fn assert_revision_tree_matches(fixture: &FixtureRepository, 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}"),
        }
    }
}

struct FixtureRepository {
    root: PathBuf,
}

impl FixtureRepository {
    fn new() -> Self {
        let root = unique_temp_path("timefs-task4-fixture");
        fs::create_dir_all(root.join("nested")).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"]);

        fs::write(fixture.root.join("README.md"), b"timefs fixture\n")
            .expect("README write should succeed");
        fs::write(
            fixture.root.join("nested/data.bin"),
            [0_u8, 1, 2, 3, 0, 255],
        )
        .expect("binary write should succeed");
        fs::write(fixture.root.join("run.sh"), b"#!/bin/sh\necho timefs\n")
            .expect("script write should succeed");
        symlink("run.sh", fixture.root.join("link-to-script"))
            .expect("symlink creation should succeed");

        let mut permissions = fs::metadata(fixture.root.join("run.sh"))
            .expect("script metadata should be readable")
            .permissions();
        permissions.set_mode(0o755);
        fs::set_permissions(fixture.root.join("run.sh"), permissions)
            .expect("script permissions should be writable");

        fixture.git(["add", "."]);
        fixture.git(["commit", "-m", "Create fixture repository"]);
        fixture.git(["tag", "v1.0"]);

        fs::write(fixture.root.join("README.md"), b"timefs fixture v2\n")
            .expect("updated README write should succeed");
        fs::write(fixture.root.join("topic.txt"), b"branch point data\n")
            .expect("topic marker write should succeed");
        fixture.git(["add", "."]);
        fixture.git(["commit", "-m", "Prepare revision resolvers"]);
        fixture.git(["branch", "topic"]);
        fixture.git(["branch", "feature/x"]);

        fs::write(fixture.root.join("head-only.txt"), b"current HEAD\n")
            .expect("head marker write should succeed");
        fixture.git(["add", "."]);
        fixture.git(["commit", "-m", "Advance HEAD"]);

        fixture
    }

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

    fn git<I, S>(&self, args: I) -> Output
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        let output = Command::new("git")
            .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)
            );
        }
    }

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

    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
    }

    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()
    }

    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()
    }
}

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

struct MetadataFixtureRepository {
    root: PathBuf,
    submodule_root: PathBuf,
    submodule_head: String,
}

impl MetadataFixtureRepository {
    fn new() -> Self {
        let root = unique_temp_path("timefs-task5-fixture");
        fs::create_dir_all(&root).expect("fixture directory creation should succeed");

        let mut fixture = Self {
            root,
            submodule_root: unique_temp_path("timefs-task5-submodule"),
            submodule_head: String::new(),
        };
        fixture.git(["init", "-b", "main"]);
        fixture.git(["config", "user.name", "Timefs Tests"]);
        fixture.git(["config", "user.email", "timefs-tests@example.com"]);

        fs::write(fixture.root.join("README.md"), b"timefs metadata fixture\n")
            .expect("README write should succeed");
        fs::write(fixture.root.join("broken.txt"), b"broken blob contents\n")
            .expect("broken blob write should succeed");
        fs::write(fixture.root.join("run.sh"), b"#!/bin/sh\necho metadata\n")
            .expect("script write should succeed");
        symlink("run.sh", fixture.root.join("link-to-script"))
            .expect("symlink creation should succeed");

        let mut permissions = fs::metadata(fixture.root.join("run.sh"))
            .expect("script metadata should be readable")
            .permissions();
        permissions.set_mode(0o755);
        fs::set_permissions(fixture.root.join("run.sh"), permissions)
            .expect("script permissions should be writable");

        fixture.git(["add", "."]);
        fixture.git(["commit", "-m", "Create metadata fixture"]);

        let submodule_head = initialize_submodule_source(&fixture.submodule_root);
        fixture.git_with_options(
            ["-c", "protocol.file.allow=always"],
            [
                "submodule",
                "add",
                fixture.submodule_root.to_string_lossy().as_ref(),
                "vendor/submodule",
            ],
        );
        fixture.git(["add", ".gitmodules", "vendor/submodule"]);
        fixture.git(["commit", "-m", "Add submodule placeholder"]);

        fixture.submodule_head = submodule_head;
        fixture.remove_loose_blob("HEAD:broken.txt");
        fixture
    }

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

    fn submodule_head(&self) -> &str {
        &self.submodule_head
    }

    fn git<I, S>(&self, args: I) -> Output
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        let output = Command::new("git")
            .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)
            );
        }
    }

    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)
            );
        }
    }

    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()
    }

    fn remove_loose_blob(&self, spec: &str) {
        let oid = self.git_rev_parse(spec);
        let (prefix, suffix) = oid.split_at(2);
        let object_path = self
            .root
            .join(".git")
            .join("objects")
            .join(prefix)
            .join(suffix);
        assert!(
            object_path.exists(),
            "expected loose object at {}",
            object_path.display()
        );
        fs::remove_file(&object_path).expect("loose blob removal should succeed");
    }
}

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

struct LargeFixtureRepository {
    root: PathBuf,
    file_count: usize,
    sample_paths: Vec<Vec<u8>>,
}

impl LargeFixtureRepository {
    fn new() -> Self {
        let root = unique_temp_path("timefs-task6-fixture");
        fs::create_dir_all(&root).expect("fixture directory creation should succeed");

        let fixture = Self {
            root,
            file_count: 64 * 24,
            sample_paths: vec![
                b"tree-00/file-00.txt".to_vec(),
                b"tree-17/file-09.txt".to_vec(),
                b"tree-63/file-23.txt".to_vec(),
            ],
        };
        fixture.git(["init", "-b", "main"]);
        fixture.git(["config", "user.name", "Timefs Tests"]);
        fixture.git(["config", "user.email", "timefs-tests@example.com"]);

        for dir_index in 0..64_usize {
            let dir = fixture.root.join(format!("tree-{dir_index:02}"));
            fs::create_dir_all(&dir).expect("fixture subtree creation should succeed");
            for file_index in 0..24_usize {
                let contents = format!(
                    "tree={dir_index:02} file={file_index:02} {}\n",
                    "payload".repeat(32)
                );
                fs::write(dir.join(format!("file-{file_index:02}.txt")), contents)
                    .expect("fixture file write should succeed");
            }
        }

        fixture.git(["add", "."]);
        fixture.git(["commit", "-m", "Create large cache fixture"]);
        fixture
    }

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

    fn file_count(&self) -> usize {
        self.file_count
    }

    fn sample_paths(&self) -> Vec<Vec<u8>> {
        self.sample_paths.clone()
    }

    fn git<I, S>(&self, args: I) -> Output
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        let output = Command::new("git")
            .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)
            );
        }
    }

    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
    }
}

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

struct ShallowCloneFixtureRepository {
    source: PathBuf,
    clone: PathBuf,
}

impl ShallowCloneFixtureRepository {
    fn new() -> Self {
        let source = unique_temp_path("timefs-task7-shallow-source");
        fs::create_dir_all(&source).expect("shallow source creation should succeed");
        let clone = unique_temp_path("timefs-task7-shallow-clone");

        let fixture = Self { source, clone };
        fixture.git_source(["init", "-b", "main"]);
        fixture.git_source(["config", "user.name", "Timefs Tests"]);
        fixture.git_source(["config", "user.email", "timefs-tests@example.com"]);

        fs::write(fixture.source.join("README.md"), b"shallow fixture v1\n")
            .expect("initial shallow fixture write should succeed");
        fixture.git_source(["add", "."]);
        fixture.git_source(["commit", "-m", "Create shallow fixture"]);

        fs::write(fixture.source.join("README.md"), b"shallow fixture v2\n")
            .expect("second shallow fixture write should succeed");
        fixture.git_source(["add", "."]);
        fixture.git_source(["commit", "-m", "Advance shallow fixture"]);

        fs::write(fixture.source.join("README.md"), b"shallow fixture v3\n")
            .expect("third shallow fixture write should succeed");
        fixture.git_source(["add", "."]);
        fixture.git_source(["commit", "-m", "Advance shallow fixture again"]);

        let source_url = format!("file://{}", fixture.source.display());
        let output = Command::new("git")
            .args([
                "-c",
                "protocol.file.allow=always",
                "clone",
                "--no-local",
                "--depth",
                "1",
                source_url.as_str(),
                fixture.clone.to_string_lossy().as_ref(),
            ])
            .output()
            .expect("git clone should be executable in tests");
        if !output.status.success() {
            panic!(
                "git clone failed with status {}: {}",
                output.status,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        assert!(
            fixture.clone.join(".git").join("shallow").exists(),
            "expected a real shallow clone at {}",
            fixture.clone.display()
        );

        fixture
    }

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

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

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

struct LfsFixtureRepository {
    root: PathBuf,
    resolved_contents: Vec<u8>,
    asset_pointer: Vec<u8>,
    missing_pointer: Vec<u8>,
}

impl LfsFixtureRepository {
    fn new() -> Self {
        let root = unique_temp_path("timefs-task7-lfs-fixture");
        fs::create_dir_all(&root).expect("LFS fixture directory creation should succeed");

        let resolved_contents = b"real lfs payload\n".to_vec();
        let resolved_oid =
            String::from("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
        let missing_oid =
            String::from("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
        let asset_pointer = lfs_pointer_text(&resolved_oid, resolved_contents.len());
        let missing_pointer = lfs_pointer_text(&missing_oid, 27);

        let fixture = Self {
            root,
            resolved_contents,
            asset_pointer: asset_pointer.clone(),
            missing_pointer: missing_pointer.clone(),
        };
        fixture.git(["init", "-b", "main"]);
        fixture.git(["config", "user.name", "Timefs Tests"]);
        fixture.git(["config", "user.email", "timefs-tests@example.com"]);

        fs::write(fixture.root.join("asset.bin"), asset_pointer)
            .expect("LFS pointer write should succeed");
        fs::write(fixture.root.join("missing.bin"), missing_pointer)
            .expect("missing LFS pointer write should succeed");
        fixture.git(["add", "."]);
        fixture.git(["commit", "-m", "Create LFS pointer fixture"]);

        let lfs_object_dir = fixture
            .root
            .join(".git")
            .join("lfs")
            .join("objects")
            .join(&resolved_oid[..2])
            .join(&resolved_oid[2..4]);
        fs::create_dir_all(&lfs_object_dir).expect("LFS object directory creation should succeed");
        fs::write(
            lfs_object_dir.join(&resolved_oid),
            &fixture.resolved_contents,
        )
        .expect("LFS object write should succeed");

        fixture
    }

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

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

    fn pointer_contents(&self, name: &str) -> Vec<u8> {
        match name {
            "asset.bin" => self.asset_pointer.clone(),
            "missing.bin" => self.missing_pointer.clone(),
            other => panic!("unexpected pointer file requested: {other}"),
        }
    }

    fn resolved_contents(&self) -> Vec<u8> {
        self.resolved_contents.clone()
    }
}

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

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

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 spawn_mount(repo: &Path, mountpoint: &Path) -> Child {
    spawn_mount_with_args(repo, mountpoint, &["-f"])
}

fn spawn_mount_with_args(repo: &Path, mountpoint: &Path, extra_args: &[&str]) -> Child {
    spawn_mount_with_cli_and_mount_args(repo, mountpoint, &[], extra_args)
}

fn spawn_mount_with_cli_and_mount_args(
    repo: &Path,
    mountpoint: &Path,
    cli_args: &[&str],
    extra_args: &[&str],
) -> Child {
    Command::new(timefs_binary())
        .args(cli_args)
        .arg("mount")
        .arg(repo)
        .arg(mountpoint)
        .args(extra_args)
        .stdout(Stdio::null())
        .stderr(Stdio::piped())
        .spawn()
        .expect("timefs mount should spawn")
}

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_ready(child: &mut Child, mountpoint: &Path, expected_name: &[u8]) {
    for _ in 0..120 {
        if let Ok(entries) = fs::read_dir(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(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 {}",
        mountpoint.display()
    );
}

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 wait_for_process_exit(child: &mut Child, message: &str) {
    let status = child.wait().expect("mount child status should be readable");
    assert!(status.success(), "{message}: {}", child_stderr(child));
}

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 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)
        );
    }
}

fn lfs_pointer_text(oid: &str, size: usize) -> Vec<u8> {
    format!("version https://git-lfs.github.com/spec/v1\noid sha256:{oid}\nsize {size}\n")
        .into_bytes()
}

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
}

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 sorted_dir_entries(path: &Path) -> Vec<Vec<u8>> {
    let mut names: Vec<Vec<u8>> = fs::read_dir(path)
        .expect("directory should be readable")
        .filter_map(Result::ok)
        .map(|entry| entry.file_name().into_vec())
        .collect();
    names.sort();
    names
}

fn send_sigint(pid: u32) {
    let status = Command::new("kill")
        .arg("-INT")
        .arg(pid.to_string())
        .status()
        .expect("kill should be executable");
    assert!(
        status.success(),
        "failed to deliver SIGINT to mount process"
    );
}

fn initialize_submodule_source(root: &Path) -> String {
    fs::create_dir_all(root).expect("submodule fixture directory creation should succeed");

    let run_git = |args: &[&str]| -> Output {
        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)
            );
        }
    };

    run_git(&["init", "-b", "main"]);
    run_git(&["config", "user.name", "Timefs Tests"]);
    run_git(&["config", "user.email", "timefs-tests@example.com"]);

    fs::write(root.join("submodule.txt"), b"pinned submodule contents\n")
        .expect("submodule contents write should succeed");
    run_git(&["add", "."]);
    run_git(&["commit", "-m", "Create submodule fixture"]);

    String::from_utf8(run_git(&["rev-parse", "HEAD"]).stdout)
        .expect("submodule rev-parse output should be valid UTF-8")
        .trim()
        .to_owned()
}

fn get_xattr(path: &Path, name: &str) -> Vec<u8> {
    let path = path_cstring(path);
    let name = CString::new(name).expect("xattr names should not contain NUL bytes");

    // Safety: the pointers are derived from live `CString` values and the kernel
    // fills at most the provided buffer size.
    let size = unsafe { libc::getxattr(path.as_ptr(), name.as_ptr(), std::ptr::null_mut(), 0) };
    assert!(size >= 0, "getxattr size probe should succeed");

    let size = usize::try_from(size).expect("xattr length should fit in memory");
    let mut value = vec![0_u8; size];
    // Safety: `value` is valid writable memory for `size` bytes and both C strings
    // remain alive for the duration of the syscall.
    let written = unsafe {
        libc::getxattr(
            path.as_ptr(),
            name.as_ptr(),
            value.as_mut_ptr().cast(),
            value.len(),
        )
    };
    assert_eq!(
        usize::try_from(written).expect("written xattr size should be non-negative"),
        value.len()
    );
    value
}

fn get_xattr_error(path: &Path, name: &str) -> i32 {
    let path = path_cstring(path);
    let name = CString::new(name).expect("xattr names should not contain NUL bytes");

    // Safety: both pointers are valid NUL-terminated strings and no output buffer is used.
    let result = unsafe { libc::getxattr(path.as_ptr(), name.as_ptr(), std::ptr::null_mut(), 0) };
    assert_eq!(result, -1, "missing xattrs should report an error");
    last_errno()
}

fn get_xattr_small_buffer_error(path: &Path, name: &str, size: usize) -> i32 {
    let path = path_cstring(path);
    let name = CString::new(name).expect("xattr names should not contain NUL bytes");
    let mut value = vec![0_u8; size];

    // Safety: `value` is valid writable memory for `size` bytes and both C strings
    // remain alive for the duration of the syscall.
    let result = unsafe {
        libc::getxattr(
            path.as_ptr(),
            name.as_ptr(),
            value.as_mut_ptr().cast(),
            value.len(),
        )
    };
    assert_eq!(
        result, -1,
        "undersized xattr buffers should report an error"
    );
    last_errno()
}

fn list_xattrs(path: &Path) -> Vec<Vec<u8>> {
    let path = path_cstring(path);

    // Safety: `path` is a valid NUL-terminated string and no output buffer is used.
    let size = unsafe { libc::listxattr(path.as_ptr(), std::ptr::null_mut(), 0) };
    assert!(size >= 0, "listxattr size probe should succeed");

    let size = usize::try_from(size).expect("xattr list length should fit in memory");
    let mut value = vec![0_u8; size];
    // Safety: `value` is valid writable memory for `size` bytes and `path` remains alive
    // for the duration of the syscall.
    let written = unsafe { libc::listxattr(path.as_ptr(), value.as_mut_ptr().cast(), value.len()) };
    assert_eq!(
        usize::try_from(written).expect("written xattr list should be non-negative"),
        value.len()
    );

    let mut names: Vec<Vec<u8>> = value
        .split(|byte| *byte == 0)
        .filter(|name| !name.is_empty())
        .map(<[u8]>::to_vec)
        .collect();
    names.sort();
    names
}

fn statvfs_free_blocks(path: &Path) -> (u64, u64) {
    let path = path_cstring(path);
    let mut stat = MaybeUninit::<libc::statvfs>::uninit();

    // Safety: `path` is a valid NUL-terminated string and `stat` points to writable memory.
    let result = unsafe { libc::statvfs(path.as_ptr(), stat.as_mut_ptr()) };
    assert_eq!(result, 0, "statvfs should succeed");
    // Safety: a successful `statvfs` call initializes the structure fully.
    let stat = unsafe { stat.assume_init() };

    (
        u64::try_from(stat.f_bfree).expect("free block count should fit in u64"),
        u64::try_from(stat.f_bavail).expect("available block count should fit in u64"),
    )
}

fn path_cstring(path: &Path) -> CString {
    CString::new(path.as_os_str().as_bytes()).expect("test paths should not contain NUL bytes")
}

fn last_errno() -> i32 {
    std::io::Error::last_os_error()
        .raw_os_error()
        .unwrap_or_default()
}

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
}

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"))
}
