//! Disc-wide guards for the 2D surface formats, locking in what was measured on
//! 2026-08-11. Skipped (as no-ops) when the extracted disc is absent.
//!
//! These assert *disc-wide invariants* rather than one hand-picked file, because
//! each of the findings they guard was originally missed by reasoning from a
//! sample: T8aD's "~15 % unsupported variants" were a wrong model, and LSTA's
//! "a few entries disagree with the count" was a miscount.

use std::path::{Path, PathBuf};

use sylpheed_formats::{lsta, pak::PakArchive, ratc, t8ad};

fn disc_root() -> Option<PathBuf> {
    if let Ok(p) = std::env::var("SYLPHEED_DISC") {
        let p = PathBuf::from(p);
        if p.join("dat").is_dir() {
            return Some(p);
        }
    }
    let default = Path::new(
        "/home/fabi/RE - Project Sylpheed/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja)",
    );
    if default.join("dat").is_dir() {
        return Some(default.to_path_buf());
    }
    None
}

macro_rules! skip_without_disc {
    ($root:ident) => {
        let Some($root) = disc_root() else {
            eprintln!("SKIP: extracted disc not found (set SYLPHEED_DISC to enable)");
            return;
        };
    };
}

/// Every entry of every pak, plus every RATC child, as raw bytes.
fn for_each_blob(root: &Path, mut f: impl FnMut(&str, &str, &[u8])) {
    let mut paks: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
        .expect("dat/")
        .flatten()
        .map(|e| e.path())
        .filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak"))
        .collect();
    paks.sort();
    for p in &paks {
        let pak_name = p.file_name().unwrap().to_string_lossy().to_string();
        let Ok(arc) = PakArchive::open(p) else { continue };
        for e in arc.entries() {
            let Ok(bytes) = arc.read(e) else { continue };
            f(&pak_name, &format!("{:08x}", e.name_hash), &bytes);
            if ratc::is_ratc(&bytes) {
                if let Some(kids) = ratc::parse(&bytes) {
                    for k in &kids {
                        if k.offset + k.size <= bytes.len() {
                            f(&pak_name, &k.name, &bytes[k.offset..k.offset + k.size]);
                        }
                    }
                }
            }
        }
    }
}

/// A T8aD surface is a list of sub-rectangles, and on this disc **every** one
/// decodes. Regressing the rectangle model would show up here as a decode gap,
/// which is exactly how the old 256-grid reading looked (96 %, not 100 %).
#[test]
fn every_t8ad_on_the_disc_decodes() {
    skip_without_disc!(root);
    let (mut total, mut ok) = (0usize, 0usize);
    let mut first_failure = None;
    for_each_blob(&root, |pak, name, b| {
        if !t8ad::is_t8ad(b) || b.len() < 0x40 {
            return;
        }
        total += 1;
        if t8ad::parse(b).is_some() {
            ok += 1;
        } else if first_failure.is_none() {
            first_failure = Some(format!("{pak}:{name}"));
        }
    });
    assert!(total > 19_000, "expected the disc's ~19 216 surfaces, saw {total}");
    assert_eq!(ok, total, "first failure: {first_failure:?}");
}

/// An LSTA's header count is exact and counts **both** kinds of element: T8aD
/// sprites and `PRMD` primitives. (It was long read as unreliable because the
/// comparison ignored primitives.)
#[test]
fn lsta_count_equals_sprites_plus_primitives() {
    skip_without_disc!(root);
    let count_magic = |b: &[u8], magic: &[u8; 4]| {
        let (mut n, mut i) = (0usize, 4usize);
        while i + 4 <= b.len() {
            if &b[i..i + 4] == magic {
                n += 1;
                i += 4;
            } else {
                i += 1;
            }
        }
        n
    };
    let (mut lists, mut exact) = (0usize, 0usize);
    let mut bad = Vec::new();
    for_each_blob(&root, |pak, name, b| {
        if !lsta::is_lsta(b) || b.len() < 8 {
            return;
        }
        lists += 1;
        let declared = u32::from_be_bytes([b[4], b[5], b[6], b[7]]) as usize;
        let sprites = count_magic(b, b"T8aD");
        let prims = count_magic(b, b"PRMD");
        if declared == sprites + prims {
            exact += 1;
        } else if bad.len() < 4 {
            bad.push(format!("{pak}:{name} declared {declared} != {sprites}+{prims}"));
        }
    });
    assert!(lists >= 60, "expected the disc's 64 LSTA lists, saw {lists}");
    assert_eq!(exact, lists, "mismatches: {bad:?}");
}

/// Nested RATC records are **leaves**: they carry no child list of their own.
/// "One level deep" describes the data, not a parser limit.
#[test]
fn ratc_nesting_is_exactly_one_level() {
    skip_without_disc!(root);
    let mut grandchildren = 0usize;
    let mut bundles = 0usize;
    for_each_blob(&root, |_, _, b| {
        if !ratc::is_ratc(b) {
            return;
        }
        let Some(kids) = ratc::parse(b) else { return };
        bundles += 1;
        for k in &kids {
            if k.offset + k.size > b.len() {
                continue;
            }
            let sub = &b[k.offset..k.offset + k.size];
            if ratc::is_ratc(sub) {
                grandchildren += ratc::parse(sub).map(|g| g.len()).unwrap_or(0);
            }
        }
    });
    assert!(bundles > 2_000, "expected thousands of RATC bundles, saw {bundles}");
    assert_eq!(grandchildren, 0, "a nested RATC record listed children");
}
