mod common;

use sqpack_rs::{Field, Language, SqPack};

#[test]
#[ignore = "requires real FFXIV install; run with `cargo test -- --ignored`"]
fn excel_list_lists_sheets_on_real_install() {
    let Some(data_dir) = common::maybe_data_dir() else {
        eprintln!("skipping: no sqpack data available (set SQPACK_TEST_DATA_DIR)");
        return;
    };
    let Some(install_root) = common::install_root_from_data_dir(&data_dir) else {
        eprintln!(
            "skipping: could not derive install root from {}",
            data_dir.display()
        );
        return;
    };

    let sq = SqPack::open(&install_root).expect("SqPack::open should succeed on a real install");
    let list = sq
        .excel_list()
        .expect("excel_list should parse on a real install");

    assert_eq!(list.version(), 2, "EXL header version must currently be 2");
    assert!(
        list.len() > 100,
        "retail installs ship many hundreds of sheets; got {}",
        list.len()
    );
    let item = list.get("Item").expect("Item sheet must be listed");
    assert!(
        item.is_some(),
        "Item sheet is expected to carry a stable id, got the -1 sentinel",
    );
    eprintln!(
        "excel list: {} sheets, version {}, Item id = {:?}",
        list.len(),
        list.version(),
        item,
    );
}

#[test]
#[ignore = "requires real FFXIV install; run with `cargo test -- --ignored`"]
fn excel_header_decodes_item_sheet_on_real_install() {
    let Some(data_dir) = common::maybe_data_dir() else {
        eprintln!("skipping: no sqpack data available (set SQPACK_TEST_DATA_DIR)");
        return;
    };
    let Some(install_root) = common::install_root_from_data_dir(&data_dir) else {
        eprintln!(
            "skipping: could not derive install root from {}",
            data_dir.display()
        );
        return;
    };

    let sq = SqPack::open(&install_root).expect("SqPack::open should succeed on a real install");
    let header = sq
        .excel_header("Item")
        .expect("excel_header(\"Item\") should parse on a real install");

    assert_eq!(header.version(), 3, "EXH wire-format version must be 3");
    assert!(
        header.columns().len() > 5,
        "Item sheet should declare many columns; got {}",
        header.columns().len(),
    );
    assert!(
        !header.pages().is_empty(),
        "Item sheet should declare at least one page",
    );
    assert!(
        header.languages().contains(&Language::English),
        "Item sheet should ship an English variant; got {:?}",
        header.languages(),
    );
    eprintln!(
        "Item EXH: {:?}, row_size={}, total_rows={}, columns={}, pages={}, langs={:?}",
        header.kind(),
        header.row_size(),
        header.total_row_count(),
        header.columns().len(),
        header.pages().len(),
        header.languages(),
    );
}

