mod support;

use self::support::{
    assert_revision_tree_matches_git, make_executable, with_mounted_timefs, write_symlink,
    RepositoryFixture,
};
use std::collections::HashSet;
use std::fs;
use std::os::unix::ffi::OsStringExt;
use std::path::Path;

#[test]
fn random_tree_shapes_match_git_ground_truth() {
    for seed in 0_u64..6 {
        let fixture = randomized_fixture(seed);
        let (_value, mount) = with_mounted_timefs(fixture.path(), &[], &["-f"], |mounted| {
            assert_revision_tree_matches_git(&fixture, &mounted.path().join("now"), "HEAD");
            assert_revision_tree_matches_git(
                &fixture,
                &mounted.path().join("at").join("HEAD~1"),
                "HEAD~1",
            );
            assert_revision_tree_matches_git(
                &fixture,
                &mounted.path().join("at").join("HEAD~2"),
                "HEAD~2",
            );
        });

        assert!(
            mount.stderr.is_empty(),
            "randomized fixture {seed} emitted unexpected stderr: {}",
            mount.stderr
        );
    }
}

fn randomized_fixture(seed: u64) -> RepositoryFixture {
    let fixture = RepositoryFixture::new("timefs-task8-randomized");
    let mut rng = SimpleRng::new(seed ^ 0x5eed_fade_cafe_beef);

    for generation in 0..3_u32 {
        fixture.remove_worktree_entries();
        populate_random_tree(&fixture, &mut rng, generation);
        fixture.git(["add", "-A"]);
        let message = format!("Random tree generation {generation}");
        fixture.git(["commit", "-m", message.as_str()]);
    }

    fixture
}

fn populate_random_tree(fixture: &RepositoryFixture, rng: &mut SimpleRng, generation: u32) {
    let entry_count = 12 + rng.range_usize(0, 8);
    let mut seen = HashSet::new();

    for index in 0..entry_count {
        let relative = unique_random_path(rng, generation, index, &mut seen);
        let full_path = fixture.path().join(std::ffi::OsString::from_vec(relative));
        if let Some(parent) = full_path.parent() {
            fs::create_dir_all(parent)
                .expect("randomized fixture parent directory creation should succeed");
        }

        match rng.range_usize(0, 3) {
            0 => {
                let len = 1 + rng.range_usize(0, 48);
                fs::write(&full_path, random_bytes(rng, len))
                    .expect("randomized blob write should succeed");
            }
            1 => {
                let len = 1 + rng.range_usize(0, 48);
                fs::write(&full_path, random_bytes(rng, len))
                    .expect("randomized executable write should succeed");
                make_executable(&full_path);
            }
            _ => {
                let target = format!("target-{}-{}", generation, rng.range_usize(0, 1_000));
                write_symlink(&target, &full_path);
            }
        }
    }
}

fn unique_random_path(
    rng: &mut SimpleRng,
    generation: u32,
    index: usize,
    seen: &mut HashSet<Vec<u8>>,
) -> Vec<u8> {
    loop {
        let depth = rng.range_usize(0, 3);
        let mut path = Vec::new();
        for level in 0..depth {
            if !path.is_empty() {
                path.push(b'/');
            }
            path.extend_from_slice(
                format!("dir-{generation}-{level}-{}", rng.range_usize(0, 64)).as_bytes(),
            );
        }
        if !path.is_empty() {
            path.push(b'/');
        }
        path.extend_from_slice(
            format!("leaf-{generation}-{index}-{}", rng.range_usize(0, 4_096)).as_bytes(),
        );

        if seen.insert(path.clone()) {
            return path;
        }
    }
}

fn random_bytes(rng: &mut SimpleRng, len: usize) -> Vec<u8> {
    (0..len)
        .map(|_| u8::try_from(32 + rng.range_usize(0, 95)).expect("ASCII byte range fits in u8"))
        .collect()
}

struct SimpleRng(u64);

impl SimpleRng {
    fn new(seed: u64) -> Self {
        Self(seed)
    }

    fn next_u64(&mut self) -> u64 {
        self.0 = self
            .0
            .wrapping_mul(6_364_136_223_846_793_005)
            .wrapping_add(1_442_695_040_888_963_407);
        self.0
    }

    fn range_usize(&mut self, start: usize, end: usize) -> usize {
        assert!(start < end, "invalid random range");
        let span = end - start;
        start + (self.next_u64() as usize % span)
    }
}

#[allow(dead_code)]
fn _assert_exists(path: &Path) {
    assert!(path.exists(), "expected path to exist: {}", path.display());
}
