//! End-to-end coverage for the `move_fsm_state` operation (O3).
//!
//! Demonstrates the full pipeline: precondition slice load, pre-validator
//! decision, op body mutates the FSM state + appends an EntityAssert to
//! the commit layer, render writes `state.ncl` byte-stable, post-
//! validator confirms invariants, witness signed and externally
//! verifiable from `(state_root, witness)` alone.

use ed25519_dalek::SigningKey;
use ontoref_ontology::types::{
    Dimension, DimensionState, Horizon, StateConfig, TensionLevel, Transition,
};
use ontoref_ops::validation::Verdict;
use ontoref_ops::{dispatch_op, OpContext, OpError};

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

fn sample_state() -> StateConfig {
    StateConfig {
        dimensions: vec![Dimension {
            id: "adoption".to_owned(),
            name: "Adoption".to_owned(),
            description: "Sample FSM dimension for the O3 piloto.".to_owned(),
            current_state: "a".to_owned(),
            desired_state: "c".to_owned(),
            horizon: Horizon::Weeks,
            states: vec![
                DimensionState {
                    id: "a".to_owned(),
                    name: "A".to_owned(),
                    description: "initial".to_owned(),
                    tension: TensionLevel::Low,
                },
                DimensionState {
                    id: "b".to_owned(),
                    name: "B".to_owned(),
                    description: "middle".to_owned(),
                    tension: TensionLevel::Medium,
                },
                DimensionState {
                    id: "c".to_owned(),
                    name: "C".to_owned(),
                    description: "final".to_owned(),
                    tension: TensionLevel::Low,
                },
            ],
            transitions: vec![
                Transition {
                    from: "a".to_owned(),
                    to: "b".to_owned(),
                    condition: "ready".to_owned(),
                    catalyst: "trigger".to_owned(),
                    blocker: "none".to_owned(),
                    horizon: Horizon::Weeks,
                },
                Transition {
                    from: "b".to_owned(),
                    to: "c".to_owned(),
                    condition: "ready".to_owned(),
                    catalyst: "trigger".to_owned(),
                    blocker: "none".to_owned(),
                    horizon: Horizon::Weeks,
                },
            ],
            coupled_with: vec![],
        }],
    }
}

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

#[test]
fn happy_path_full_pipeline() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let project_root = tmp.path().to_path_buf();
    std::fs::create_dir_all(ontoref_ontology::layout::resolve_section(
        &project_root,
        "ontology",
    ))
    .expect("mkdir .ontology");

    let mut ctx = build_ctx(project_root.clone());
    let initial_root = ctx.commit_layer.root();

    let inputs = serde_json::json!({ "dimension": "adoption", "new_state": "b" });
    let result = dispatch_op("move_fsm_state", &inputs, &mut ctx).expect("dispatch ok");

    // ── witness ─────────────────────────────────────────────────────────────
    assert_eq!(result.witness.op_id, "move_fsm_state");
    assert_eq!(result.witness.witness_shape, "state_transition");
    result
        .witness
        .verify()
        .expect("witness must verify under the actor's public key");

    // ── substrate ───────────────────────────────────────────────────────────
    let new_root = ctx.commit_layer.root();
    assert_eq!(
        new_root, result.witness.state_root,
        "witness binds to current state_root"
    );
    assert_ne!(
        new_root, initial_root,
        "state_root must advance after the op"
    );

    // ── typed state ────────────────────────────────────────────────────────
    let dim = ctx.state.as_ref().unwrap().dimensions.first().unwrap();
    assert_eq!(dim.current_state, "b");

    // ── render ─────────────────────────────────────────────────────────────
    let rendered_path = project_root.join(".ontology/state.ncl");
    assert!(rendered_path.exists(), "state.ncl must be rendered");
    let rendered = std::fs::read_to_string(&rendered_path).expect("read state.ncl");
    assert!(
        rendered.contains("current_state = \"b\""),
        "rendered state.ncl must carry the mutated current_state; got:\n{rendered}"
    );

    // ── effects ────────────────────────────────────────────────────────────
    assert_eq!(result.effects.len(), 1);
    let e = &result.effects[0];
    assert_eq!(e.kind, "Update");
    assert_eq!(e.entity, "adoption");
    assert_eq!(e.attrs, vec!["current_state".to_owned()]);
    assert_eq!(e.dimensions, vec!["adoption".to_owned()]);

    // ── metrics ────────────────────────────────────────────────────────────
    // Every stage instrumented even when no validators/effects fire —
    // the witness stage in particular MUST be observable since we sign.
    // (us values may be 0 on extremely fast machines; the field's
    // presence is the contract.)
    let _ = result.metrics.precondition_load_us;
    let _ = result.metrics.pre_validate_us;
    let _ = result.metrics.exec_us;
    let _ = result.metrics.apply_us;
    let _ = result.metrics.render_us;
    let _ = result.metrics.post_validate_us;
    let _ = result.metrics.witness_us;
}