#[test]
#[ignore = "requires real FFXIV install; run with `cargo test -- --ignored`"]
fn excel_data_decodes_first_item_page_on_real_install() {
    let Some(data_dir) = common::maybe_data_dir() else {
        eprintln!("skipping: no sqpack data available (set SQPACK_TEST_DATA_DIR)");
        return;
    };
    let Some(install_root) = common::install_root_from_data_dir(&data_dir) else {
        eprintln!(
            "skipping: could not derive install root from {}",
            data_dir.display()
        );
        return;
    };

    let sq = SqPack::open(&install_root).expect("SqPack::open should succeed on a real install");
    let header = sq
        .excel_header("Item")
        .expect("excel_header(\"Item\") should parse on a real install");

    let first_page = header
        .pages()
        .first()
        .expect("Item should have a first page");
    let data = sq
        .excel_data("Item", &header, first_page.start_id, Language::English)
        .expect("excel_data should parse the first English Item page");

    assert!(
        data.row_count() > 0,
        "Item first page should carry at least one row; got {}",
        data.row_count(),
    );
    assert_eq!(data.language(), Language::English);

    let first_row = data.rows().next().expect("first page has rows");
    let mut found_name: Option<String> = None;
    for col_idx in 0..first_row.column_count() {
        if let Ok(Field::String(s)) = first_row.field(col_idx) {
            if !s.is_empty() {
                found_name = Some(s.to_owned());
                break;
            }
        }
    }
    eprintln!(
        "Item EXD page {} (en): {} rows, first row id={} first non-empty string={:?}",
        first_page.start_id,
        data.row_count(),
        first_row.row_id(),
        found_name,
    );

    let gil = data
        .row(1)
        .expect("Item ID 1 (Gil) must exist on a real install");
    let mut gil_string: Option<String> = None;
    for col_idx in 0..gil.column_count() {
        if let Ok(Field::String(s)) = gil.field(col_idx) {
            if !s.is_empty() {
                gil_string = Some(s.to_owned());
                break;
            }
        }
    }
    assert!(
        gil_string.is_some(),
        "Item ID 1 (Gil) must decode at least one non-empty Field::String column; \
         this pins the decode-but-wrong-values failure mode",
    );
    eprintln!("Item ID 1 (en): first non-empty string={gil_string:?}");
}

#[test]
#[ignore = "requires real FFXIV install; run with `cargo test -- --ignored`"]
fn iterates_item_sheet_via_sheet_handle_on_real_install() {
    let Some(data_dir) = common::maybe_data_dir() else {
        eprintln!("skipping: no sqpack data available (set SQPACK_TEST_DATA_DIR)");
        return;
    };
    let Some(install_root) = common::install_root_from_data_dir(&data_dir) else {
        eprintln!(
            "skipping: could not derive install root from {}",
            data_dir.display()
        );
        return;
    };

    let sq = SqPack::open(&install_root).expect("SqPack::open should succeed on a real install");
    let sheet = sq
        .sheet("Item")
        .expect("sheet(\"Item\") should open on a real install");
    assert_eq!(sheet.name(), "Item");

    let first_ten: Vec<_> = sheet
        .rows(Language::English)
        .take(10)
        .collect::<Result<Vec<_>, _>>()
        .expect("first 10 rows should decode");
    assert_eq!(
        first_ten.len(),
        10,
        "sheet.rows() should yield at least 10 rows for Item"
    );
    let ids: Vec<u32> = first_ten
        .iter()
        .map(sqpack_rs::excel::OwnedRow::row_id)
        .collect();
    let mut sorted_ids = ids.clone();
    sorted_ids.sort_unstable();
    assert_eq!(
        ids, sorted_ids,
        "row IDs must be yielded in ascending order"
    );

    let gil = sheet
        .row(1, Language::English)
        .expect("row 1 lookup should succeed")
        .expect("Item row 1 (Gil) must be present");
    assert_eq!(gil.row_id(), 1);
    eprintln!("Item sheet handle (en): first 10 row_ids={ids:?}, row(1) hit",);
}

#[test]
#[ignore = "requires real FFXIV install; run with `cargo test -- --ignored`"]
fn enumerates_every_sheet_on_real_install() {
    let Some(data_dir) = common::maybe_data_dir() else {
        eprintln!("skipping: no sqpack data available (set SQPACK_TEST_DATA_DIR)");
        return;
    };
    let Some(install_root) = common::install_root_from_data_dir(&data_dir) else {
        eprintln!(
            "skipping: could not derive install root from {}",
            data_dir.display()
        );
        return;
    };

    let sq = SqPack::open(&install_root).expect("SqPack::open should succeed on a real install");

    let names: Vec<_> = sq
        .sheet_names()
        .expect("sheet_names should read root.exl")
        .collect();
    assert!(
        names.len() > 100,
        "retail installs ship many hundreds of sheets; sheet_names gave {}",
        names.len(),
    );

    let mut visited = 0usize;
    let mut ok_count = 0usize;
    let mut err_count = 0usize;
    for item in sq.sheets().expect("sheets should read root.exl") {
        visited += 1;
        match item {
            Ok(_) => ok_count += 1,
            Err(_) => err_count += 1,
        }
    }
    assert_eq!(
        visited,
        names.len(),
        "sheets and sheet_names must agree on count"
    );
    assert!(
        ok_count > 100,
        "expected >100 sheets to open cleanly on a real install; got {ok_count} ok / {err_count} err",
    );
    eprintln!("sheets enumeration: {visited} total ({ok_count} ok, {err_count} err)");
}

