mod common;

use sqpack_rs::dat::ModelPayload;
use sqpack_rs::mdl::Mdl;
use sqpack_rs::{Error, FileKind, SqPack, SqPackPayload};

fn maybe_open_sqpack() -> Option<SqPack> {
    let data_dir = common::maybe_data_dir()?;
    let install_root = common::install_root_from_data_dir(&data_dir)?;
    match SqPack::open(&install_root) {
        Ok(sq) => Some(sq),
        Err(e) => {
            eprintln!("skipping: SqPack::open failed: {e}");
            None
        }
    }
}

fn maybe_model_payload(sq: &SqPack, game_path: &str) -> Option<Vec<u8>> {
    maybe_model(sq, game_path).map(|m| m.bytes().to_vec())
}

fn maybe_model(sq: &SqPack, game_path: &str) -> Option<ModelPayload> {
    match sq.read_file_typed(game_path) {
        Ok(SqPackPayload::Model(m)) => Some(m),
        Ok(other) => {
            eprintln!(
                "skipping {game_path}: expected Model payload, got {:?}",
                other.kind()
            );
            None
        }
        Err(Error::PathNotFound { .. }) => {
            eprintln!("skipping {game_path}: path not found on this install");
            None
        }
        Err(e) => panic!("unexpected error reading {game_path}: {e}"),
    }
}

#[test]
#[ignore = "requires real FFXIV install; run with `cargo test -- --ignored`"]
fn equipment_model_routes_to_model_payload() {
    let Some(sq) = maybe_open_sqpack() else {
        eprintln!("skipping: no sqpack data available (set SQPACK_TEST_DATA_DIR)");
        return;
    };

    let path = "chara/equipment/e0000/model/c0101e0000_top.mdl";
    let Some(bytes) = maybe_model_payload(&sq, path) else {
        return;
    };

    assert!(
        !bytes.is_empty(),
        "Model payload for {path} should not be empty"
    );

    eprintln!(
        "loaded {path}: {len} bytes of decompressed Model payload",
        len = bytes.len()
    );
}

#[test]
#[ignore = "requires real FFXIV install; run with `cargo test -- --ignored`"]
fn peek_kind_is_model_for_multiple_equipment_paths() {
    let Some(sq) = maybe_open_sqpack() else {
        eprintln!("skipping: no sqpack data available (set SQPACK_TEST_DATA_DIR)");
        return;
    };

    let candidates = [
        "chara/equipment/e0000/model/c0101e0000_top.mdl",
        "chara/equipment/e0000/model/c0101e0000_dwn.mdl",
        "chara/equipment/e0000/model/c0101e0000_glv.mdl",
        "chara/equipment/e0000/model/c0101e0000_sho.mdl",
        "chara/equipment/e0000/model/c0201e0000_top.mdl",
    ];

    let mut resolved = 0usize;
    for path in &candidates {
        match sq.peek_kind(path) {
            Ok(FileKind::Model) => {
                eprintln!("{path}: FileKind::Model (correct)");
                resolved += 1;
            }
            Ok(other) => panic!("{path}: expected FileKind::Model, got {other:?}"),
            Err(Error::PathNotFound { .. }) => {
                eprintln!("{path}: not found on this install (skipped)");
            }
            Err(e) => panic!("{path}: unexpected error: {e}"),
        }
    }

    assert!(
        resolved > 0,
        "expected at least one candidate to resolve as Model on a normal install; \
         either the install is missing core equipment models or the candidate list has drifted"
    );
    eprintln!(
        "{resolved}/{} candidate paths resolved as FileKind::Model",
        candidates.len()
    );
}

