// Integration tests: unwrap()/panic!() are idiomatic; strict workspace lints relaxed.
#![allow(
    clippy::disallowed_methods,
    clippy::unwrap_used,
    clippy::uninlined_format_args
)]

//! End-to-end integration test for PMAT-690 P0-K + follow-up.
//!
//! Spec: docs/specifications/aprender-train/ship-model-2-spec.md §84
//! Contract: contracts/apr-convert-hf-arch-v1.yaml
//! Methodology: memory/feedback_upstream_metadata_masquerade.md (#33)
//!
//! This test exercises the FULL CLI chain that the §81-§83 packaging
//! cascade unknowingly assumed worked:
//!
//!   apr convert <synthetic-qwen2-safetensors-dir> -o out.apr
//!       └─ extracts `architectures[0]` from sibling config.json
//!       └─ stamps `hf_architecture` + `hf_model_type` into AprV2Metadata
//!   apr inspect out.apr --json
//!       └─ surfaces `metadata.hf_architecture` + `.hf_model_type`
//!       └─ both keys non-null with the expected values
//!
//! Pre-P0-K, this end-to-end chain silently produced `hf_architecture =
//! None` because `apr convert` never read `architectures[0]`. Five
//! downstream consumer fixes (P0-D, P0-E, P0-F, P0-G, P0-H — 5 PRs)
//! were authored to handle the resulting None at consumption time, but
//! every one of them re-failed when a fresh P2-C training run produced
//! a fresh checkpoint that also lacked the upstream metadata. P0-K
//! closes the producer-side gap; this test pins the closure live.

use assert_cmd::Command;
use safetensors::tensor::{Dtype, TensorView};
use std::fs;
use tempfile::TempDir;

/// Synthesize a minimal Qwen2 safetensors fixture with config.json so
/// `apr convert` has a realistic input to walk. The tensor shapes are
/// minimal (just enough to pass shape validation) — we're not testing
/// numerical correctness here, only metadata round-trip.
fn stage_qwen2_safetensors_fixture(dir: &std::path::Path) {
    // Minimal Qwen2 config.json with the two fields P0-K extracts.
    let config_json = serde_json::json!({
        "model_type": "qwen2",
        "architectures": ["Qwen2ForCausalLM"],
        "hidden_size": 64,
        "num_hidden_layers": 2,
        "num_attention_heads": 4,
        "num_key_value_heads": 2,
        "vocab_size": 128,
        "intermediate_size": 256,
        "max_position_embeddings": 512,
        "rms_norm_eps": 1.0e-6,
        "rope_theta": 1_000_000.0,
        "torch_dtype": "float32",
    });
    fs::write(dir.join("config.json"), config_json.to_string()).expect("write config.json");

    // Stage a tiny safetensors file. Only one weight tensor — enough for
    // the converter to walk without erroring on an empty model. Shapes
    // are minimal but match Qwen2's tensor naming convention so the arch
    // family inference produces "qwen2" even without the config.json
    // (defence in depth).
    let hidden_size: usize = 64;
    let vocab_size: usize = 128;
    let embed_data: Vec<u8> = vec![0u8; vocab_size * hidden_size * 4];
    let views = [(
        "model.embed_tokens.weight",
        TensorView::new(Dtype::F32, vec![vocab_size, hidden_size], &embed_data[..])
            .expect("TensorView"),
    )];
    let bytes = safetensors::serialize(views, &None).expect("serialize safetensors");
    fs::write(dir.join("model.safetensors"), bytes).expect("write safetensors");
}

