//! E2E coverage for the six ops added in O6:
//! `evaluate_ondaod`, `transition_adr`, `update_ontology_edge`,
//! `sync_apply`, `start_dev_session`, `close_dev_session`. Plus the
//! catalog-coherence test that compares every inventory entry against
//! its `catalog/operations/<id>.ncl` declaration.

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

use ed25519_dalek::SigningKey;
use ontoref_ops::{dispatch_op, OpContext, OperationEntry};

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

fn ctx() -> OpContext {
    OpContext::new_in_memory("developer", "", signing_key())
}

// ── evaluate_ondaod ───────────────────────────────────────────────────────

#[test]
fn evaluate_ondaod_emits_meta_witness() {
    let mut c = ctx();
    let inputs = serde_json::json!({
        "topic": "Should we accept ADR-024?",
        "engaged_tensions": ["ontology-vs-reflection", "formalization-vs-adoption"],
        "synthesis_state": "Realized",
        "motion_direction": "TowardYang",
    });
    let r = dispatch_op("evaluate_ondaod", &inputs, &mut c).expect("dispatch ok");
    assert_eq!(r.witness.op_id, "evaluate_ondaod");
    assert_eq!(r.witness.witness_shape, "ondaod_evaluation");
    r.witness.verify().expect("witness verifies");
    assert_eq!(r.effects[0].kind, "Insert");
}

// ── transition_adr ────────────────────────────────────────────────────────

#[test]
fn transition_adr_accept_requires_ondaod() {
    let mut c = ctx();
    let inputs = serde_json::json!({
        "id": "adr-024",
        "new_status": "Accepted",
        "ondaod_evaluation": { "engaged_tensions": [], "synthesis_state": "" }
    });
    let err = dispatch_op("transition_adr", &inputs, &mut c)
        .expect_err("accept without ondaod must reject");
    assert!(matches!(
        err,
        ontoref_ops::OpError::ValidatorRejected { .. }
    ));
}

#[test]
fn transition_adr_accept_with_ondaod_succeeds() {
    let mut c = ctx();
    let inputs = serde_json::json!({
        "id": "adr-024",
        "new_status": "Accepted",
        "ondaod_evaluation": {
            "engaged_tensions": ["ontology-vs-reflection"],
            "synthesis_state": "Realized",
        }
    });
    let r = dispatch_op("transition_adr", &inputs, &mut c).expect("dispatch ok");
    assert_eq!(r.witness.witness_shape, "adr_transition");
    r.witness.verify().expect("witness verifies");
}

#[test]
fn transition_adr_unsupported_target_rejects() {
    let mut c = ctx();
    let inputs = serde_json::json!({
        "id": "adr-024",
        "new_status": "Withdrawn",
        "ondaod_evaluation": { "engaged_tensions": ["x"], "synthesis_state": "x" }
    });
    let err =
        dispatch_op("transition_adr", &inputs, &mut c).expect_err("unsupported target must reject");
    assert!(matches!(
        err,
        ontoref_ops::OpError::ValidatorRejected { .. }
    ));
}

// ── update_ontology_edge ──────────────────────────────────────────────────

#[test]
fn update_ontology_edge_inserts_basic_edge() {
    let mut c = ctx();
    c.core = Some(serde_json::json!({ "nodes": [], "edges": [] }));
    let inputs = serde_json::json!({
        "from": "self-describing",
        "to": "dag-formalized",
        "kind": "ManifestsIn",
    });
    let r = dispatch_op("update_ontology_edge", &inputs, &mut c).expect("dispatch ok");
    assert_eq!(r.witness.witness_shape, "edge_upsert");
    r.witness.verify().expect("witness verifies");
}

#[test]
fn update_ontology_edge_rejects_self_loop() {
    let mut c = ctx();
    c.core = Some(serde_json::json!({ "nodes": [], "edges": [] }));
    let inputs = serde_json::json!({
        "from": "x",
        "to": "x",
        "kind": "DependsOn",
    });
    let err =
        dispatch_op("update_ontology_edge", &inputs, &mut c).expect_err("self-loop must reject");
    assert!(matches!(
        err,
        ontoref_ops::OpError::ValidatorRejected { .. }
    ));
}

