//! The Rust adapter for the shared golden vectors — the third implementation alongside
//! `tests/adapters/js_adapter.mjs` and `tests/adapters/py_adapter.py`. Reads the same
//! `tests/fixtures/track-format.json` and asserts each case's normalized form + round-trip.

use serde_json::{json, Value};
use track_format::{parse, serialize, End, Track};

/// Build the neutral normalized structure (docs/track-format.md §5) from a parsed Track.
fn norm(t: &Track) -> Value {
    json!({
        "bpm": t.bpm,
        "bars": t.bars,
        "volume": t.volume,
        "countMs": t.count_ms,
        "ramp": t.ramp.as_ref().map(|r| json!({"start": r.start, "amt": r.amt, "every": r.every})),
        "trainer": t.trainer.as_ref().map(|x| json!({"play": x.play, "mute": x.mute})),
        "rep": match t.end { None => Value::Null, Some(_) => json!(t.rep.unwrap_or(1)) },
        "end": match &t.end {
            None => Value::Null,
            Some(End::Stop) => json!("stop"),
            Some(End::Goto(n)) => json!(n),
        },
        "lanes": t.lanes.iter().map(|l| {
            let mut o = json!({
                "sound": l.sound, "groups": l.groups, "sub": l.sub, "swing": l.swing,
                "poly": l.poly, "mute": l.mute, "gainDb": l.gain_db, "levels": l.levels
            });
            // `orns` defaults to all-zeros and is omitted then, so legacy vectors (no `orns`) still match.
            if l.orns.iter().any(|&v| v != 0) {
                o.as_object_mut().unwrap().insert("orns".into(), json!(l.orns));
            }
            o
        }).collect::<Vec<_>>(),
    })
}

/// Deep-equal that treats all JSON numbers as f64 (so 0 == 0.0 across the int/float boundary).
fn json_eq(a: &Value, b: &Value) -> bool {
    match (a, b) {
        (Value::Number(x), Value::Number(y)) => x.as_f64() == y.as_f64(),
        (Value::Array(x), Value::Array(y)) => {
            x.len() == y.len() && x.iter().zip(y).all(|(p, q)| json_eq(p, q))
        }
        (Value::Object(x), Value::Object(y)) => {
            x.len() == y.len() && x.iter().all(|(k, v)| y.get(k).map_or(false, |w| json_eq(v, w)))
        }
        _ => a == b,
    }
}

fn fixtures() -> Value {
    let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../tests/fixtures/track-format.json");
    let raw = std::fs::read_to_string(path).expect("read fixtures json");
    serde_json::from_str(&raw).expect("parse fixtures json")
}

#[test]
fn conformance() {
    let doc = fixtures();
    let mut fails = 0;
    for case in doc["cases"].as_array().unwrap() {
        let id = case["id"].as_str().unwrap_or("?");
        let patch = case["in"].as_str().unwrap_or("");
        let got = norm(&parse(patch));
        if !json_eq(&case["norm"], &got) {
            fails += 1;
            eprintln!("FAIL {id}\n  expected: {}\n  got:      {got}", case["norm"]);
        }
    }
    assert_eq!(fails, 0, "{fails} conformance mismatch(es)");
}

#[test]
fn idempotent() {
    let doc = fixtures();
    for case in doc["cases"].as_array().unwrap() {
        let patch = case["in"].as_str().unwrap_or("");
        let c1 = serialize(&parse(patch));
        let c2 = serialize(&parse(&c1));
        assert_eq!(c1, c2, "non-idempotent serialize for {}", case["id"]);
    }
}
