#![cfg(feature = "path-list")]

use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};

use sqpack_rs::hash::{hash_index1_key, hash_index2};
use sqpack_rs::vfs::{InMemoryFs, Vfs, VfsMetadata, VfsRead};
use sqpack_rs::{PathList, SqPack};

const SQPACK_HEADER_SIZE: u32 = 1024;
const INDEX_ENTRY_SIZE: u32 = 16;
const INDEX2_ENTRY_SIZE: u32 = 8;
const BLOCK_HEADER_SIZE: u32 = 16;
const BLOCK_TYPE_UNCOMPRESSED: u32 = 32_000;
const ARCHIVE_STEM: &str = "0a0000.win32";

const SQPACK_MAGIC: [u8; 8] = [b'S', b'q', b'P', b'a', b'c', b'k', 0, 0];

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(&[0, 0, 0]);
    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 append_standard_entry(buf: &mut Vec<u8>, payload: &[u8]) -> u64 {
    let aligned = (buf.len() + 127) & !127;
    buf.resize(aligned, 0);
    let info_off = u64::try_from(buf.len()).unwrap();

    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 block_total = u16::try_from(BLOCK_HEADER_SIZE as usize + payload.len()).expect("fits u16");
    let uncompressed = u16::try_from(payload.len()).expect("fits u16");
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.extend_from_slice(&block_total.to_le_bytes());
    buf.extend_from_slice(&uncompressed.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).to_le_bytes());
    buf.extend_from_slice(payload);

    info_off
}

fn build_dat0(payloads: &[&[u8]]) -> (Vec<u8>, Vec<u64>) {
    let mut buf = make_sqpack_header(1);
    let mut offsets = Vec::with_capacity(payloads.len());
    for p in payloads {
        let off = append_standard_entry(&mut buf, p);
        offsets.push(off);
    }
    (buf, offsets)
}

fn build_index_v1(entries: &[(&str, u64)]) -> Vec<u8> {
    let header_total = SQPACK_HEADER_SIZE + 1024;
    let entry_bytes_len = entries.len() * INDEX_ENTRY_SIZE as usize;
    let entry_bytes_off = header_total;

    let mut hdr = make_sqpack_header(2);
    let idx_hdr = make_index_header(entry_bytes_off, u32::try_from(entry_bytes_len).unwrap(), 1);
    hdr.extend_from_slice(&idx_hdr);

    let mut keyed: Vec<(u64, u64)> = entries
        .iter()
        .map(|(p, off)| (hash_index1_key(p), *off))
        .collect();
    keyed.sort_by_key(|(h, _)| *h);

    for (hash, dat_off) in keyed {
        hdr.extend_from_slice(&hash.to_le_bytes());
        let packed: u32 = u32::try_from(dat_off / 8).expect("offset fits u32 / 8");
        hdr.extend_from_slice(&packed.to_le_bytes());
        hdr.extend_from_slice(&0u32.to_le_bytes());
    }
    hdr
}

fn build_index_v2(entries: &[(&str, u64)]) -> Vec<u8> {
    let header_total = SQPACK_HEADER_SIZE + 1024;
    let entry_bytes_len = entries.len() * INDEX2_ENTRY_SIZE as usize;
    let entry_bytes_off = header_total;

    let mut hdr = make_sqpack_header(2);
    let idx_hdr = make_index_header(entry_bytes_off, u32::try_from(entry_bytes_len).unwrap(), 1);
    hdr.extend_from_slice(&idx_hdr);

    let mut keyed: Vec<(u32, u64)> = entries
        .iter()
        .map(|(p, off)| (hash_index2(p), *off))
        .collect();
    keyed.sort_by_key(|(h, _)| *h);

    for (hash, dat_off) in keyed {
        hdr.extend_from_slice(&hash.to_le_bytes());
        let packed: u32 = u32::try_from(dat_off / 8).expect("offset fits u32 / 8");
        hdr.extend_from_slice(&packed.to_le_bytes());
    }
    hdr
}