#[test]
fn update_ontology_edge_rejects_1_step_cycle() {
    let mut c = ctx();
    c.core = Some(serde_json::json!({
        "nodes": [],
        "edges": [
            { "from": "b", "to": "a", "kind": "DependsOn" }
        ]
    }));
    let inputs = serde_json::json!({
        "from": "a",
        "to": "b",
        "kind": "DependsOn",
    });
    let err =
        dispatch_op("update_ontology_edge", &inputs, &mut c).expect_err("1-step cycle must reject");
    assert!(matches!(
        err,
        ontoref_ops::OpError::ValidatorRejected { .. }
    ));
}

// ── sync_apply ────────────────────────────────────────────────────────────

#[test]
fn sync_apply_records_batch() {
    let mut c = ctx();
    let inputs = serde_json::json!({
        "diffs": [
            { "op_id": "move_fsm_state", "inputs": { "dimension": "d", "new_state": "x" } },
            { "op_id": "manage_backlog_item", "inputs": { "action": "add", "title": "y" } }
        ],
        "summary": "two diffs from save reconciler",
    });
    let r = dispatch_op("sync_apply", &inputs, &mut c).expect("dispatch ok");
    assert_eq!(r.witness.witness_shape, "sync_batch");
    r.witness.verify().expect("witness verifies");
}

// ── start_dev_session / close_dev_session ─────────────────────────────────

#[test]
fn start_and_close_dev_session_round_trip() {
    let mut c = ctx();

    let start = dispatch_op(
        "start_dev_session",
        &serde_json::json!({ "scope": "ontoref-ops", "target": "src/ops/" }),
        &mut c,
    )
    .expect("start ok");
    assert_eq!(start.witness.witness_shape, "dev_session_start");
    let session_entity = start.effects[0].entity.clone();
    assert!(session_entity.starts_with("dev_session:"));
    start.witness.verify().expect("witness verifies");

    let close = dispatch_op(
        "close_dev_session",
        &serde_json::json!({
            "session_id": session_entity,
            "drift_summary": "no drift observed"
        }),
        &mut c,
    )
    .expect("close ok");
    assert_eq!(close.witness.witness_shape, "dev_session_close");
    close.witness.verify().expect("witness verifies");
}

// ── catalog ↔ inventory coherence ─────────────────────────────────────────

fn workspace_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .and_then(Path::parent)
        .expect("workspace root resolvable")
        .to_path_buf()
}

fn nickel_export(path: &Path) -> serde_json::Value {
    let output = Command::new("nickel")
        .arg("export")
        .arg("--format")
        .arg("json")
        .arg(path)
        .output()
        .expect("nickel binary required on PATH");
    assert!(
        output.status.success(),
        "nickel export {} failed: {}",
        path.display(),
        String::from_utf8_lossy(&output.stderr)
    );
    serde_json::from_slice(&output.stdout).expect("nickel output is JSON")
}

#[test]
fn every_pilot_op_has_a_catalog_declaration() {
    let root = workspace_root();
    let pilot_ids = [
        "evaluate_ondaod",
        "propose_adr",
        "transition_adr",
        "update_ontology_node",
        "update_ontology_edge",
        "move_fsm_state",
        "manage_backlog_item",
        "sync_apply",
        "start_dev_session",
        "close_dev_session",
    ];
    for id in pilot_ids {
        let ncl = ontoref_ontology::layout::resolve_section(
            &root,
            &format!("catalog/operations/{id}.ncl"),
        );
        assert!(
            ncl.exists(),
            "missing catalog declaration for op '{id}' at {}",
            ncl.display()
        );

        let entry = inventory::iter::<OperationEntry>()
            .find(|e| e.id == id)
            .unwrap_or_else(|| panic!("op '{id}' not registered via #[onto_operation]"));

        let declared = nickel_export(&ncl);

        assert_eq!(
            declared.get("id").and_then(|v| v.as_str()),
            Some(entry.id),
            "id mismatch for op '{id}'"
        );
        assert_eq!(
            declared.get("description").and_then(|v| v.as_str()),
            Some(entry.description),
            "description mismatch for op '{id}'"
        );
        // validation_sla in NCL exports as a tag-shape string (e.g. "Synchronous") for
        // the Synchronous and Background tags.
        let declared_sla_tag = declared.get("validation_sla").and_then(|v| v.as_str());
        assert_eq!(
            declared_sla_tag,
            Some(entry.validation_sla),
            "validation_sla mismatch for op '{id}'"
        );
        let declared_witness_shape = declared
            .get("witness_shape")
            .and_then(|w| w.get("payload_kind"))
            .and_then(|v| v.as_str());
        assert_eq!(
            declared_witness_shape,
            Some(entry.witness_shape),
            "witness_shape.payload_kind mismatch for op '{id}'"
        );
    }
}
