//! Typecheck pipeline for the `state.ncl` renderer (O1.5 / D29).
//!
//! Verifies two contracts:
//!   1. Rendered bytes from any well-formed `StateConfig` pass `nickel
//!      typecheck`.
//!   2. The transactional pipeline aborts and removes its staging file when
//!      typecheck fails — proving the invariant "every NCL file on disk parses
//!      against its schema".

use std::path::{Path, PathBuf};
use std::process::Command;

use ontoref_ontology::render::render_state_ncl;
use ontoref_ontology::render_pipeline::render_and_write;
use ontoref_ontology::types::StateConfig;

/// Project root that holds the `.ontoref/` spine.
///
/// Walks up from this crate until it finds the directory containing
/// `.ontoref/`. Post-ADR-048 (constellation) the spine sits at the
/// constellation parent, outside `code/`; pre-ADR-048 it was inside the
/// workspace. Searching ancestors resolves it under either layout instead of
/// hard-coding a hop count.
fn workspace_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .ancestors()
        .find(|dir| dir.join(".ontoref").is_dir())
        .map(Path::to_path_buf)
        .expect("a `.ontoref/` spine must exist in an ancestor of CARGO_MANIFEST_DIR")
}

fn nickel_export_state(path: &Path) -> StateConfig {
    let output = Command::new("nickel")
        .arg("export")
        .arg("--format")
        .arg("json")
        .arg(path)
        .output()
        .expect("nickel binary must be on PATH for this test");
    assert!(output.status.success(), "nickel export failed");
    serde_json::from_slice(&output.stdout).expect("StateConfig parse")
}

fn nickel_typecheck(path: &Path) -> Result<(), String> {
    let output = Command::new("nickel")
        .arg("typecheck")
        .arg(path)
        .output()
        .expect("nickel binary on PATH");
    if output.status.success() {
        Ok(())
    } else {
        Err(String::from_utf8_lossy(&output.stderr).into_owned())
    }
}

#[test]
fn rendered_state_ncl_passes_nickel_typecheck() {
    let root = workspace_root();
    let source = ontoref_ontology::layout::resolve_section(&root, "ontology/state.ncl");
    let state = nickel_export_state(&source);

    let tmp = tempfile::tempdir().expect("tempdir");
    let staged = tmp.path().join("state.ncl");
    std::fs::write(&staged, render_state_ncl(&state)).expect("write rendered");

    nickel_typecheck(&staged).expect("rendered state.ncl must pass nickel typecheck");
}

#[test]
fn pipeline_writes_atomically_when_typecheck_passes() {
    let root = workspace_root();
    let source = ontoref_ontology::layout::resolve_section(&root, "ontology/state.ncl");
    let state = nickel_export_state(&source);

    let tmp = tempfile::tempdir().expect("tempdir");
    let target = tmp.path().join("state.ncl");

    render_and_write(&state, &target).expect("pipeline succeeds");
    assert!(target.exists(), "target file must be written on success");
    let written = std::fs::read_to_string(&target).expect("read target");
    assert_eq!(written, render_state_ncl(&state));

    // No leftover staging files in the temp directory after a successful run.
    let entries: Vec<_> = std::fs::read_dir(tmp.path())
        .expect("read tempdir")
        .filter_map(|e| e.ok())
        .map(|e| e.file_name())
        .collect();
    let staging_present = entries
        .iter()
        .any(|n| n.to_string_lossy().ends_with(".staged"));
    assert!(
        !staging_present,
        "staging file must be cleaned up on success"
    );
}
