use std::path::PathBuf;
use std::sync::Arc;

use sqpack_rs::dat::BLOCK_HEADER_SIZE;
use sqpack_rs::index::INDEX_ENTRY_SIZE;
use sqpack_rs::index2::INDEX2_ENTRY_SIZE;
use sqpack_rs::vfs::InMemoryFs;
use sqpack_rs::{Error, FileKind, SqPack, SqPackPayload, Vfs};

const SQPACK_HEADER_SIZE: u32 = 1024;
const BLOCK_TYPE_UNCOMPRESSED: u32 = 32000;
const SQPACK_MAGIC: [u8; 8] = [0x53, 0x71, 0x50, 0x61, 0x63, 0x6b, 0x00, 0x00];

fn make_sqpack_header(file_type: u32) -> Vec<u8> {
    let mut buf = Vec::with_capacity(SQPACK_HEADER_SIZE as usize);
    buf.extend_from_slice(&SQPACK_MAGIC);
    buf.push(0);
    buf.extend_from_slice(&[0u8; 3]);
    buf.extend_from_slice(&SQPACK_HEADER_SIZE.to_le_bytes());
    buf.extend_from_slice(&1u32.to_le_bytes());
    buf.extend_from_slice(&file_type.to_le_bytes());
    buf.resize(SQPACK_HEADER_SIZE as usize, 0);
    buf
}

fn make_index_header(
    index_data_offset: u32,
    index_data_size: u32,
    number_of_data_file: u32,
) -> Vec<u8> {
    let mut buf = Vec::with_capacity(1024);
    buf.extend_from_slice(&1024u32.to_le_bytes());
    buf.extend_from_slice(&1u32.to_le_bytes());
    buf.extend_from_slice(&index_data_offset.to_le_bytes());
    buf.extend_from_slice(&index_data_size.to_le_bytes());
    buf.resize(buf.len() + 64, 0);
    buf.extend_from_slice(&number_of_data_file.to_le_bytes());
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.resize(buf.len() + 64, 0);
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.resize(buf.len() + 64, 0);
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.resize(buf.len() + 64, 0);
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.resize(buf.len() + 656, 0);
    buf.resize(buf.len() + 64, 0);
    assert_eq!(buf.len(), 1024);
    buf
}

fn make_index_file(entries: &[(u64, u32)]) -> Vec<u8> {
    make_index_file_n(entries, 1)
}

fn make_index_file_n(entries: &[(u64, u32)], number_of_data_file: u32) -> Vec<u8> {
    let mut buf = Vec::new();
    buf.extend_from_slice(&make_sqpack_header(2));
    let index_data_offset = SQPACK_HEADER_SIZE + 1024;
    let index_data_size = u32::try_from(entries.len()).unwrap() * INDEX_ENTRY_SIZE;
    buf.extend_from_slice(&make_index_header(
        index_data_offset,
        index_data_size,
        number_of_data_file,
    ));
    for (hash, data) in entries {
        buf.extend_from_slice(&hash.to_le_bytes());
        buf.extend_from_slice(&data.to_le_bytes());
        buf.extend_from_slice(&0u32.to_le_bytes());
    }
    buf
}

fn make_index2_file(entries: &[(u32, u32)]) -> Vec<u8> {
    let mut buf = Vec::new();
    buf.extend_from_slice(&make_sqpack_header(2));
    let index_data_offset = SQPACK_HEADER_SIZE + 1024;
    let index_data_size = u32::try_from(entries.len()).unwrap() * INDEX2_ENTRY_SIZE;
    buf.extend_from_slice(&make_index_header(index_data_offset, index_data_size, 1));
    for (hash, data) in entries {
        buf.extend_from_slice(&hash.to_le_bytes());
        buf.extend_from_slice(&data.to_le_bytes());
    }
    buf
}