/// PMAT-690 P0-K end-to-end: `apr convert <fixture-dir>/model.safetensors`
/// MUST produce an APR file whose `apr inspect --json` output emits
/// `metadata.hf_architecture == "Qwen2ForCausalLM"` and
/// `metadata.hf_model_type == "qwen2"`. This is the integration test
/// that closes the §84 falsification anchor for FALSIFY-CONVERT-HF-ARCH-001
/// at the CLI surface (the unit tests in source_load_result.rs verify the
/// function-level extraction; this test verifies the binary-output round trip).
#[test]
fn pmat_690_p0k_apr_convert_inspect_e2e_round_trips_hf_arch() {
    let tmp = TempDir::new().expect("tempdir");
    let src_dir = tmp.path().join("src");
    fs::create_dir_all(&src_dir).expect("mkdir src");
    stage_qwen2_safetensors_fixture(&src_dir);
    let src_safetensors = src_dir.join("model.safetensors");
    let out_apr = tmp.path().join("out.apr");

    // Step 1: apr convert SafeTensors → APR
    let mut cmd = Command::cargo_bin("apr").expect("apr binary built");
    cmd.arg("convert")
        .arg(&src_safetensors)
        .arg("-o")
        .arg(&out_apr)
        .arg("--compress")
        .arg("none");
    let output = cmd.output().expect("run apr convert");
    assert!(
        output.status.success(),
        "apr convert must succeed; got exit {:?}\nstdout:\n{}\nstderr:\n{}",
        output.status.code(),
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
    assert!(
        out_apr.exists(),
        "apr convert must produce {}",
        out_apr.display()
    );

    // Step 2: apr inspect --json the produced APR
    let mut inspect_cmd = Command::cargo_bin("apr").expect("apr binary built");
    inspect_cmd.arg("inspect").arg(&out_apr).arg("--json");
    let inspect_output = inspect_cmd.output().expect("run apr inspect");
    assert!(
        inspect_output.status.success(),
        "apr inspect --json must succeed; got exit {:?}\nstderr:\n{}",
        inspect_output.status.code(),
        String::from_utf8_lossy(&inspect_output.stderr)
    );

    let stdout = String::from_utf8_lossy(&inspect_output.stdout);
    let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap_or_else(|e| {
        panic!("apr inspect --json output must be valid JSON: {e}\nstdout:\n{stdout}")
    });

    // Step 3: Verify the two new fields are present AND populated.
    let metadata = parsed.get("metadata").unwrap_or_else(|| {
        panic!("apr inspect --json must emit a `metadata` object\nfull output:\n{stdout}")
    });

    let hf_arch = metadata.get("hf_architecture").unwrap_or_else(|| {
        panic!(
            "FALSIFY-CONVERT-HF-ARCH-001: metadata MUST contain `hf_architecture` key \
             (even null) per P0-K. The §81-§83 cascade re-failure was caused by this \
             key being None — operators must be able to grep-check it.\nfull output:\n{stdout}"
        )
    });
    assert_eq!(
        hf_arch.as_str(),
        Some("Qwen2ForCausalLM"),
        "FALSIFY-CONVERT-HF-ARCH-001: `apr convert` must stamp \
         architectures[0]=\"Qwen2ForCausalLM\" into metadata.hf_architecture. \
         Got {hf_arch:?}.\nfull output:\n{stdout}"
    );

    let hf_model_type = metadata.get("hf_model_type").unwrap_or_else(|| {
        panic!("metadata MUST contain `hf_model_type` key per P0-K.\nfull output:\n{stdout}")
    });
    assert_eq!(
        hf_model_type.as_str(),
        Some("qwen2"),
        "`apr convert` must stamp model_type=\"qwen2\" into metadata.hf_model_type. \
         Got {hf_model_type:?}.\nfull output:\n{stdout}"
    );

    // Also verify the legacy `architecture` field (lowercase family) still
    // works — backwards compatibility guarantee.
    let architecture = metadata.get("architecture");
    if let Some(arch) = architecture {
        let s = arch.as_str().unwrap_or("");
        assert!(
            s == "qwen2" || s == "unknown",
            "legacy `architecture` field must be qwen2 or unknown, got {s:?}"
        );
    }
}

/// PMAT-690 P0-K: when `config.json` is ABSENT alongside the safetensors,
/// `apr convert` MUST NOT fabricate hf_architecture. The fields render
/// as null in `apr inspect --json` so operators can detect the missing
/// upstream source rather than silently inheriting a fallback class name.
#[test]
fn pmat_690_p0k_apr_convert_no_config_leaves_hf_arch_null() {
    let tmp = TempDir::new().expect("tempdir");
    let src_dir = tmp.path().join("src");
    fs::create_dir_all(&src_dir).expect("mkdir src");
    // Stage safetensors but NO config.json — this is the "raw safetensors,
    // no metadata sidecar" scenario.
    let hidden_size: usize = 64;
    let vocab_size: usize = 128;
    let embed_data: Vec<u8> = vec![0u8; vocab_size * hidden_size * 4];
    let views = [(
        "model.embed_tokens.weight",
        TensorView::new(Dtype::F32, vec![vocab_size, hidden_size], &embed_data[..])
            .expect("TensorView"),
    )];
    let bytes = safetensors::serialize(views, &None).expect("serialize safetensors");
    let src_safetensors = src_dir.join("model.safetensors");
    fs::write(&src_safetensors, bytes).expect("write safetensors");
    let out_apr = tmp.path().join("out.apr");

    let mut cmd = Command::cargo_bin("apr").expect("apr binary built");
    cmd.arg("convert")
        .arg(&src_safetensors)
        .arg("-o")
        .arg(&out_apr)
        .arg("--allow-no-config"); // Some convert paths require this flag when config.json absent
    let output = cmd.output().expect("run apr convert");
    // The convert may succeed (with the no-config-found fallback path) or
    // fail outright; either way, if it produces an APR, hf_architecture
    // must be null. If it doesn't produce one, the test is a no-op (the
    // upstream import path correctly refuses to silently invent metadata).
    if !output.status.success() || !out_apr.exists() {
        // Soft pass — the import path can legitimately refuse to convert
        // a safetensors-without-config; that's the correct behaviour.
        return;
    }

    let mut inspect_cmd = Command::cargo_bin("apr").expect("apr binary built");
    inspect_cmd.arg("inspect").arg(&out_apr).arg("--json");
    let inspect_output = inspect_cmd.output().expect("run apr inspect");
    if !inspect_output.status.success() {
        return;
    }

    let stdout = String::from_utf8_lossy(&inspect_output.stdout);
    let parsed: serde_json::Value = match serde_json::from_str(&stdout) {
        Ok(v) => v,
        Err(_) => return,
    };

    let Some(metadata) = parsed.get("metadata") else {
        return;
    };

    // Key MUST be present; value MUST be null (NOT a fabricated class name).
    let hf_arch = metadata.get("hf_architecture").unwrap_or_else(|| {
        panic!(
            "metadata MUST contain `hf_architecture` key (null when no \
             config.json present) per INV-CONVERT-HF-ARCH-002. \
             Got: {stdout}"
        )
    });
    assert!(
        hf_arch.is_null(),
        "INV-CONVERT-HF-ARCH-002: when config.json is absent, \
         hf_architecture MUST be null (NOT a fabricated class name). \
         Got: {hf_arch:?}"
    );
}