fn provision_install(entries: &[(&str, &[u8])]) -> SqPack {
    let install_root = std::path::PathBuf::from("/install");
    let vfs = Arc::new(InMemoryFs::new());

    let payloads: Vec<&[u8]> = entries.iter().map(|(_, p)| *p).collect();
    let (dat_bytes, dat_offsets) = build_dat0(&payloads);

    let path_offset_pairs: Vec<(&str, u64)> = entries
        .iter()
        .zip(dat_offsets.iter())
        .map(|((p, _), off)| (*p, *off))
        .collect();

    let v1_bytes = build_index_v1(&path_offset_pairs);
    let v2_bytes = build_index_v2(&path_offset_pairs);

    let sqpack_dir = install_root.join("sqpack/ffxiv");
    let index_path = sqpack_dir.join(format!("{ARCHIVE_STEM}.index"));
    let index2_path = sqpack_dir.join(format!("{ARCHIVE_STEM}.index2"));
    let dat_path = sqpack_dir.join(format!("{ARCHIVE_STEM}.dat0"));

    vfs.insert(&index_path, v1_bytes);
    vfs.insert(&index2_path, v2_bytes);
    vfs.insert(&dat_path, dat_bytes);

    SqPack::open_with_vfs(install_root, vfs).expect("open install")
}

#[test]
fn no_list_attached_means_lookups_return_none() {
    let sq = provision_install(&[("exd/root.exl", b"hello")]);
    assert!(sq.path_list().is_none());
    assert_eq!(sq.lookup_path_v1(hash_index1_key("exd/root.exl")), None);
    assert_eq!(sq.lookup_path_v2(hash_index2("exd/root.exl")), None);
    let known: Vec<Arc<str>> = sq.known_paths().collect();
    assert!(known.is_empty());
}

#[test]
fn lookup_round_trips_for_attached_list() {
    let sq = provision_install(&[("exd/root.exl", b"hello")]);
    let list = Arc::new(PathList::with_paths(["exd/root.exl"]));
    let sq = sq.with_path_list(list);

    assert_eq!(
        sq.lookup_path_v1(hash_index1_key("exd/root.exl")),
        Some("exd/root.exl"),
    );
    assert_eq!(
        sq.lookup_path_v2(hash_index2("exd/root.exl")),
        Some("exd/root.exl"),
    );
}

#[test]
fn known_paths_yields_paths_present_in_install() {
    let sq = provision_install(&[("exd/root.exl", b"a"), ("exd/item.exh", b"b")]);
    let list = Arc::new(PathList::with_paths([
        "exd/root.exl",
        "exd/item.exh",
        "exd/never_shipped_with_this_install.exh",
    ]));
    let sq = sq.with_path_list(list);

    let known: Vec<String> = sq.known_paths().map(|s| s.as_ref().to_owned()).collect();
    assert_eq!(known, vec!["exd/root.exl", "exd/item.exh"]);
}

#[test]
fn known_paths_iteration_is_insertion_order() {
    let sq = provision_install(&[("exd/item.exh", b"a"), ("exd/root.exl", b"b")]);
    let list = Arc::new(PathList::with_paths(["exd/item.exh", "exd/root.exl"]));
    let sq = sq.with_path_list(list);
    let known: Vec<String> = sq.known_paths().map(|s| s.as_ref().to_owned()).collect();
    assert_eq!(known, vec!["exd/item.exh", "exd/root.exl"]);
}

#[test]
fn known_paths_recovered_path_feeds_back_into_read_file() {
    let payload: &[u8] = b"hello world";
    let sq = provision_install(&[("exd/root.exl", payload)]);
    let list = Arc::new(PathList::with_paths(["exd/root.exl"]));
    let sq = sq.with_path_list(list);

    let path = sq.known_paths().next().expect("at least one known path");
    let bytes = sq.read_file(path.as_ref()).expect("read recovered path");
    assert_eq!(bytes, payload);
}

#[test]
fn set_and_unset_path_list_in_place() {
    let mut sq = provision_install(&[("exd/root.exl", b"hi")]);
    assert!(sq.path_list().is_none());
    let list = Arc::new(PathList::with_paths(["exd/root.exl"]));
    sq.set_path_list(Arc::clone(&list));
    assert!(sq.path_list().is_some());
    assert_eq!(
        sq.lookup_path_v2(hash_index2("exd/root.exl")),
        Some("exd/root.exl")
    );
    sq.unset_path_list();
    assert!(sq.path_list().is_none());
    assert_eq!(sq.lookup_path_v2(hash_index2("exd/root.exl")), None);
}

