//! Acceptance test for fail-closed parse-time version dispatch.
//!
//! Without the fail-closed gate, `parse_hbc_header` accepts any `u32`
//! version, routing `0..=83` into the `PreV84` layout and
//! `_ => V98LateToV99` (which swallows every `version > 99` including
//! `u32::MAX`). Failure deferred until `get_version_opcodes` /
//! `get_version_schemas` reject late, after a parsed `HbcFile` was
//! already in hand.
//!
//! With it, the version dispatch rejects any value outside
//! `MIN_SUPPORTED_VERSION..=MAX_SUPPORTED_VERSION` (default `40..=100`)
//! at parse entry with a typed `HermesError::UnsupportedVersion {
//! observed, supported_min, supported_max }` — *before* any layout-
//! dependent reads run.
//!
//! Fixtures live at
//! `tests/fixtures/adversarial/version_dispatch/{too_low,too_high}.hbc`
//! and are regenerated by the `#[ignore]` helper at the bottom of
//! this file. Each is a 128-byte minimal HBC blob carrying the
//! magic + a deliberately out-of-range `version` field.
//!
//! Confirmed: there are no gaps in `40..=100` — every value has explicit
//! opcode / schema / string-id / func-id table entries — so a
//! `version_in_gap.hbc` fixture is not applicable and is omitted.

use std::fs;

use droidsaw_hermes::HermesError;
use droidsaw_hermes::header::{MAX_SUPPORTED_VERSION, MIN_SUPPORTED_VERSION};
use droidsaw_hermes::parser::HbcFile;

const FIXTURE_DIR: &str = "tests/fixtures/adversarial/version_dispatch";

const VERSION_TOO_LOW: u32 = 39;
const VERSION_TOO_HIGH: u32 = 200;

/// 128-byte HBC header carrying the magic + the requested
/// `version` field at offset 8. All other header bytes are zero —
/// the fail-closed gate must fire before any of them are consumed.
fn build_header_with_version(version: u32) -> Vec<u8> {
    let mut buf = vec![0u8; 128];
    buf[0..8].copy_from_slice(&0x1F1903C103BC1FC6u64.to_le_bytes());
    buf[8..12].copy_from_slice(&version.to_le_bytes());
    buf
}

fn assert_unsupported(bytes: &[u8], expected_observed: u32) {
    let err = HbcFile::parse(bytes, None)
        .map(|_| ())
        .expect_err("fail-closed dispatch must reject out-of-range version");
    match err {
        HermesError::UnsupportedVersion {
            observed,
            supported_min,
            supported_max,
        } => {
            assert_eq!(observed, expected_observed, "observed field carries header version");
            assert_eq!(supported_min, MIN_SUPPORTED_VERSION);
            assert_eq!(supported_max, MAX_SUPPORTED_VERSION);
        }
        other => panic!("expected UnsupportedVersion, got {other:?}"),
    }
}

#[test]
fn version_too_low_rejected_at_parse_entry() {
    let bytes = build_header_with_version(VERSION_TOO_LOW);
    assert_unsupported(&bytes, VERSION_TOO_LOW);
}

#[test]
fn version_too_high_rejected_at_parse_entry() {
    let bytes = build_header_with_version(VERSION_TOO_HIGH);
    assert_unsupported(&bytes, VERSION_TOO_HIGH);
}

#[test]
fn version_just_below_min_rejected() {
    let bytes = build_header_with_version(MIN_SUPPORTED_VERSION - 1);
    assert_unsupported(&bytes, MIN_SUPPORTED_VERSION - 1);
}

#[test]
fn version_just_above_max_rejected() {
    let bytes = build_header_with_version(MAX_SUPPORTED_VERSION + 1);
    assert_unsupported(&bytes, MAX_SUPPORTED_VERSION + 1);
}

#[test]
fn version_zero_rejected() {
    let bytes = build_header_with_version(0);
    assert_unsupported(&bytes, 0);
}

#[test]
fn version_u32_max_rejected() {
    let bytes = build_header_with_version(u32::MAX);
    assert_unsupported(&bytes, u32::MAX);
}

#[test]
fn fixture_too_low_bytes_match_disk() {
    let on_disk = fs::read(format!("{FIXTURE_DIR}/version_too_low.hbc"))
        .expect("fixture must be checked in — run regen_version_dispatch_fixtures");
    assert_eq!(on_disk, build_header_with_version(VERSION_TOO_LOW));
}

#[test]
fn fixture_too_high_bytes_match_disk() {
    let on_disk = fs::read(format!("{FIXTURE_DIR}/version_too_high.hbc"))
        .expect("fixture must be checked in — run regen_version_dispatch_fixtures");
    assert_eq!(on_disk, build_header_with_version(VERSION_TOO_HIGH));
}

#[test]
fn fixture_too_low_loads_and_rejects() {
    let bytes = fs::read(format!("{FIXTURE_DIR}/version_too_low.hbc"))
        .expect("fixture must be checked in");
    assert_unsupported(&bytes, VERSION_TOO_LOW);
}

#[test]
fn fixture_too_high_loads_and_rejects() {
    let bytes = fs::read(format!("{FIXTURE_DIR}/version_too_high.hbc"))
        .expect("fixture must be checked in");
    assert_unsupported(&bytes, VERSION_TOO_HIGH);
}

/// Regenerate the on-disk version-dispatch fixtures. `#[ignore]` in
/// CI; run manually with `cargo test --test version_dispatch \
/// regen_ -- --ignored --nocapture` after a structural change to
/// `build_header_with_version`.
#[test]
#[ignore = "regen helper — run manually with --ignored after layout changes"]
fn regen_version_dispatch_fixtures() {
    fs::create_dir_all(FIXTURE_DIR).unwrap();
    for (name, version) in
        [("version_too_low.hbc", VERSION_TOO_LOW), ("version_too_high.hbc", VERSION_TOO_HIGH)]
    {
        let path = format!("{FIXTURE_DIR}/{name}");
        let bytes = build_header_with_version(version);
        fs::write(&path, &bytes).unwrap();
        println!("wrote {} bytes to {path}", bytes.len());
    }
}
