//! Cross-domain e2e coverage (O4).
//!
//! Exercises the three Phase 1 cross-domain ops — `propose_adr`,
//! `update_ontology_node`, `manage_backlog_item` — in a single
//! integration test crate. Each op is dispatched against a fresh
//! [`OpContext`]; the test verifies its witness, the on-disk render,
//! and the resulting commit-layer state. A final replay test rebuilds
//! the substrate from the captured oplog ops and confirms the same
//! state root — the determinism contract that ADR-024 leans on.

use ed25519_dalek::SigningKey;
use ontoref_commit::BinaryMerkle;
use ontoref_ops::{dispatch_op, OpContext, OpError};

fn signing_key() -> SigningKey {
    SigningKey::from_bytes(&[71u8; 32])
}

fn ctx_for(project_root: std::path::PathBuf) -> OpContext {
    OpContext::new_in_memory("developer", "", signing_key()).with_project_root(project_root)
}

fn sample_core_snapshot() -> serde_json::Value {
    serde_json::json!({
        "nodes": [
            {
                "id": "protocol-not-runtime",
                "name": "Protocol, Not Runtime",
                "pole": "Yang",
                "level": "Axiom",
                "description": "Onref is a protocol.",
                "invariant": true,
                "artifact_paths": [],
                "adrs": []
            }
        ],
        "edges": []
    })
}

// ── propose_adr ────────────────────────────────────────────────────────────

#[test]
fn propose_adr_writes_adr_file_and_signs_witness() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let mut ctx = ctx_for(tmp.path().to_path_buf());

    let inputs = serde_json::json!({
        "id": "adr-099",
        "slug": "cross-domain-pilot",
        "title": "Cross-domain piloto",
        "decision": "Adopt the operations layer.",
        "ondaod_evaluation": {
            "engaged_tensions": ["ontology-vs-reflection"],
            "synthesis_state": "Realized",
            "motion_direction": "Static"
        }
    });

    let result = dispatch_op("propose_adr", &inputs, &mut ctx).expect("dispatch ok");
    assert_eq!(result.witness.op_id, "propose_adr");
    assert_eq!(result.witness.witness_shape, "adr_proposed");
    result.witness.verify().expect("witness verifies");

    let adr_path = tmp
        .path()
        .join(".ontoref/adrs/adr-099-cross-domain-pilot.ncl");
    assert!(adr_path.exists(), "ADR file must be written");
    let body = std::fs::read_to_string(&adr_path).expect("read adr");
    assert!(body.contains("status = 'Proposed"));
    assert!(body.contains("Adopt the operations layer."));

    // Effects record exactly one Insert ADR.
    assert_eq!(result.effects.len(), 1);
    assert_eq!(result.effects[0].kind, "Insert");
    assert_eq!(result.effects[0].entity, "adr-099");
}

#[test]
fn propose_adr_rejects_empty_ondaod_with_no_rationale() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let mut ctx = ctx_for(tmp.path().to_path_buf());

    let inputs = serde_json::json!({
        "id": "adr-100",
        "slug": "missing-ondaod",
        "title": "Missing ondaod",
        "decision": "x",
        "ondaod_evaluation": {
            "engaged_tensions": [],
            "synthesis_state": ""
        }
    });
    let err = dispatch_op("propose_adr", &inputs, &mut ctx).expect_err("empty ondaod must reject");
    assert!(matches!(err, OpError::ValidatorRejected { .. }));
}

#[test]
fn propose_adr_rejects_malformed_id() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let mut ctx = ctx_for(tmp.path().to_path_buf());

    let inputs = serde_json::json!({
        "id": "ADR_NOPE",  // bad format
        "slug": "bad",
        "title": "bad",
        "decision": "x",
        "ondaod_evaluation": { "engaged_tensions": ["t"], "synthesis_state": "x" }
    });
    let err =
        dispatch_op("propose_adr", &inputs, &mut ctx).expect_err("malformed adr id must reject");
    assert!(matches!(err, OpError::OpBody { .. }));
}

// ── update_ontology_node ──────────────────────────────────────────────────

#[test]
fn update_ontology_node_inserts_new_node() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let mut ctx = ctx_for(tmp.path().to_path_buf()).with_core(sample_core_snapshot());

    let inputs = serde_json::json!({
        "node_id": "internal-coherence-enforced",
        "name": "Internal Coherence Enforced",
        "description": "Adopted ops layer enforces internal invariants.",
        "pole": "Yang",
        "level": "Axiom",
        "invariant": true
    });

    let result = dispatch_op("update_ontology_node", &inputs, &mut ctx).expect("dispatch ok");
    assert_eq!(result.witness.op_id, "update_ontology_node");
    assert_eq!(result.witness.witness_shape, "node_upsert");
    result.witness.verify().expect("witness verifies");
    assert_eq!(result.effects[0].kind, "Insert");
    assert_eq!(result.effects[0].entity, "internal-coherence-enforced");

    // core.ncl rewrite contains the new node (canonical layout per ADR-032).
    let core_path = tmp.path().join(".ontoref/ontology/core.ncl");
    assert!(core_path.exists());
    let body = std::fs::read_to_string(&core_path).expect("read core");
    assert!(body.contains("internal-coherence-enforced"));
    assert!(body.contains("Adopted ops layer enforces internal invariants"));
}

