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

use sqpack_rs::SqPack;
use sqpack_rs::index::INDEX_ENTRY_SIZE;
use sqpack_rs::index2::INDEX2_ENTRY_SIZE;
use sqpack_rs::sqpack::EnumeratedArchive;
use sqpack_rs::vfs::InMemoryFs;

const SQPACK_HEADER_SIZE: u32 = 1024;
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) -> 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(&1u32.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> {
    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));
    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));
    for (hash, data) in entries {
        buf.extend_from_slice(&hash.to_le_bytes());
        buf.extend_from_slice(&data.to_le_bytes());
    }
    buf
}

fn build_install(files: &[(&str, Vec<u8>)]) -> SqPack {
    let fs = InMemoryFs::new();
    fs.insert("/install/sqpack/ffxiv/marker", b"x".to_vec());
    for (path, body) in files {
        fs.insert(PathBuf::from(path), body.clone());
    }
    SqPack::open_with_vfs("/install", Arc::new(fs)).expect("open")
}

#[test]
fn enumerates_present_v1_archive() {
    let sq = build_install(&[(
        "/install/sqpack/ffxiv/0a0000.win32.index",
        make_index_file(&[(0x1u64, 0x10u32)]),
    )]);
    let archives: Vec<_> = sq.enumerate_archives().collect();
    assert_eq!(archives.len(), 1);
    match archives
        .into_iter()
        .next()
        .unwrap()
        .expect("archive parses")
    {
        EnumeratedArchive::V1(triple, idx) => {
            assert_eq!(triple.category.id(), 0x0a);
            assert_eq!(triple.expansion, 0);
            assert_eq!(triple.chunk, 0);
            assert_eq!(idx.len(), 1);
        }
        other => panic!("expected V1, got {other:?}"),
    }
}

#[test]
fn enumerates_multiple_categories_in_table_order() {
    let sq = build_install(&[
        (
            "/install/sqpack/ffxiv/000000.win32.index",
            make_index_file(&[]),
        ),
        (
            "/install/sqpack/ffxiv/0a0000.win32.index",
            make_index_file(&[]),
        ),
    ]);
    let archives: Vec<_> = sq
        .enumerate_archives()
        .collect::<Result<Vec<_>, _>>()
        .expect("parses");
    assert_eq!(archives.len(), 2);
    assert_eq!(archives[0].triple().category.id(), 0x00, "common first");
    assert_eq!(archives[1].triple().category.id(), 0x0a, "exd later");
}

#[test]
fn enumerates_v1_before_v2_at_same_triple() {
    let sq = build_install(&[
        (
            "/install/sqpack/ffxiv/0a0000.win32.index",
            make_index_file(&[]),
        ),
        (
            "/install/sqpack/ffxiv/0a0000.win32.index2",
            make_index2_file(&[]),
        ),
    ]);
    let archives: Vec<_> = sq
        .enumerate_archives()
        .collect::<Result<Vec<_>, _>>()
        .expect("parses");
    assert_eq!(archives.len(), 2);
    assert!(matches!(archives[0], EnumeratedArchive::V1(_, _)));
    assert!(matches!(archives[1], EnumeratedArchive::V2(_, _)));
}

#[test]
fn stops_chunk_probe_on_gap_per_category_expansion() {
    let sq = build_install(&[
        (
            "/install/sqpack/ffxiv/000000.win32.index",
            make_index_file(&[]),
        ),
        (
            "/install/sqpack/ffxiv/0a0000.win32.index",
            make_index_file(&[]),
        ),
    ]);
    let archives: Vec<_> = sq
        .enumerate_archives()
        .collect::<Result<Vec<_>, _>>()
        .expect("parses");
    assert_eq!(archives.len(), 2, "expected both categories' chunk 0");
}

#[test]
fn empty_install_yields_zero_archives() {
    let sq = build_install(&[]);
    assert_eq!(sq.enumerate_archives().count(), 0);
}

#[test]
fn walker_emits_v1_and_v2_across_chunk_boundary() {
    let sq = build_install(&[
        (
            "/install/sqpack/ffxiv/0a0000.win32.index",
            make_index_file(&[]),
        ),
        (
            "/install/sqpack/ffxiv/0a0000.win32.index2",
            make_index2_file(&[]),
        ),
        (
            "/install/sqpack/ffxiv/0a0001.win32.index",
            make_index_file(&[]),
        ),
        (
            "/install/sqpack/ffxiv/0a0001.win32.index2",
            make_index2_file(&[]),
        ),
    ]);
    let archives: Vec<_> = sq
        .enumerate_archives()
        .collect::<Result<Vec<_>, _>>()
        .expect("all archives parse");

    assert_eq!(
        archives.len(),
        4,
        "expected V1@0, V2@0, V1@1, V2@1 — got {len} items",
        len = archives.len()
    );
    assert!(
        matches!(archives[0], EnumeratedArchive::V1(_, _)),
        "item 0 must be V1"
    );
    assert_eq!(archives[0].triple().chunk, 0, "item 0 chunk must be 0");
    assert!(
        matches!(archives[1], EnumeratedArchive::V2(_, _)),
        "item 1 must be V2"
    );
    assert_eq!(archives[1].triple().chunk, 0, "item 1 chunk must be 0");
    assert!(
        matches!(archives[2], EnumeratedArchive::V1(_, _)),
        "item 2 must be V1"
    );
    assert_eq!(archives[2].triple().chunk, 1, "item 2 chunk must be 1");
    assert!(
        matches!(archives[3], EnumeratedArchive::V2(_, _)),
        "item 3 must be V2"
    );
    assert_eq!(archives[3].triple().chunk, 1, "item 3 chunk must be 1");
}

#[test]
fn walker_cross_expansion_advance_after_chunk_gap() {
    let sq = build_install(&[
        (
            "/install/sqpack/ffxiv/0a0000.win32.index",
            make_index_file(&[]),
        ),
        (
            "/install/sqpack/ex1/0a0100.win32.index",
            make_index_file(&[]),
        ),
    ]);
    let archives: Vec<_> = sq
        .enumerate_archives()
        .collect::<Result<Vec<_>, _>>()
        .expect("parses");

    assert_eq!(
        archives.len(),
        2,
        "expected one archive per expansion, got {len}",
        len = archives.len()
    );
    assert_eq!(
        archives[0].triple().expansion,
        0,
        "first archive must be expansion 0"
    );
    assert_eq!(archives[0].triple().chunk, 0);
    assert_eq!(
        archives[1].triple().expansion,
        1,
        "second archive must be expansion 1"
    );
    assert_eq!(archives[1].triple().chunk, 0);
}

#[test]
fn surfaces_parse_errors_inline() {
    let mut garbage = Vec::new();
    garbage.extend_from_slice(&SQPACK_MAGIC);
    garbage.push(0);
    garbage.extend_from_slice(&[0u8; 3]);
    garbage.extend_from_slice(&SQPACK_HEADER_SIZE.to_le_bytes());
    garbage.extend_from_slice(&1u32.to_le_bytes());
    garbage.extend_from_slice(&2u32.to_le_bytes());

    let sq = build_install(&[
        ("/install/sqpack/ffxiv/000000.win32.index", garbage),
        (
            "/install/sqpack/ffxiv/0a0000.win32.index",
            make_index_file(&[]),
        ),
    ]);
    let archives: Vec<_> = sq.enumerate_archives().collect();
    assert_eq!(archives.len(), 2);
    assert!(archives[0].is_err(), "garbage .index should produce Err");
    assert!(
        archives[1].is_ok(),
        "subsequent valid .index should still parse"
    );
}