fn make_standard_payload(payload: &[u8]) -> Vec<u8> {
    let mut buf = Vec::new();
    let raw_file_size = u32::try_from(payload.len()).unwrap();
    buf.extend_from_slice(&32u32.to_le_bytes());
    buf.extend_from_slice(&2u32.to_le_bytes());
    buf.extend_from_slice(&raw_file_size.to_le_bytes());
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.extend_from_slice(&1u32.to_le_bytes());
    let compressed_size = u16::try_from(BLOCK_HEADER_SIZE as usize + payload.len()).unwrap();
    let uncompressed_size = u16::try_from(payload.len()).unwrap();
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.extend_from_slice(&compressed_size.to_le_bytes());
    buf.extend_from_slice(&uncompressed_size.to_le_bytes());
    buf.extend_from_slice(&BLOCK_HEADER_SIZE.to_le_bytes());
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.extend_from_slice(&BLOCK_TYPE_UNCOMPRESSED.to_le_bytes());
    buf.extend_from_slice(&u32::from(uncompressed_size).to_le_bytes());
    buf.extend_from_slice(payload);
    buf
}

fn make_standard_dat(payload: &[u8]) -> Vec<u8> {
    let mut buf = make_sqpack_header(1);
    buf.extend_from_slice(&make_standard_payload(payload));
    buf
}

fn hash_index1_key_for_path(path: &str) -> u64 {
    sqpack_rs::hash::hash_index1_key(path)
}

fn hash_index2_for_path(path: &str) -> u32 {
    sqpack_rs::hash::hash_index2(path)
}

#[test]
fn vfs_open_in_for_index_round_trips() {
    let fs = InMemoryFs::new();
    let path = PathBuf::from("/install/sqpack/ffxiv/0a0000.win32.index");
    fs.insert(&path, make_index_file(&[(0x42u64, 0x20u32)]));

    let idx = sqpack_rs::Index::open_in(&path, &fs).expect("Index::open_in");
    assert_eq!(idx.len(), 1);
    let e = idx.get(0x42).expect("entry present");
    assert_eq!(e.byte_offset(), 0x20u64 * 8);
}

#[test]
fn vfs_open_in_for_index2_round_trips() {
    let fs = InMemoryFs::new();
    let path = PathBuf::from("/install/sqpack/ffxiv/0a0000.win32.index2");
    fs.insert(&path, make_index2_file(&[(0xDEAD_BEEFu32, 0x40u32)]));

    let idx = sqpack_rs::Index2::open_in(&path, &fs).expect("Index2::open_in");
    assert_eq!(idx.len(), 1);
    let e = idx.get(0xDEAD_BEEF).expect("entry present");
    assert_eq!(e.byte_offset(), 0x40u64 * 8);
}

#[test]
fn vfs_open_in_for_dat_round_trips() {
    let fs = InMemoryFs::new();
    let path = PathBuf::from("/install/sqpack/ffxiv/0a0000.win32.dat0");
    let payload = b"hello-vfs-dat".to_vec();
    fs.insert(&path, make_standard_dat(&payload));

    let mut dat = sqpack_rs::DatFile::open_in(&path, &fs).expect("DatFile::open_in");
    let out = dat
        .read_file(u64::from(SQPACK_HEADER_SIZE))
        .expect("read_file");
    assert_eq!(out, payload);
}

#[test]
fn vfs_sqpack_open_with_vfs_validates_sqpack_dir() {
    let fs = InMemoryFs::new();
    fs.insert("/install/sqpack/ffxiv/marker", b"x".to_vec());

    let sq =
        SqPack::open_with_vfs("/install", Arc::new(fs.clone())).expect("SqPack::open_with_vfs");
    assert_eq!(sq.install_root(), std::path::Path::new("/install"));
}

#[test]
fn vfs_sqpack_open_with_vfs_rejects_missing_sqpack_dir() {
    let fs = InMemoryFs::new();
    let err = SqPack::open_with_vfs("/missing", Arc::new(fs)).unwrap_err();
    match err {
        Error::Io { path, .. } => {
            let p = path.expect("Io error carries the path");
            assert!(p.ends_with("sqpack"));
        }
        other => panic!("expected Io error, got {other:?}"),
    }
}

#[test]
fn vfs_sqpack_read_file_end_to_end() {
    let fs = InMemoryFs::new();

    let payload = b"hello-vfs-from-sqpack".to_vec();
    let dat_buf = make_standard_dat(&payload);
    let info_off = u64::from(SQPACK_HEADER_SIZE);

    let v1_key = hash_index1_key_for_path("exd/root.exl");
    let data = u32::try_from(info_off / 8).unwrap();

    fs.insert(
        "/install/sqpack/ffxiv/0a0000.win32.index",
        make_index_file(&[(v1_key, data)]),
    );
    fs.insert("/install/sqpack/ffxiv/0a0000.win32.dat0", dat_buf);

    let sq = SqPack::open_with_vfs("/install", Arc::new(fs)).expect("open");
    let bytes = sq.read_file("exd/root.exl").expect("read_file");
    assert_eq!(bytes, payload);
}

