//! Daemon-side wire-format crosscheck against the shared vector at
//! `tests/crosscheck/vector.json` (at the repo root, two levels up
//! from this crate).
//!
//! Each fixture in vector.json was generated by `reference_signer.py`
//! using independent Python crypto, then is consumed by:
//!   - the TypeScript SDK (via tests/crosscheck/run_ts.mjs)
//!   - the Python SDK (via licensing-client-python/tests/test_crosscheck.py)
//!   - the Rust SDK (via its own unit tests against the same byte layouts)
//!
//! What was missing: the **daemon itself** never ran against the same
//! vectors. Drift in the daemon's parser (which uses the same byte
//! layouts but shares no code with the SDKs) could ship undetected
//! until an SDK on the wire couldn't validate a daemon-issued key.
//!
//! This file closes the loop. For every fixture we call
//! `crypto::parse_key` and assert that every parsed field matches
//! the `expected` block byte-for-byte. If a future change to the
//! daemon's parser drifts away from the wire format the SDKs
//! enforce, this fails before the build even lands in a .s9pk.
//!
//! Signature verification is intentionally NOT included here — the
//! crypto unit tests in `src/crypto/mod.rs` already prove the
//! daemon's sign+verify roundtrip works, and the SDKs prove the
//! same key verifies in three independent crypto implementations.
//! What hadn't been pinned was the parser-to-fields contract, which
//! is what this file enforces.

use keysat::crypto;
use serde_json::Value;
use uuid::Uuid;

const VECTOR_PATH: &str = concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/../../tests/crosscheck/vector.json"
);

fn load_vector() -> Value {
    let raw = std::fs::read_to_string(VECTOR_PATH).unwrap_or_else(|e| {
        panic!("crosscheck vector not at {VECTOR_PATH}: {e}\n\
                If you cloned without the parent repo, make sure the \
                tests/crosscheck/ directory is present.")
    });
    serde_json::from_str(&raw).expect("vector.json should be valid JSON")
}

/// Run one fixture. Asserts every `expected` field matches the
/// daemon's `parse_key` output. The vector's field names mirror
/// what the SDKs use, which is camelCase; the daemon uses snake_case
/// for the same things internally.
fn check_fixture(name: &str, fixture: &Value) {
    let key = fixture["licenseKey"]
        .as_str()
        .unwrap_or_else(|| panic!("{name}: missing licenseKey"));
    let expected = &fixture["expected"];

    let (payload, _signature, _signed_bytes) = crypto::parse_key(key)
        .unwrap_or_else(|e| panic!("{name}: parse_key failed: {e}"));

    // Version
    let want_version = expected["version"].as_u64().unwrap();
    assert_eq!(
        payload.version as u64, want_version,
        "{name}: version mismatch"
    );

    // UUIDs
    let want_product = Uuid::parse_str(expected["productUuid"].as_str().unwrap()).unwrap();
    assert_eq!(payload.product_id, want_product, "{name}: product_id");

    let want_license = Uuid::parse_str(expected["licenseUuid"].as_str().unwrap()).unwrap();
    assert_eq!(payload.license_id, want_license, "{name}: license_id");

    // Times (Unix seconds)
    let want_issued = expected["issuedAt"].as_i64().unwrap();
    assert_eq!(payload.issued_at, want_issued, "{name}: issued_at");

    let want_expires = expected["expiresAt"].as_i64().unwrap();
    assert_eq!(payload.expires_at, want_expires, "{name}: expires_at");

    // Flags + derived bits
    let want_flags = expected["flags"].as_u64().unwrap();
    assert_eq!(payload.flags as u64, want_flags, "{name}: flags");

    let want_fp_bound = expected["isFingerprintBound"].as_bool().unwrap();
    assert_eq!(
        payload.is_fingerprint_bound(),
        want_fp_bound,
        "{name}: is_fingerprint_bound"
    );

    let want_trial = expected["isTrial"].as_bool().unwrap();
    assert_eq!(payload.is_trial(), want_trial, "{name}: is_trial");

    // Entitlements (order-sensitive — wire format preserves order)
    let want_ents: Vec<String> = expected["entitlements"]
        .as_array()
        .unwrap()
        .iter()
        .map(|v| v.as_str().unwrap().to_string())
        .collect();
    assert_eq!(payload.entitlements, want_ents, "{name}: entitlements");

    // Fingerprint hash. The `expected.fingerprintHashHex` is what the
    // wire format must contain; we compare against payload's bytes.
    let want_fp_hex = expected["fingerprintHashHex"].as_str().unwrap();
    let got_fp_hex = hex::encode(payload.fingerprint_hash);
    assert_eq!(got_fp_hex, want_fp_hex, "{name}: fingerprint_hash");

    // Cross-check: if `fingerprintRaw` is provided and binding is
    // active, hashing the raw fingerprint with the daemon's helper
    // should produce the same hex. This locks in the
    // `hash_fingerprint` SHA-256 contract that SDKs rely on.
    if want_fp_bound {
        if let Some(raw) = expected["fingerprintRaw"].as_str() {
            let computed = crypto::hash_fingerprint(raw);
            let computed_hex = hex::encode(computed);
            assert_eq!(
                computed_hex, want_fp_hex,
                "{name}: hash_fingerprint(raw) does not match wire bytes"
            );
        }
    }
}

#[test]
fn vector_v1_legacy_roundtrip() {
    let vector = load_vector();
    check_fixture("v1", &vector["v1"]);
}

#[test]
fn vector_v2_trial_with_entitlements() {
    let vector = load_vector();
    check_fixture("v2", &vector["v2"]);
}

#[test]
fn vector_v2_perpetual_unbound() {
    let vector = load_vector();
    check_fixture("v2_perpetual_unbound", &vector["v2_perpetual_unbound"]);
}

/// The vector file's publicKeyPem is the master crosscheck key —
/// every fixture's signature was made against that PEM. This test
/// just locks in that the file has it (so a future regeneration of
/// the vectors keeps the public-key surface accessible to all
/// tooling that reads vector.json).
#[test]
fn vector_has_public_key_pem() {
    let vector = load_vector();
    let pem = vector["publicKeyPem"]
        .as_str()
        .expect("vector.json must include publicKeyPem");
    assert!(
        pem.contains("BEGIN PUBLIC KEY"),
        "publicKeyPem should be a PEM block: {pem}"
    );
}