#[test]
fn update_ontology_node_updates_existing_node() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let mut ctx = ctx_for(tmp.path().to_path_buf()).with_core(sample_core_snapshot());

    let inputs = serde_json::json!({
        "node_id": "protocol-not-runtime",
        "description": "Updated description for the protocol invariant."
    });
    let result = dispatch_op("update_ontology_node", &inputs, &mut ctx).expect("dispatch ok");
    assert_eq!(result.effects[0].kind, "Update");
    let body =
        std::fs::read_to_string(tmp.path().join(".ontoref/ontology/core.ncl")).expect("read core");
    assert!(body.contains("Updated description for the protocol invariant"));
}

#[test]
fn update_ontology_node_rejects_missing_id() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let mut ctx = ctx_for(tmp.path().to_path_buf()).with_core(sample_core_snapshot());

    let inputs = serde_json::json!({ "name": "X" });
    let err = dispatch_op("update_ontology_node", &inputs, &mut ctx)
        .expect_err("missing node_id must reject");
    assert!(matches!(err, OpError::PreconditionLoad { .. }));
}

// ── manage_backlog_item ───────────────────────────────────────────────────

#[test]
fn manage_backlog_item_add_creates_entry() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let mut ctx = ctx_for(tmp.path().to_path_buf());

    let inputs = serde_json::json!({
        "action": "add",
        "title": "Wire MCP surface to dispatch",
        "kind": "Feature",
        "priority": "High",
        "status": "Open"
    });
    let result = dispatch_op("manage_backlog_item", &inputs, &mut ctx).expect("dispatch ok");
    assert_eq!(result.witness.op_id, "manage_backlog_item");
    assert_eq!(result.witness.witness_shape, "backlog_item");
    result.witness.verify().expect("witness verifies");
    assert_eq!(result.effects[0].kind, "Insert");

    let body = std::fs::read_to_string(tmp.path().join(".ontoref/reflection/backlog.ncl"))
        .expect("read backlog");
    assert!(body.contains("Wire MCP surface to dispatch"));
    assert!(body.contains("Feature"));
}

#[test]
fn manage_backlog_item_update_mutates_existing() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let mut ctx = ctx_for(tmp.path().to_path_buf());

    let add_inputs = serde_json::json!({
        "action": "add",
        "id": "bl-9001",
        "title": "Initial item",
        "status": "Open"
    });
    dispatch_op("manage_backlog_item", &add_inputs, &mut ctx).expect("add ok");

    let update_inputs = serde_json::json!({
        "action": "update",
        "id": "bl-9001",
        "status": "Closed"
    });
    let result = dispatch_op("manage_backlog_item", &update_inputs, &mut ctx).expect("update ok");
    assert_eq!(result.effects[0].kind, "Update");
    let body = std::fs::read_to_string(tmp.path().join(".ontoref/reflection/backlog.ncl"))
        .expect("read backlog");
    assert!(body.contains("Closed"));
}

#[test]
fn manage_backlog_item_rejects_add_with_no_title() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let mut ctx = ctx_for(tmp.path().to_path_buf());

    let inputs = serde_json::json!({ "action": "add", "kind": "Bug" });
    let err = dispatch_op("manage_backlog_item", &inputs, &mut ctx)
        .expect_err("add with no title must reject");
    assert!(matches!(err, OpError::ValidatorRejected { .. }));
}

// ── replay determinism ────────────────────────────────────────────────────

#[test]
fn replay_determinism_across_three_ops() {
    // Execute the three ops in sequence against a fresh context and
    // record (a) the resulting state_root and (b) the sequence of
    // EntityAssert ops captured via the commit_layer's apply hook.
    //
    // For Phase 1, we can't externally observe each apply'd op without
    // wiring an oplog, but we CAN demonstrate determinism by running
    // the same sequence twice and confirming the state roots match
    // byte-for-byte.
    let run = || -> [u8; 32] {
        let tmp = tempfile::tempdir().expect("tempdir");
        let mut ctx = ctx_for(tmp.path().to_path_buf()).with_core(sample_core_snapshot());

        // 1. propose_adr
        let _ = dispatch_op(
            "propose_adr",
            &serde_json::json!({
                "id": "adr-201",
                "slug": "replay-test",
                "title": "Replay Test",
                "decision": "Run the same ops twice and compare roots.",
                "ondaod_evaluation": {
                    "engaged_tensions": ["ontology-vs-reflection"],
                    "synthesis_state": "Realized",
                    "motion_direction": "Static"
                }
            }),
            &mut ctx,
        )
        .expect("propose_adr");

        // 2. update_ontology_node (insert)
        let _ = dispatch_op(
            "update_ontology_node",
            &serde_json::json!({
                "node_id": "replay-marker",
                "name": "Replay Marker",
                "description": "Inserted by the replay test.",
                "pole": "Yang",
                "level": "Practice"
            }),
            &mut ctx,
        )
        .expect("update_ontology_node");

        // 3. manage_backlog_item
        let _ = dispatch_op(
            "manage_backlog_item",
            &serde_json::json!({
                "action": "add",
                "id": "bl-201",
                "title": "Replay backlog entry",
                "priority": "Medium",
                "status": "Open"
            }),
            &mut ctx,
        )
        .expect("manage_backlog_item");

        ctx.commit_layer.root()
    };

    let root_a = run();
    let root_b = run();
    assert_eq!(
        root_a, root_b,
        "running the same op sequence twice must produce the same state_root — replay determinism"
    );
    // And the state root MUST differ from a fresh empty commit layer.
    let empty_root = BinaryMerkle::new().root();
    assert_ne!(root_a, empty_root, "ops must have advanced the state_root");
}