#[test]
fn vfs_sqpack_read_file_typed_end_to_end() {
    let fs = InMemoryFs::new();
    let payload = b"typed".repeat(4);
    let dat_buf = make_standard_dat(&payload);
    let info_off = u64::from(SQPACK_HEADER_SIZE);
    let v1_key = hash_index1_key_for_path("exd/root.exl");
    let data = u32::try_from(info_off / 8).unwrap();
    fs.insert(
        "/install/sqpack/ffxiv/0a0000.win32.index",
        make_index_file(&[(v1_key, data)]),
    );
    fs.insert("/install/sqpack/ffxiv/0a0000.win32.dat0", dat_buf);

    let sq = SqPack::open_with_vfs("/install", Arc::new(fs)).expect("open");
    let p = sq.read_file_typed("exd/root.exl").expect("read_file_typed");
    match p {
        SqPackPayload::Standard(bytes) => assert_eq!(bytes, payload),
        other => panic!("expected Standard, got {other:?}"),
    }
}

#[test]
fn vfs_sqpack_peek_kind_end_to_end() {
    let fs = InMemoryFs::new();
    let payload = b"peeked".to_vec();
    let dat_buf = make_standard_dat(&payload);
    let info_off = u64::from(SQPACK_HEADER_SIZE);
    let v1_key = hash_index1_key_for_path("exd/root.exl");
    let data = u32::try_from(info_off / 8).unwrap();
    fs.insert(
        "/install/sqpack/ffxiv/0a0000.win32.index",
        make_index_file(&[(v1_key, data)]),
    );
    fs.insert("/install/sqpack/ffxiv/0a0000.win32.dat0", dat_buf);

    let sq = SqPack::open_with_vfs("/install", Arc::new(fs)).expect("open");
    let kind = sq.peek_kind("exd/root.exl").expect("peek_kind");
    assert_eq!(kind, FileKind::Standard);
}

#[test]
fn vfs_sqpack_read_file_streaming_end_to_end() {
    let fs = InMemoryFs::new();
    let payload = (0u8..64).collect::<Vec<_>>();
    let dat_buf = make_standard_dat(&payload);
    let info_off = u64::from(SQPACK_HEADER_SIZE);
    let v1_key = hash_index1_key_for_path("exd/root.exl");
    let data = u32::try_from(info_off / 8).unwrap();
    fs.insert(
        "/install/sqpack/ffxiv/0a0000.win32.index",
        make_index_file(&[(v1_key, data)]),
    );
    fs.insert("/install/sqpack/ffxiv/0a0000.win32.dat0", dat_buf);

    let sq = SqPack::open_with_vfs("/install", Arc::new(fs)).expect("open");
    let stream = sq.read_file_streaming("exd/root.exl").expect("streaming");

    let mut concatenated = Vec::new();
    for chunk in stream {
        let chunk = chunk.expect("decode block");
        concatenated.extend_from_slice(&chunk.bytes);
    }
    assert_eq!(concatenated, payload);
}

#[test]
fn vfs_sqpack_enumerate_archives_end_to_end() {
    let fs = InMemoryFs::new();
    let v1_key = hash_index1_key_for_path("exd/root.exl");
    let v2_key = hash_index2_for_path("exd/root.exl");
    fs.insert(
        "/install/sqpack/ffxiv/0a0000.win32.index",
        make_index_file(&[(v1_key, 0x10)]),
    );
    fs.insert(
        "/install/sqpack/ffxiv/0a0000.win32.index2",
        make_index2_file(&[(v2_key, 0x10)]),
    );

    let sq = SqPack::open_with_vfs("/install", Arc::new(fs)).expect("open");
    let archives: Vec<_> = sq
        .enumerate_archives()
        .collect::<Result<Vec<_>, _>>()
        .expect("enumerate");
    assert_eq!(archives.len(), 2, "expected V1 + V2 yields");
}

