//! Analysis-side goldens: every row in the xenia-cpu fixtures must
//! round-trip cleanly through the [`xenia_analysis::ppc`] shim. This
//! pins the shim's behaviour to the canonical `xenia_cpu::disasm::format`
//! output so that any future refactor of the shim layer surfaces here.
//!
//! Loads the same JSON fixtures committed under
//! `crates/xenia-cpu/tests/golden/`. No separate analysis-side fixture
//! files — the cpu canon is the source of truth.

use std::path::PathBuf;

use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct GoldenRow {
    label: String,
    raw: String,
    addr: String,
    mnemonic: String,
    operands: String,
    #[serde(default)]
    ext_mnemonic: Option<String>,
    #[serde(default)]
    ext_operands: Option<String>,
    #[serde(default)]
    branch_target: Option<String>,
}

#[derive(Debug, Deserialize)]
struct GoldenFile {
    rows: Vec<GoldenRow>,
}

fn cpu_fixture(name: &str) -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("..")
        .join("xenia-cpu")
        .join("tests")
        .join("golden")
        .join(name)
}

fn parse_hex(s: &str) -> u32 {
    let trimmed = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")).unwrap_or(s);
    u32::from_str_radix(trimmed, 16).expect("hex u32")
}

/// Verify the shim's `Decoded { base, ext }` mirrors the canonical fields
/// from `xenia_cpu::disasm::format` for every fixture row.
fn check_fixture(fixture_name: &str) {
    let path = cpu_fixture(fixture_name);
    assert!(
        path.exists(),
        "missing fixture {} — run `cargo test -p xenia-cpu --test disasm_goldens` to (re)generate it",
        path.display()
    );
    let src = std::fs::read_to_string(&path).unwrap();
    let golden: GoldenFile = serde_json::from_str(&src).unwrap();

    for row in &golden.rows {
        let raw = parse_hex(&row.raw);
        let addr = parse_hex(&row.addr);

        let canonical =
            xenia_cpu::disasm::format(&xenia_cpu::decode(raw, addr));
        let shim = xenia_analysis::ppc::disasm(raw, addr);

        assert_eq!(
            shim.base, canonical.disasm,
            "shim.base drifted for {} (raw={})",
            row.label, row.raw,
        );
        assert_eq!(
            shim.ext, canonical.ext_disasm,
            "shim.ext drifted for {} (raw={})",
            row.label, row.raw,
        );

        // Also pin against the fixture's structured fields — guards against
        // someone changing the cpu canon without regenerating the fixture.
        assert_eq!(canonical.mnemonic, row.mnemonic, "mnemonic drift: {}", row.label);
        assert_eq!(canonical.operands, row.operands, "operands drift: {}", row.label);
        assert_eq!(canonical.ext_mnemonic, row.ext_mnemonic, "ext_mnemonic drift: {}", row.label);
        assert_eq!(canonical.ext_operands, row.ext_operands, "ext_operands drift: {}", row.label);

        let target_str = canonical.branch_target.map(|t| format!("0x{t:08X}"));
        assert_eq!(target_str, row.branch_target, "branch_target drift: {}", row.label);
    }
}

#[test]
fn analysis_shim_matches_base_mnemonics() {
    check_fixture("base_mnemonics.json");
}

#[test]
fn analysis_shim_matches_extended_mnemonics() {
    check_fixture("extended_mnemonics.json");
}

#[test]
fn analysis_shim_matches_vmx128_registers() {
    check_fixture("vmx128_registers.json");
}

/// Spot-check that the shim's `display()` returns the extended form when
/// present and falls back to the base otherwise. This is the contract
/// `formatter.rs` and the .asm output rely on.
#[test]
fn shim_display_prefers_extended() {
    // ori r0, r0, 0 → base "ori     r0, r0, 0x0", ext "nop"
    let d = xenia_analysis::ppc::disasm(0x60000000, 0);
    assert_eq!(d.display(), "nop");

    // addi r3, r1, 16 → no extended form, display falls back to base
    let raw = (14u32 << 26) | (3 << 21) | (1 << 16) | 16;
    let d = xenia_analysis::ppc::disasm(raw, 0);
    assert!(
        d.ext.is_none(),
        "addi r3, r1, 16 has no extended form (only addi r3, r0, … → li)"
    );
    assert_eq!(d.display(), d.base);
}
