mod common;

use sqpack_rs::scd::{ExtraData, SCD_MAGIC_SUBTYPE, SCD_MAGIC_TYPE, Scd, ScdFormat};
use sqpack_rs::{DatFile, Index, 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 looks_like_scd(bytes: &[u8]) -> bool {
    bytes.len() >= 8 && bytes[0..4] == SCD_MAGIC_TYPE && bytes[4..8] == SCD_MAGIC_SUBTYPE
}

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

    let dat_path = data_dir.join("030000.win32.dat0");
    let idx_path = data_dir.join("030000.win32.index");

    if !dat_path.exists() || !idx_path.exists() {
        eprintln!("skipping: 030000.win32.dat0 or .index not found");
        return;
    }

    let idx = Index::open(&idx_path).expect("Index::open");
    let mut dat = DatFile::open(&dat_path).expect("DatFile::open");

    let mut checked = 0usize;
    let target = 3usize;
    let mut scd_count = 0usize;

    'outer: for entry in idx
        .entries()
        .iter()
        .filter(|e| !e.is_synonym() && e.data_file_id() == 0)
        .take(2000)
    {
        if let Ok(SqPackPayload::Standard(payload)) = dat.read_file_typed(entry.byte_offset()) {
            let bytes: &[u8] = &payload;
            if !looks_like_scd(bytes) {
                continue;
            }
            scd_count += 1;
            let scd = Scd::parse(bytes)
                .unwrap_or_else(|e| panic!("Scd::parse failed for hash {:#018x}: {e}", entry.hash));

            let header = scd.header();
            assert!(
                !scd.tracks().is_empty(),
                "SCD at hash {:#018x} must carry at least one track",
                entry.hash
            );
            assert_eq!(
                scd.tracks().len(),
                usize::from(header.audio_count),
                "tracks().len() must equal audio_count"
            );

            for (i, t) in scd.tracks().iter().enumerate() {
                let desc = t.descriptor();
                if !matches!(desc.format, ScdFormat::Empty) {
                    assert!(
                        desc.sample_rate > 0,
                        "non-empty track {i} (hash {:#018x}) has sample_rate == 0 — structurally suspicious",
                        entry.hash
                    );
                    assert!(
                        desc.channel_count > 0,
                        "non-empty track {i} (hash {:#018x}) has channel_count == 0 — structurally suspicious",
                        entry.hash
                    );
                }
                let raw = t.raw_bytes();
                let bytes_range = bytes.as_ptr_range();
                let raw_range = raw.as_ptr_range();
                assert!(
                    raw_range.start >= bytes_range.start && raw_range.end <= bytes_range.end,
                    "track {i} raw_bytes window must point inside the input slice",
                );

                eprintln!(
                    "  scd {checked} track {i}: {:?} ch={} rate={}Hz payload={}B",
                    desc.format,
                    desc.channel_count,
                    desc.sample_rate,
                    raw.len(),
                );
            }
            checked += 1;
            if checked >= target {
                break 'outer;
            }
        }
    }

    eprintln!(
        "scd scan: encountered {scd_count} SCD candidate(s) in first 2000 entries of 030000.win32 \
         ({checked} fully parsed)"
    );
}