#[test]
fn shared_arc_across_installs() {
    let list = Arc::new(PathList::with_paths(["exd/root.exl", "exd/item.exh"]));
    let a = provision_install(&[("exd/root.exl", b"a")]).with_path_list(Arc::clone(&list));
    let b = provision_install(&[("exd/item.exh", b"b")]).with_path_list(Arc::clone(&list));
    let known_a: Vec<String> = a.known_paths().map(|s| s.as_ref().to_owned()).collect();
    let known_b: Vec<String> = b.known_paths().map(|s| s.as_ref().to_owned()).collect();
    assert_eq!(known_a, vec!["exd/root.exl"]);
    assert_eq!(known_b, vec!["exd/item.exh"]);
}

struct CountingVfs {
    inner: Arc<dyn Vfs>,
    total_exists_calls: AtomicUsize,
}

impl CountingVfs {
    fn new(inner: Arc<dyn Vfs>) -> Self {
        Self {
            inner,
            total_exists_calls: AtomicUsize::new(0),
        }
    }

    fn total_exists(&self) -> usize {
        self.total_exists_calls.load(Ordering::SeqCst)
    }
}

impl Vfs for CountingVfs {
    fn open_read(&self, path: &Path) -> io::Result<Box<dyn VfsRead>> {
        self.inner.open_read(path)
    }
    fn exists(&self, path: &Path) -> bool {
        self.total_exists_calls.fetch_add(1, Ordering::SeqCst);
        self.inner.exists(path)
    }
    fn metadata(&self, path: &Path) -> io::Result<VfsMetadata> {
        self.inner.metadata(path)
    }
    fn read_dir(&self, path: &Path) -> io::Result<Vec<PathBuf>> {
        self.inner.read_dir(path)
    }
}

#[test]
fn known_paths_iterator_does_not_probe_unrelated_expansions() {
    let install_root = std::path::PathBuf::from("/install");
    let inner_vfs = Arc::new(InMemoryFs::new());

    let (dat_bytes, dat_offsets) = build_dat0(&[b"hello"]);
    let pairs: Vec<(&str, u64)> = vec![("exd/root.exl", dat_offsets[0])];
    let v1_bytes = build_index_v1(&pairs);
    let v2_bytes = build_index_v2(&pairs);

    let sqpack_dir = install_root.join("sqpack/ffxiv");
    inner_vfs.insert(sqpack_dir.join(format!("{ARCHIVE_STEM}.index")), v1_bytes);
    inner_vfs.insert(sqpack_dir.join(format!("{ARCHIVE_STEM}.index2")), v2_bytes);
    inner_vfs.insert(sqpack_dir.join(format!("{ARCHIVE_STEM}.dat0")), dat_bytes);

    let counter = Arc::new(CountingVfs::new(inner_vfs.clone() as Arc<dyn Vfs>));
    let sq = SqPack::open_with_vfs(install_root, counter.clone() as Arc<dyn Vfs>).expect("open");
    let list = Arc::new(PathList::with_paths(["exd/root.exl"]));
    let sq = sq.with_path_list(list);

    let pre = counter.total_exists();
    let known: Vec<Arc<str>> = sq.known_paths().collect();
    let post = counter.total_exists();
    let during = post - pre;

    assert_eq!(known.len(), 1, "the embedded path must resolve");
    assert!(
        during < 8,
        "iterator must not probe all 16 expansions; \
         observed {during} exists() calls for one path",
    );
}

#[test]
fn seed_list_is_useful_on_a_seeded_install() {
    let sq = provision_install(&[("exd/root.exl", b"x")]);
    let list = Arc::new(PathList::with_seed_list());
    let sq = sq.with_path_list(list);
    let known: Vec<String> = sq.known_paths().map(|s| s.as_ref().to_owned()).collect();
    assert_eq!(known, vec!["exd/root.exl"]);
}

#[test]
fn known_paths_no_list_attached_yields_empty() {
    let sq = provision_install(&[("exd/root.exl", b"x")]);
    let known: Vec<Arc<str>> = sq.known_paths().collect();
    assert!(
        known.is_empty(),
        "iterator without a list must yield nothing"
    );
}