#[test]
#[ignore = "requires real FFXIV install; run with `cargo test -- --ignored`"]
fn item_sheet_row_ids_match_across_languages_on_real_install() {
    let Some(data_dir) = common::maybe_data_dir() else {
        eprintln!("skipping: no sqpack data available (set SQPACK_TEST_DATA_DIR)");
        return;
    };
    let Some(install_root) = common::install_root_from_data_dir(&data_dir) else {
        eprintln!(
            "skipping: could not derive install root from {}",
            data_dir.display()
        );
        return;
    };

    let sq = SqPack::open(&install_root).expect("SqPack::open should succeed on a real install");
    let header = sq
        .excel_header("Item")
        .expect("Item EXH should parse on a real install");
    let langs = header.languages();
    if !langs.contains(&Language::English) || !langs.contains(&Language::Japanese) {
        eprintln!(
            "skipping: Item sheet does not ship both English and Japanese on this install (got {langs:?})",
        );
        return;
    }

    let sheet = sq
        .sheet("Item")
        .expect("sheet(\"Item\") should open on a real install");

    let en_ids: Vec<u32> = sheet
        .rows(Language::English)
        .take(100)
        .collect::<Result<Vec<_>, _>>()
        .expect("English Item rows should decode")
        .iter()
        .map(sqpack_rs::excel::OwnedRow::row_id)
        .collect();
    let ja_ids: Vec<u32> = sheet
        .rows(Language::Japanese)
        .take(100)
        .collect::<Result<Vec<_>, _>>()
        .expect("Japanese Item rows should decode")
        .iter()
        .map(sqpack_rs::excel::OwnedRow::row_id)
        .collect();

    assert_eq!(en_ids.len(), 100, "expected 100 English rows");
    assert_eq!(ja_ids.len(), 100, "expected 100 Japanese rows");
    assert_eq!(
        en_ids, ja_ids,
        "Item row_ids must be identical across en/ja — divergence indicates the \
         per-language path-suffix logic regressed",
    );
    eprintln!(
        "Item row_id parity (en/ja): first 5 = {:?}, ..., last 5 = {:?}",
        &en_ids[..5],
        &en_ids[en_ids.len() - 5..],
    );
}