#[test]
#[ignore = "requires real FFXIV install; run with `cargo test -- --ignored`"]
fn scanned_scd_track_formats_decode_or_pass_through() {
    let Some(data_dir) = common::maybe_data_dir() else {
        eprintln!("skipping: no sqpack data available (set SQPACK_TEST_DATA_DIR)");
        return;
    };
    let dat_path = data_dir.join("030000.win32.dat0");
    let idx_path = data_dir.join("030000.win32.index");
    if !dat_path.exists() || !idx_path.exists() {
        eprintln!("skipping: 030000.win32.dat0 or .index not found");
        return;
    }

    let idx = Index::open(&idx_path).expect("Index::open");
    let mut dat = DatFile::open(&dat_path).expect("DatFile::open");

    let mut known_codec_tracks = 0usize;
    let mut empty_tracks = 0usize;
    let mut unknown_tracks = 0usize;
    let mut files_seen = 0usize;
    let target_files = 10usize;

    'outer: for entry in idx
        .entries()
        .iter()
        .filter(|e| !e.is_synonym() && e.data_file_id() == 0)
        .take(2000)
    {
        if let Ok(SqPackPayload::Standard(payload)) = dat.read_file_typed(entry.byte_offset()) {
            let bytes: &[u8] = &payload;
            if !looks_like_scd(bytes) {
                continue;
            }
            let scd = match Scd::parse(bytes) {
                Ok(scd) => scd,
                Err(e) => panic!("Scd::parse failed for hash {:#018x}: {e}", entry.hash),
            };
            for t in scd.tracks() {
                match t.format() {
                    ScdFormat::Empty => empty_tracks += 1,
                    ScdFormat::Unknown(_) => unknown_tracks += 1,
                    _ => known_codec_tracks += 1,
                }
            }
            files_seen += 1;
            if files_seen >= target_files {
                break 'outer;
            }
        }
    }

    eprintln!(
        "scd codec scan: {files_seen} files, {known_codec_tracks} known-codec tracks, \
         {empty_tracks} empty, {unknown_tracks} unknown"
    );

    if files_seen > 0 {
        assert!(
            known_codec_tracks + empty_tracks + unknown_tracks > 0,
            "no tracks across {files_seen} SCD files — parser regression?"
        );
    }
}

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

    let game_path = "sound/system/title/bgm_title_a.scd";
    let bytes = match sq.read_file(game_path) {
        Ok(b) => b,
        Err(sqpack_rs::Error::PathNotFound { .. }) => {
            eprintln!("skipping: {game_path} not found in this install");
            return;
        }
        Err(e) => panic!("SqPack::read_file({game_path:?}) failed: {e}"),
    };

    assert!(
        looks_like_scd(&bytes),
        "bytes returned for {game_path} must begin with SCD magic"
    );

    let scd = Scd::parse(&bytes).unwrap_or_else(|e| panic!("Scd::parse({game_path}) failed: {e}"));

    assert_eq!(
        scd.tracks().len(),
        usize::from(scd.header().audio_count),
        "tracks().len() must equal audio_count"
    );
    assert_eq!(scd.audio_offsets().len(), scd.tracks().len());
    assert_eq!(
        scd.sound_offsets().len(),
        usize::from(scd.header().sound_count)
    );
    assert_eq!(
        scd.track_offsets().len(),
        usize::from(scd.header().track_count)
    );

    assert!(
        !scd.tracks().is_empty(),
        "title BGM SCD must carry at least one audio track"
    );

    eprintln!(
        "scd high-level path {game_path}: {} tracks, audio_count={}",
        scd.tracks().len(),
        scd.header().audio_count,
    );
}

#[test]
#[ignore = "requires real FFXIV install; run with `cargo test -- --ignored`"]
fn ms_adpcm_tracks_decode_to_non_empty_pcm() {
    let Some(data_dir) = common::maybe_data_dir() else {
        eprintln!("skipping: no sqpack data available (set SQPACK_TEST_DATA_DIR)");
        return;
    };
    let dat_path = data_dir.join("030000.win32.dat0");
    let idx_path = data_dir.join("030000.win32.index");
    if !dat_path.exists() || !idx_path.exists() {
        eprintln!("skipping: 030000.win32.dat0 or .index not found");
        return;
    }

    let idx = Index::open(&idx_path).expect("Index::open");
    let mut dat = DatFile::open(&dat_path).expect("DatFile::open");

    let mut decoded = 0usize;
    let target = 3usize;

    'outer: for entry in idx
        .entries()
        .iter()
        .filter(|e| !e.is_synonym() && e.data_file_id() == 0)
        .take(4000)
    {
        let Ok(SqPackPayload::Standard(payload)) = dat.read_file_typed(entry.byte_offset()) else {
            continue;
        };
        let bytes: &[u8] = &payload;
        if !looks_like_scd(bytes) {
            continue;
        }
        let Ok(scd) = Scd::parse(bytes) else {
            continue;
        };
        for (i, t) in scd.tracks().iter().enumerate() {
            if !matches!(t.format(), ScdFormat::MsAdpcm) {
                continue;
            }
            assert!(
                t.channel_count() >= 1,
                "MS-ADPCM channel_count must be >= 1"
            );
            assert!(t.sample_rate() > 0, "MS-ADPCM sample_rate must be > 0");

            let pcm = t.decode_to_pcm16().unwrap_or_else(|e| {
                panic!(
                    "MS-ADPCM decode failed for hash {:#018x} track {i}: {e}",
                    entry.hash
                )
            });
            assert!(
                !pcm.is_empty(),
                "MS-ADPCM track {i} (hash {:#018x}) decoded to empty PCM",
                entry.hash,
            );
            let channels = t.channel_count() as usize;
            assert_eq!(
                pcm.len() % channels,
                0,
                "PCM length {} not a multiple of channel_count {} for hash {:#018x} track {i}",
                pcm.len(),
                channels,
                entry.hash,
            );
            let frames = pcm.len() / channels;
            assert!(frames > 0, "decoded zero frames");

            eprintln!(
                "  MS-ADPCM hash {:#018x} track {i}: {} frames, {} ch, {} Hz",
                entry.hash,
                frames,
                t.channel_count(),
                t.sample_rate(),
            );
            decoded += 1;
            if decoded >= target {
                break 'outer;
            }
        }
    }

    eprintln!("ms_adpcm_real_file_decode: {decoded} MS-ADPCM track(s) decoded");
}