#[test]
#[ignore = "requires real FFXIV install; run with `cargo test -- --ignored`"]
#[allow(clippy::too_many_lines)]
fn lumina_bytes_round_trips_through_mdl_parse() {
    let Some(sq) = maybe_open_sqpack() else {
        eprintln!("skipping: no sqpack data available (set SQPACK_TEST_DATA_DIR)");
        return;
    };

    let candidates = [
        "chara/equipment/e0000/model/c0101e0000_top.mdl",
        "chara/equipment/e0000/model/c0101e0000_dwn.mdl",
        "chara/equipment/e0000/model/c0101e0000_glv.mdl",
        "chara/equipment/e0000/model/c0101e0000_sho.mdl",
        "chara/equipment/e0000/model/c0101e0000_met.mdl",
        "chara/equipment/e0000/model/c0201e0000_top.mdl",
        "chara/equipment/e0000/model/c0201e0000_dwn.mdl",
        "chara/equipment/e0000/model/c0201e0000_glv.mdl",
        "chara/equipment/e0000/model/c0201e0000_sho.mdl",
        "chara/weapon/w0101/obj/body/b0001/model/w0101b0001.mdl",
    ];

    let mut tried = 0usize;
    let mut succeeded = 0usize;
    for path in &candidates {
        let Some(payload) = maybe_model(&sq, path) else {
            continue;
        };
        tried += 1;

        let lumina = payload.lumina_bytes();
        assert_eq!(
            &lumina[lumina.len() - payload.bytes().len()..],
            payload.bytes(),
            "{path}: lumina_bytes tail must equal ModelPayload::bytes verbatim"
        );

        let mdl = Mdl::parse(&lumina).unwrap_or_else(|e| {
            panic!("{path}: Mdl::parse failed unexpectedly — this candidate must round-trip: {e}");
        });

        let hdr = mdl.file_header();
        let meta = &payload.metadata;
        assert_eq!(hdr.version, meta.version, "{path}: version");
        assert_eq!(hdr.stack_size, meta.stack_size, "{path}: stack_size");
        assert_eq!(hdr.runtime_size, meta.runtime_size, "{path}: runtime_size");
        assert_eq!(
            hdr.vertex_declaration_count, meta.vertex_declaration_count,
            "{path}: vertex_declaration_count"
        );
        assert_eq!(
            hdr.material_count, meta.material_count,
            "{path}: material_count"
        );
        assert_eq!(hdr.lod_count, meta.lod_count, "{path}: lod_count");
        assert_eq!(
            hdr.enable_index_buffer_streaming, meta.enable_index_buffer_streaming,
            "{path}: enable_index_buffer_streaming"
        );
        assert_eq!(
            hdr.enable_edge_geometry, meta.enable_edge_geometry,
            "{path}: enable_edge_geometry"
        );
        assert_eq!(
            hdr.vertex_buffer_size, meta.vertex_buffer_size,
            "{path}: vertex_buffer_size"
        );
        assert_eq!(
            hdr.index_buffer_size, meta.index_buffer_size,
            "{path}: index_buffer_size"
        );
        assert_eq!(
            hdr.vertex_offset, meta.vertex_offset,
            "{path}: vertex_offset"
        );
        assert_eq!(hdr.index_offset, meta.index_offset, "{path}: index_offset");

        assert!(
            (1..=3).contains(&hdr.lod_count),
            "{path}: lod_count {} out of plausible range",
            hdr.lod_count
        );
        assert!(
            hdr.material_count > 0,
            "{path}: real equipment models declare at least one material"
        );

        let bone_count = usize::from(mdl.model_header().bone_count);
        for (lod_idx, table) in mdl.bone_tables().iter().enumerate() {
            for (entry_idx, &bone) in table.indices().iter().enumerate() {
                assert!(
                    usize::from(bone) < bone_count,
                    "{path}: bone-table[{lod_idx}][{entry_idx}] = {bone} out of range \
                     (bone_count {bone_count}); version=0x{:08x} tag={:?}",
                    hdr.version,
                    table.version_tag(),
                );
            }
        }
        let expected_tag = if hdr.version >= sqpack_rs::mdl::BoneTable::V2_VERSION_MIN {
            sqpack_rs::mdl::BoneTableVersion::V2
        } else {
            sqpack_rs::mdl::BoneTableVersion::Legacy
        };
        for table in mdl.bone_tables() {
            assert_eq!(
                table.version_tag(),
                expected_tag,
                "{path}: bone-table version_tag drift (header version 0x{:08x})",
                hdr.version,
            );
        }

        succeeded += 1;
        eprintln!(
            "  {path}: {} bytes (prefix 68 + body {})",
            lumina.len(),
            payload.bytes().len()
        );
    }
    eprintln!("lumina_bytes round-trip: {succeeded}/{tried} candidate paths parsed cleanly");
    assert_eq!(
        succeeded, tried,
        "expected every resolved candidate to round-trip cleanly"
    );
    assert!(
        tried >= 6,
        "expected at least 6 candidates to resolve on a normal install (got {tried}); \
         either the install is missing core equipment models or the candidate list has drifted"
    );
}

#[test]
#[ignore = "requires real FFXIV install; run with `cargo test -- --ignored`"]
fn peek_kind_reports_model_for_equipment_path() {
    let Some(sq) = maybe_open_sqpack() else {
        eprintln!("skipping: no sqpack data available (set SQPACK_TEST_DATA_DIR)");
        return;
    };

    let candidates = [
        "chara/equipment/e0000/model/c0101e0000_top.mdl",
        "chara/equipment/e0000/model/c0101e0000_dwn.mdl",
        "chara/equipment/e0000/model/c0101e0000_glv.mdl",
    ];
    for path in &candidates {
        match sq.peek_kind(path) {
            Ok(FileKind::Model) => {
                eprintln!("{path}: FileKind::Model (as expected)");
                return;
            }
            Ok(other) => panic!("{path}: expected Model, got {other:?}"),
            Err(Error::PathNotFound { .. }) => {
                eprintln!("{path}: not present on this install");
            }
            Err(e) => panic!("{path}: unexpected error: {e}"),
        }
    }
    eprintln!(
        "no candidate model paths resolved on this install; \
         test passes degenerately (every path was PathNotFound)"
    );
}