#[test]
fn known_paths_empty_path_list_yields_empty() {
    let sq = provision_install(&[("exd/root.exl", b"x")]);
    let list = Arc::new(PathList::new());
    let sq = sq.with_path_list(list);
    let known: Vec<Arc<str>> = sq.known_paths().collect();
    assert!(
        known.is_empty(),
        "empty PathList must produce empty known_paths iteration"
    );
}

#[test]
fn known_paths_all_absent_yields_empty() {
    let sq = provision_install(&[("exd/root.exl", b"x")]);
    let list = Arc::new(PathList::with_paths([
        "exd/never_present_a.exh",
        "exd/never_present_b.exh",
    ]));
    let sq = sq.with_path_list(list);
    let known: Vec<Arc<str>> = sq.known_paths().collect();
    assert!(
        known.is_empty(),
        "paths absent from install must not appear in known_paths; got {known:?}",
    );
}

#[test]
fn known_paths_size_hint_reflects_remaining_entries() {
    let sq = provision_install(&[
        ("exd/root.exl", b"a"),
        ("exd/item.exh", b"b"),
        ("exd/action.exh", b"c"),
    ]);
    let list = Arc::new(PathList::with_paths([
        "exd/root.exl",
        "exd/item.exh",
        "exd/action.exh",
    ]));
    let sq = sq.with_path_list(list);

    let mut iter = sq.known_paths();

    let (lo, hi) = iter.size_hint();
    assert_eq!(lo, 0, "lower bound must always be 0");
    assert_eq!(
        hi,
        Some(3),
        "upper bound must equal remaining list entries at start"
    );

    let _ = iter.next().expect("first item must resolve");
    let (lo, hi) = iter.size_hint();
    assert_eq!(lo, 0);
    assert_eq!(
        hi,
        Some(2),
        "upper bound must decrease by 1 after each next()"
    );

    let _ = iter.next().expect("second item must resolve");
    let (lo, hi) = iter.size_hint();
    assert_eq!(lo, 0);
    assert_eq!(hi, Some(1));

    let _ = iter.next().expect("third item must resolve");
    let _ = iter.next();
    let (lo, hi) = iter.size_hint();
    assert_eq!(lo, 0);
    assert_eq!(hi, Some(0), "upper bound must be 0 after exhaustion");
}

#[test]
fn known_paths_yielded_arc_is_same_allocation_as_path_list() {
    let sq = provision_install(&[("exd/root.exl", b"x")]);
    let list = Arc::new(PathList::with_paths(["exd/root.exl"]));
    let internal_ptr = Arc::as_ptr(&list.arc_paths().next().expect("one entry"));

    let sq = sq.with_path_list(Arc::clone(&list));
    let yielded = sq.known_paths().next().expect("must resolve");
    assert_eq!(
        Arc::as_ptr(&yielded),
        internal_ptr,
        "yielded Arc must be the same allocation as the PathList's internal storage",
    );
}

#[test]
fn known_paths_two_calls_return_independent_iterators() {
    let sq = provision_install(&[("exd/root.exl", b"a"), ("exd/item.exh", b"b")]);
    let list = Arc::new(PathList::with_paths(["exd/root.exl", "exd/item.exh"]));
    let sq = sq.with_path_list(list);

    let first: Vec<String> = sq.known_paths().map(|s| s.as_ref().to_owned()).collect();
    let second: Vec<String> = sq.known_paths().map(|s| s.as_ref().to_owned()).collect();

    assert_eq!(
        first, second,
        "two sequential known_paths() calls must produce identical results",
    );
    assert_eq!(first.len(), 2);
}

#[test]
fn set_path_list_between_iterations_second_sees_new_list() {
    let sq = provision_install(&[("exd/root.exl", b"a"), ("exd/item.exh", b"b")]);
    let list_a = Arc::new(PathList::with_paths(["exd/root.exl"]));
    let list_b = Arc::new(PathList::with_paths(["exd/item.exh"]));

    let mut sq = sq.with_path_list(list_a);

    let first: Vec<String> = sq.known_paths().map(|s| s.as_ref().to_owned()).collect();
    assert_eq!(
        first,
        vec!["exd/root.exl"],
        "first iteration must use list_a"
    );

    sq.set_path_list(list_b);

    let second: Vec<String> = sq.known_paths().map(|s| s.as_ref().to_owned()).collect();
    assert_eq!(
        second,
        vec!["exd/item.exh"],
        "second iteration must use list_b after set_path_list"
    );
}