#[test]
fn vfs_sqpack_read_file_routes_to_dat1_without_sqpack_header() {
    let fs = InMemoryFs::new();

    let dat0_payload = b"contents-of-a".to_vec();
    let dat0_buf = make_standard_dat(&dat0_payload);
    let dat0_info_off = u64::from(SQPACK_HEADER_SIZE);

    let dat1_payload = b"contents-of-b-in-dat1".to_vec();
    let dat1_buf = make_standard_payload(&dat1_payload);
    let dat1_info_off = 0u64;

    let v1_key_a = hash_index1_key_for_path("exd/a.exl");
    let data_a = u32::try_from(dat0_info_off / 8).unwrap();
    let v1_key_b = hash_index1_key_for_path("exd/b.exl");
    let data_b = u32::try_from(dat1_info_off / 8).unwrap() | (1 << 1);

    fs.insert(
        "/install/sqpack/ffxiv/0a0000.win32.index",
        make_index_file_n(&[(v1_key_a, data_a), (v1_key_b, data_b)], 2),
    );
    fs.insert("/install/sqpack/ffxiv/0a0000.win32.dat0", dat0_buf);
    fs.insert("/install/sqpack/ffxiv/0a0000.win32.dat1", dat1_buf);

    let sq = SqPack::open_with_vfs("/install", Arc::new(fs)).expect("open");
    let a_bytes = sq.read_file("exd/a.exl").expect("read_file .dat0 entry");
    assert_eq!(a_bytes, dat0_payload);
    let b_bytes = sq.read_file("exd/b.exl").expect("read_file .dat1 entry");
    assert_eq!(b_bytes, dat1_payload);
}

#[test]
fn vfs_garbage_index_bytes_surface_as_invalid_magic() {
    let fs = InMemoryFs::new();
    let garbage = vec![0xFF_u8; 4096];
    fs.insert("/install/sqpack/ffxiv/0a0000.win32.index", garbage);

    let sq = SqPack::open_with_vfs("/install", Arc::new(fs)).expect("open");
    let err = sq.read_file("exd/root.exl").unwrap_err();
    assert!(
        matches!(err, Error::InvalidMagic),
        "expected InvalidMagic from garbage bytes, got {err:?}"
    );
}

#[test]
fn vfs_in_memory_fs_concurrent_reads_from_clones_do_not_panic() {
    use std::thread;

    let fs = sqpack_rs::vfs::InMemoryFs::new();
    let payload = b"concurrent-read-check".to_vec();
    let path = std::path::PathBuf::from("/concurrent/test.bin");
    fs.insert(&path, payload.clone());

    let handles: Vec<_> = (0..4)
        .map(|_| {
            let fs_clone = fs.clone();
            let path_clone = path.clone();
            let expected = payload.clone();
            thread::spawn(move || {
                let mut r = fs_clone
                    .open_read(&path_clone)
                    .expect("open_read must succeed");
                let mut buf = Vec::new();
                std::io::Read::read_to_end(&mut r, &mut buf).expect("read_to_end must succeed");
                assert_eq!(buf, expected, "concurrent read produced wrong bytes");
            })
        })
        .collect();

    for h in handles {
        h.join().expect("thread panicked");
    }
}

#[test]
fn vfs_clones_share_storage_for_inserts_after_open() {
    let fs = InMemoryFs::new();
    let fs_for_sq = fs.clone();

    let payload = b"shared-storage".to_vec();
    let dat_buf = make_standard_dat(&payload);
    let info_off = u64::from(SQPACK_HEADER_SIZE);
    let v1_key = hash_index1_key_for_path("exd/root.exl");
    let data = u32::try_from(info_off / 8).unwrap();

    fs.insert("/install/sqpack/ffxiv/marker", b"x".to_vec());
    let sq =
        SqPack::open_with_vfs("/install", Arc::new(fs_for_sq)).expect("open with empty fixture");
    fs.insert(
        "/install/sqpack/ffxiv/0a0000.win32.index",
        make_index_file(&[(v1_key, data)]),
    );
    fs.insert("/install/sqpack/ffxiv/0a0000.win32.dat0", dat_buf);

    let bytes = sq.read_file("exd/root.exl").expect("read_file");
    assert_eq!(bytes, payload);
}