#[test]
fn rejects_invalid_transition() {
    let tmp = tempfile::tempdir().expect("tempdir");
    std::fs::create_dir_all(tmp.path().join(".ontoref/ontology")).expect("mkdir");
    let mut ctx = build_ctx(tmp.path().to_path_buf());

    let inputs = serde_json::json!({ "dimension": "adoption", "new_state": "nonexistent" });
    let err = dispatch_op("move_fsm_state", &inputs, &mut ctx)
        .expect_err("invalid transition must reject");

    match err {
        OpError::ValidatorRejected { verdict, op_id } => {
            assert_eq!(op_id, "move_fsm_state");
            match verdict {
                Verdict::Reject { validator_id, .. } => {
                    assert_eq!(validator_id, "fsm_transition_allowed");
                }
                other => panic!("expected Verdict::Reject, got {other:?}"),
            }
        }
        other => panic!("expected ValidatorRejected, got {other:?}"),
    }
}

#[test]
fn rejects_unknown_dimension() {
    let tmp = tempfile::tempdir().expect("tempdir");
    std::fs::create_dir_all(tmp.path().join(".ontoref/ontology")).expect("mkdir");
    let mut ctx = build_ctx(tmp.path().to_path_buf());

    let inputs = serde_json::json!({ "dimension": "no-such-dim", "new_state": "b" });
    let err = dispatch_op("move_fsm_state", &inputs, &mut ctx)
        .expect_err("unknown dimension must reject");
    assert!(matches!(err, OpError::PreconditionLoad { .. }));
}

#[test]
fn external_verifier_only_needs_state_root_and_witness() {
    use ontoref_ops::PipelineWitness;

    let tmp = tempfile::tempdir().expect("tempdir");
    std::fs::create_dir_all(tmp.path().join(".ontoref/ontology")).expect("mkdir");
    let mut ctx = build_ctx(tmp.path().to_path_buf());

    let inputs = serde_json::json!({ "dimension": "adoption", "new_state": "b" });
    let result = dispatch_op("move_fsm_state", &inputs, &mut ctx).expect("dispatch ok");

    // The external verifier receives ONLY the witness. The full state,
    // commit_layer, and op inputs are NOT in its possession.
    let witness: PipelineWitness = result.witness.clone();
    drop(ctx);

    // Reproducing the canonical payload from the witness fields and
    // running ed25519 verify is sufficient to prove the op fired with
    // a state_root the actor signed.
    witness
        .verify()
        .expect("external verification with only the witness must succeed");

    // Negative: tampering with the witness's state_root invalidates it.
    let mut tampered = witness.clone();
    tampered.state_root = [0xFFu8; 32];
    assert!(tampered.verify().is_err());

    // Negative: tampering with the witness's op_id invalidates it.
    let mut renamed = witness.clone();
    renamed.op_id = "wrong_op".to_owned();
    assert!(renamed.verify().is_err());
}