#[test]
fn known_paths_cache_hit_for_same_triple() {
    let install_root = std::path::PathBuf::from("/install_cache_test");
    let inner_vfs = Arc::new(sqpack_rs::vfs::InMemoryFs::new());

    let paths_in_install: &[(&str, &[u8])] = &[
        ("exd/root.exl", b"a"),
        ("exd/item.exh", b"b"),
        ("exd/action.exh", b"c"),
        ("exd/status.exh", b"d"),
        ("exd/classjob.exh", b"e"),
    ];

    let payloads: Vec<&[u8]> = paths_in_install.iter().map(|(_, p)| *p).collect();
    let (dat_bytes, dat_offsets) = build_dat0(&payloads);

    let path_offset_pairs: Vec<(&str, u64)> = paths_in_install
        .iter()
        .zip(dat_offsets.iter())
        .map(|((p, _), off)| (*p, *off))
        .collect();

    let v1_bytes = build_index_v1(&path_offset_pairs);
    let v2_bytes = build_index_v2(&path_offset_pairs);

    let sqpack_dir = install_root.join("sqpack/ffxiv");
    inner_vfs.insert(sqpack_dir.join(format!("{ARCHIVE_STEM}.index")), v1_bytes);
    inner_vfs.insert(sqpack_dir.join(format!("{ARCHIVE_STEM}.index2")), v2_bytes);
    inner_vfs.insert(sqpack_dir.join(format!("{ARCHIVE_STEM}.dat0")), dat_bytes);

    let counter = Arc::new(CountingVfs::new(
        inner_vfs.clone() as Arc<dyn sqpack_rs::vfs::Vfs>
    ));
    let sq = SqPack::open_with_vfs(
        install_root,
        counter.clone() as Arc<dyn sqpack_rs::vfs::Vfs>,
    )
    .expect("open");

    let list = Arc::new(PathList::with_paths([
        "exd/root.exl",
        "exd/item.exh",
        "exd/action.exh",
        "exd/status.exh",
        "exd/classjob.exh",
    ]));
    let sq = sq.with_path_list(list);

    let pre = counter.total_exists();
    let known: Vec<Arc<str>> = sq.known_paths().collect();
    let post = counter.total_exists();
    let during = post - pre;

    assert_eq!(known.len(), 5, "all 5 paths must resolve");
    assert!(
        during <= 8,
        "cache must prevent re-probing the same triple; \
         expected ≤ 8 exists() calls for 5 paths in the same archive, got {during}",
    );
}

#[test]
fn path_list_accessor_returns_attached_arc() {
    let sq = provision_install(&[("exd/root.exl", b"x")]);
    let list = Arc::new(PathList::with_paths(["exd/root.exl"]));
    let expected_ptr = Arc::as_ptr(&list);
    let sq = sq.with_path_list(Arc::clone(&list));

    let got = sq.path_list().expect("list must be attached");
    assert_eq!(
        Arc::as_ptr(got),
        expected_ptr,
        "path_list() must return the same Arc that was attached",
    );
}

#[test]
fn unset_path_list_leaves_state_clean() {
    let mut sq = provision_install(&[("exd/root.exl", b"x")]);
    let list = Arc::new(PathList::with_paths(["exd/root.exl"]));
    sq.set_path_list(list);
    assert!(sq.path_list().is_some());
    assert!(sq.lookup_path_v1(hash_index1_key("exd/root.exl")).is_some());

    sq.unset_path_list();

    assert!(
        sq.path_list().is_none(),
        "path_list() must be None after unset"
    );
    assert_eq!(
        sq.lookup_path_v1(hash_index1_key("exd/root.exl")),
        None,
        "lookup_path_v1 must return None after unset",
    );
    assert_eq!(
        sq.lookup_path_v2(hash_index2("exd/root.exl")),
        None,
        "lookup_path_v2 must return None after unset",
    );
    let known: Vec<Arc<str>> = sq.known_paths().collect();
    assert!(known.is_empty(), "known_paths must be empty after unset");
}