#[test]
#[ignore = "requires real FFXIV install; run with `cargo test -- --ignored`"]
fn ogg_vorbis_tracks_produce_standalone_ogg_with_oggs_magic() {
    let Some(data_dir) = common::maybe_data_dir() else {
        eprintln!("skipping: no sqpack data available (set SQPACK_TEST_DATA_DIR)");
        return;
    };
    let dat_path = data_dir.join("030000.win32.dat0");
    let idx_path = data_dir.join("030000.win32.index");
    if !dat_path.exists() || !idx_path.exists() {
        eprintln!("skipping: 030000.win32.dat0 or .index not found");
        return;
    }

    let idx = Index::open(&idx_path).expect("Index::open");
    let mut dat = DatFile::open(&dat_path).expect("DatFile::open");

    let mut checked = 0usize;
    let target = 3usize;

    'outer: for entry in idx
        .entries()
        .iter()
        .filter(|e| !e.is_synonym() && e.data_file_id() == 0)
        .take(4000)
    {
        let Ok(SqPackPayload::Standard(payload)) = dat.read_file_typed(entry.byte_offset()) else {
            continue;
        };
        let bytes: &[u8] = &payload;
        if !looks_like_scd(bytes) {
            continue;
        }
        let Ok(scd) = Scd::parse(bytes) else {
            continue;
        };
        for (i, t) in scd.tracks().iter().enumerate() {
            if !matches!(t.format(), ScdFormat::OggVorbis) {
                continue;
            }
            let (hdr_version, hdr_xor, hdr_ogg_hdr_size) = match *t.extra_data() {
                ExtraData::OggVorbis { header, .. } => {
                    (header.version, header.xor_byte, header.ogg_header_size)
                }
                _ => unreachable!("format matched OggVorbis above"),
            };
            let raw_len = t.raw_bytes().len();
            let ogg = t.to_ogg_bytes().unwrap_or_else(|e| {
                panic!(
                    "to_ogg_bytes failed for hash {:#018x} track {i}: {e}",
                    entry.hash
                )
            });
            assert!(
                ogg.len() >= 4,
                "OGG output too short to carry OggS magic for hash {:#018x} track {i}",
                entry.hash,
            );
            assert_eq!(
                &ogg[..4],
                b"OggS",
                "de-obfuscated OGG bytes for hash {:#018x} track {i} \
                 (version={hdr_version}, xor_byte={hdr_xor:#04x}, \
                 ogg_header_size={hdr_ogg_hdr_size}, raw_len={raw_len}) \
                 must start with OggS magic",
                entry.hash,
            );
            eprintln!(
                "  OggVorbis hash {:#018x} track {i}: {} OGG bytes",
                entry.hash,
                ogg.len(),
            );
            checked += 1;
            if checked >= target {
                break 'outer;
            }
        }
    }

    eprintln!("ogg_vorbis_real_file_deobfuscate: {checked} OGG Vorbis track(s) verified");
}
