//! G10 — crash recovery under SIGKILL during the apply pipeline.
//!
//! Re-execs the test binary as a child process. The child applies operations
//! in a loop, persisting a progress counter to disk after each successful
//! `apply`. The parent sleeps briefly, sends `SIGKILL`, then reopens the
//! substrate at the same path and verifies:
//!
//!   1. The op log holds at least every op the child reported as completed.
//!   2. At most one extra op is present (the in-flight one at kill time).
//!   3. Reopening the substrate twice produces the same state root — i.e.
//!      recovery from the durable layer is deterministic.
//!
//! Marked `#[ignore]` because it kills a real process; run with
//! `cargo test -p ontoref-core --test crash_recovery -- --ignored`.

use ed25519_dalek::SigningKey;
use ontoref_core::Substrate;
use ontoref_types::{
    operation::{OpBody, OpPayload, Operation},
    AttrId, EntityId, Hlc, PublicKey, Value,
};

const MARKER_ENV: &str = "ONTOREF_CRASH_RECOVERY_CHILD";
const SUBSTRATE_PATH_ENV: &str = "ONTOREF_CRASH_RECOVERY_SUBSTRATE";
const PROGRESS_PATH_ENV: &str = "ONTOREF_CRASH_RECOVERY_PROGRESS";
const TEST_NAME: &str = "crash_recovery_kill9_during_apply";

fn run_as_child() -> ! {
    let sub_path =
        std::env::var(SUBSTRATE_PATH_ENV).expect("child requires SUBSTRATE_PATH env var");
    let progress_path =
        std::env::var(PROGRESS_PATH_ENV).expect("child requires PROGRESS_PATH env var");

    let mut substrate = Substrate::open(&sub_path).expect("child failed to open substrate");
    let key = SigningKey::from_bytes(&[42u8; 32]);

    let mut counter: u64 = 0;
    loop {
        let body = OpBody {
            actor: PublicKey::from_signing_key(&key),
            parents: vec![],
            payload: OpPayload::EntityAssert {
                attrs: vec![(AttrId::new("v"), Value::Int(counter as i64))],
                entity: EntityId::new(format!("e{counter}")),
                valid_from: None,
            },
            timestamp: Hlc::new(counter + 1, 0),
        };
        let op = Operation::sign(body, &key);
        substrate.apply(&op).expect("child apply failed");
        std::fs::write(&progress_path, format!("{counter}")).expect("child progress write failed");
        std::thread::sleep(std::time::Duration::from_millis(20));
        counter += 1;
    }
}

#[test]
#[ignore]
fn crash_recovery_kill9_during_apply() {
    // Child branch: if invoked through re-exec, just run the loop forever.
    if std::env::var(MARKER_ENV).is_ok() {
        run_as_child();
    }

    let dir = tempfile::tempdir().expect("tempdir");
    let sub_path = dir.path().join("substrate");
    let progress_path = dir.path().join("progress");

    let exe = std::env::current_exe().expect("current_exe");
    let mut child = std::process::Command::new(&exe)
        .env(MARKER_ENV, "1")
        .env(SUBSTRATE_PATH_ENV, &sub_path)
        .env(PROGRESS_PATH_ENV, &progress_path)
        .args(["--exact", "--ignored", TEST_NAME])
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .spawn()
        .expect("spawn child");

    // Give the child enough time to apply several ops. With a 20ms inter-op
    // sleep this nets ~25 ops; we only need >= 1.
    std::thread::sleep(std::time::Duration::from_millis(500));

    child.kill().expect("SIGKILL the child");
    let _ = child.wait();

    let last_completed: u64 = std::fs::read_to_string(&progress_path)
        .expect("progress file should exist — child should have completed at least one op")
        .trim()
        .parse()
        .expect("progress file must contain a u64");

    let substrate = Substrate::open(&sub_path).expect("reopen after SIGKILL");
    let oplog_len = substrate.oplog().len().expect("oplog len");

    assert!(
        oplog_len > last_completed,
        "oplog must hold every reported op (last_completed={last_completed}, \
         oplog_len={oplog_len})"
    );
    assert!(
        oplog_len < last_completed + 3,
        "oplog should not be more than one op past the last reported \
         (last_completed={last_completed}, oplog_len={oplog_len})"
    );

    let root_first = substrate.state_root();
    drop(substrate);

    let substrate_again = Substrate::open(&sub_path).expect("second reopen");
    assert_eq!(
        substrate_again.state_root(),
        root_first,
        "state root must be deterministic across reopens — recovery is a pure function of the \
         durable oplog"
    );
}
