use std::path::PathBuf;
use tar::{Builder, Header, EntryType};
use relicario_core::safe_unpack_git_archive;

/// Craft a raw POSIX ustar tar with a single entry using the given raw path bytes.
/// The tar crate's `Builder` sanitises paths, so we write the 512-byte header
/// manually to produce truly malicious archives.
fn raw_tar_with_path(raw_path: &[u8], content: &[u8]) -> Vec<u8> {
    let mut buf = vec![0u8; 512]; // one header block

    // Bytes 0-99: name field (null-padded)
    let name_len = raw_path.len().min(100);
    buf[..name_len].copy_from_slice(&raw_path[..name_len]);

    // Bytes 100-107: mode = "0000644\0"
    buf[100..108].copy_from_slice(b"0000644\0");

    // Bytes 108-115: uid
    buf[108..116].copy_from_slice(b"0000000\0");

    // Bytes 116-123: gid
    buf[116..124].copy_from_slice(b"0000000\0");

    // Bytes 124-135: size (octal, 11 digits + null)
    let size_str = format!("{:011o}\0", content.len());
    buf[124..136].copy_from_slice(size_str.as_bytes());

    // Bytes 136-147: mtime
    buf[136..148].copy_from_slice(b"00000000000\0");

    // Bytes 148-155: checksum placeholder (spaces during compute)
    buf[148..156].copy_from_slice(b"        ");

    // Byte 156: typeflag = '0' (regular file)
    buf[156] = b'0';

    // Bytes 257-262: magic "ustar\0"
    buf[257..263].copy_from_slice(b"ustar\0");
    // Bytes 263-264: version "00"
    buf[263..265].copy_from_slice(b"00");

    // Compute checksum (sum of all bytes, checksum field treated as spaces).
    let checksum: u32 = buf.iter().map(|&b| b as u32).sum();
    let cksum_str = format!("{:06o}\0 ", checksum);
    buf[148..156].copy_from_slice(cksum_str.as_bytes());

    // Append padded content blocks.
    let mut out = buf;
    if !content.is_empty() {
        out.extend_from_slice(content);
        // Pad to 512-byte boundary.
        let remainder = content.len() % 512;
        if remainder != 0 {
            out.extend(vec![0u8; 512 - remainder]);
        }
    }

    // Two zero blocks = end-of-archive.
    out.extend(vec![0u8; 1024]);
    out
}

/// Build a tar with a raw symlink entry (typeflag = '2').
fn raw_symlink_tar() -> Vec<u8> {
    let mut buf = vec![0u8; 512];

    // name
    buf[..9].copy_from_slice(b"evil_link");
    // mode
    buf[100..108].copy_from_slice(b"0000755\0");
    // uid/gid
    buf[108..116].copy_from_slice(b"0000000\0");
    buf[116..124].copy_from_slice(b"0000000\0");
    // size = 0
    buf[124..136].copy_from_slice(b"00000000000\0");
    // mtime
    buf[136..148].copy_from_slice(b"00000000000\0");
    // checksum placeholder
    buf[148..156].copy_from_slice(b"        ");
    // typeflag = '2' (symlink)
    buf[156] = b'2';
    // linkname
    let target = b"/etc/passwd";
    buf[157..157 + target.len()].copy_from_slice(target);
    // magic
    buf[257..263].copy_from_slice(b"ustar\0");
    buf[263..265].copy_from_slice(b"00");

    // Compute checksum.
    let checksum: u32 = buf.iter().map(|&b| b as u32).sum();
    let cksum_str = format!("{:06o}\0 ", checksum);
    buf[148..156].copy_from_slice(cksum_str.as_bytes());

    let mut out = buf;
    out.extend(vec![0u8; 1024]); // end-of-archive
    out
}

fn build_normal_tar() -> Vec<u8> {
    let mut buf = Vec::new();
    {
        let mut builder = Builder::new(&mut buf);
        let content = b"hello";
        let mut header = Header::new_gnu();
        header.set_entry_type(EntryType::Regular);
        header.set_size(content.len() as u64);
        header.set_cksum();
        builder
            .append_data(&mut header, "subdir/hello.txt", content.as_ref())
            .unwrap();
        builder.finish().unwrap();
    }
    buf
}

fn build_oversize_tar() -> Vec<u8> {
    // Actual 2048-byte body; test will use cap=1024
    let mut buf = Vec::new();
    {
        let mut builder = Builder::new(&mut buf);
        let content = vec![0u8; 2048];
        let mut header = Header::new_gnu();
        header.set_entry_type(EntryType::Regular);
        header.set_size(content.len() as u64);
        header.set_cksum();
        builder
            .append_data(&mut header, "bigfile.bin", content.as_slice())
            .unwrap();
        builder.finish().unwrap();
    }
    buf
}

#[test]
fn restore_rejects_path_traversal() {
    // Craft a tar with "../../escaped.txt" using raw bytes (Builder sanitises paths).
    let bytes = raw_tar_with_path(b"../../escaped.txt", b"evil content");
    let err = safe_unpack_git_archive(&bytes, 1024 * 1024).unwrap_err();
    let msg = format!("{err:#}");
    assert!(
        msg.contains("path traversal") || msg.contains(".."),
        "got: {msg}"
    );
}

#[test]
fn restore_rejects_absolute_path() {
    // Craft a tar with "/etc/escaped.txt" using raw bytes.
    let bytes = raw_tar_with_path(b"/etc/escaped.txt", b"evil content");
    let err = safe_unpack_git_archive(&bytes, 1024 * 1024).unwrap_err();
    let msg = format!("{err:#}");
    assert!(
        msg.contains("path traversal") || msg.contains("absolute"),
        "got: {msg}"
    );
}

#[test]
fn restore_rejects_symlink() {
    let bytes = raw_symlink_tar();
    let err = safe_unpack_git_archive(&bytes, 1024 * 1024).unwrap_err();
    let msg = format!("{err:#}");
    assert!(
        msg.contains("symlink") || msg.contains("link"),
        "got: {msg}"
    );
}

#[test]
fn restore_rejects_size_bomb() {
    let bytes = build_oversize_tar(); // actual 2048-byte entry
    let err = safe_unpack_git_archive(&bytes, 1024).unwrap_err(); // cap = 1024 bytes
    let msg = format!("{err:#}");
    assert!(
        msg.contains("size") || msg.contains("cap") || msg.contains("too large"),
        "got: {msg}"
    );
}

#[test]
fn restore_accepts_normal_files() {
    let buf = build_normal_tar();
    let entries = safe_unpack_git_archive(&buf, 1024 * 1024).expect("happy path");
    assert_eq!(entries.len(), 1);
    assert_eq!(entries[0].0, PathBuf::from("subdir/hello.txt"));
    assert_eq!(entries[0].1, b"hello");
}