#[test]
#[ignore = "requires real FFXIV install; run with `cargo test -- --ignored`"]
fn sub_rows_sheet_iterates_flattened_on_real_install() {
    use sqpack_rs::excel::SheetKind;
    use std::collections::HashSet;

    let Some(data_dir) = common::maybe_data_dir() else {
        eprintln!("skipping: no sqpack data available (set SQPACK_TEST_DATA_DIR)");
        return;
    };
    let Some(install_root) = common::install_root_from_data_dir(&data_dir) else {
        eprintln!(
            "skipping: could not derive install root from {}",
            data_dir.display()
        );
        return;
    };

    let sq = SqPack::open(&install_root).expect("SqPack::open should succeed on a real install");

    let candidates = [
        "QuestRedoChapterUI",
        "Quest",
        "Achievement",
        "WKSEmergencyMissionGroup",
        "AnimaWeaponFUITalk",
        "ZoneTimeline",
    ];
    let mut found_subrows = false;
    let mut found_flattening = false;
    for name in candidates {
        let Ok(sheet) = sq.sheet(name) else { continue };
        if sheet.kind() != SheetKind::SubRows {
            continue;
        }
        let lang = if sheet.languages().contains(&Language::English) {
            Language::English
        } else {
            *sheet
                .languages()
                .first()
                .expect("SubRows sheet must declare at least one language")
        };
        let rows: Vec<_> = match sheet.rows(lang).take(5000).collect::<Result<Vec<_>, _>>() {
            Ok(r) => r,
            Err(_) => continue,
        };
        if rows.is_empty() {
            continue;
        }
        found_subrows = true;

        let has_sub_ids = rows.iter().all(|r| r.sub_row_id().is_some());
        assert!(
            has_sub_ids,
            "SubRows sheet {name} must yield rows with sub_row_id() populated",
        );

        let unique_top_ids: HashSet<u32> = rows
            .iter()
            .map(sqpack_rs::excel::OwnedRow::row_id)
            .collect();
        if rows.len() > unique_top_ids.len() {
            let mut counts: std::collections::HashMap<u32, usize> =
                std::collections::HashMap::new();
            for r in &rows {
                *counts.entry(r.row_id()).or_default() += 1;
            }
            let max_count = counts.values().copied().max().unwrap_or(0);
            assert!(
                max_count > 1,
                "expected at least one parent row_id to appear with multiple sub-rows; \
                 max sibling count was {max_count}",
            );
            eprintln!(
                "SubRows iter on {name} ({lang:?}): {} OwnedRows, {} unique row_ids, \
                 max siblings={max_count} — flattening verified",
                rows.len(),
                unique_top_ids.len(),
            );
            found_flattening = true;
            break;
        }
        eprintln!(
            "SubRows iter on {name} ({lang:?}): {} OwnedRows, {} unique row_ids — \
             no multi-sub-row entries (1 sub-row per parent), sub_row_id populated",
            rows.len(),
            unique_top_ids.len(),
        );
    }
    assert!(
        found_subrows,
        "at least one SubRows-kind sheet must exist among the candidates",
    );
    if !found_flattening {
        eprintln!(
            "note: no candidate SubRows sheet exhibits multi-sub-row flattening on \
             this install; sub_row_id population was verified instead",
        );
    }
}

#[test]
#[ignore = "requires real FFXIV install; run with `cargo test -- --ignored`"]
fn single_language_sheet_yields_language_not_available_on_real_install() {
    let Some(data_dir) = common::maybe_data_dir() else {
        eprintln!("skipping: no sqpack data available (set SQPACK_TEST_DATA_DIR)");
        return;
    };
    let Some(install_root) = common::install_root_from_data_dir(&data_dir) else {
        eprintln!(
            "skipping: could not derive install root from {}",
            data_dir.display()
        );
        return;
    };

    let sq = SqPack::open(&install_root).expect("SqPack::open should succeed on a real install");

    let candidates = ["BaseParam", "Behavior", "EventItem", "Action"];
    let mut chosen_name: Option<&str> = None;
    for name in candidates {
        if let Ok(header) = sq.excel_header(name) {
            if !header.languages().contains(&Language::German) {
                chosen_name = Some(name);
                break;
            }
        }
    }
    let Some(name) = chosen_name else {
        eprintln!(
            "skipping: every candidate sheet ships German on this install — \
             can't exercise the LanguageNotAvailable path",
        );
        return;
    };

    let sheet = sq
        .sheet(name)
        .expect("candidate sheet should open on a real install");
    let mut iter = sheet.rows(Language::German);
    let first = iter.next().expect("iterator must yield at least Err");
    assert!(
        matches!(first, Err(sqpack_rs::Error::LanguageNotAvailable { .. })),
        "first item must be LanguageNotAvailable; got {first:?}",
    );
    assert!(
        iter.next().is_none(),
        "iterator must latch done=true after LanguageNotAvailable",
    );
    eprintln!("LanguageNotAvailable negative case on {name}: confirmed latch");
}
