//! Cross-project witness fetch — Hard constraint of ADR-028.
//!
//! Sets up two foreign ontologies (X and Y) on disk, registers them in a
//! local ontology registry, then demonstrates that
//! [`fetch_cell_witness`] returns ONLY the witness + state root for the
//! requested cell. The caller never gains access to the foreign
//! ontology's full state — the `CellWitnessResponse` type structurally
//! precludes it.
//!
//! The test also verifies the witness round-trip: the returned witness
//! verifies under the returned root, but does NOT verify under a
//! different ontology's root (negative case).

use ed25519_dalek::SigningKey;
use ontoref_commit::BinaryMerkle;
use ontoref_ontology_content::{
    compute_ontology_id, fetch_cell_witness, verify_cell, CellQuery, Error, OntologyRegistry,
    OntologyType, RegistryEntry,
};
use ontoref_oplog::OpLog;
use ontoref_types::{
    operation::{OpBody, OpPayload, Operation},
    AttrId, EntityId, Hlc, PublicKey, Value,
};

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

fn assert_op(entity: &str, attr: &str, value: Value, ts_logical: u64, seed: u8) -> Operation {
    let key = signing_key(seed);
    let body = OpBody {
        actor: PublicKey::from_signing_key(&key),
        parents: vec![],
        payload: OpPayload::EntityAssert {
            attrs: vec![(AttrId::new(attr), value)],
            entity: EntityId::new(entity),
            valid_from: None,
        },
        timestamp: Hlc::new(ts_logical, 0),
    };
    Operation::sign(body, &key)
}

/// Append `ops` to a fresh oplog at `path` and return the resulting
/// [`OntologyId`] computed from the oplog's heads.
fn build_ontology(
    path: &std::path::Path,
    ops: &[Operation],
) -> ontoref_ontology_content::OntologyId {
    let log = OpLog::open(path).expect("open oplog");
    for op in ops {
        log.append(op).expect("append op");
    }
    let heads = log.heads().expect("heads ok");
    compute_ontology_id(&heads)
}

#[test]
fn fetch_cell_witness_returns_only_witness_and_root() {
    let tmp = tempfile::tempdir().expect("tempdir");

    // ── Foreign ontology X — carries an "x-entity" cell. ────────────────────
    let x_oplog = tmp.path().join("x.oplog");
    let x_ops = vec![
        assert_op("x-entity", "x-attr", Value::Str("x-value".into()), 1, 0xAA),
        assert_op("x-entity-other", "x-attr-other", Value::Int(42), 2, 0xAA),
    ];
    let x_id = build_ontology(&x_oplog, &x_ops);

    // ── Foreign ontology Y — carries a "y-entity" cell. ─────────────────────
    let y_oplog = tmp.path().join("y.oplog");
    let y_ops = vec![assert_op(
        "y-entity",
        "y-attr",
        Value::Str("y-value".into()),
        1,
        0xBB,
    )];
    let y_id = build_ontology(&y_oplog, &y_ops);
    assert_ne!(x_id, y_id, "different ontologies must have different ids");

    // ── Project A's local registry — knows X and Y. ─────────────────────────
    let mut registry = OntologyRegistry::empty();
    registry.register(RegistryEntry {
        ontology_id: x_id,
        oplog_path: x_oplog.clone(),
        classification: OntologyType::Type2aFederable,
        verified_state_root_hex: None,
    });
    registry.register(RegistryEntry {
        ontology_id: y_id,
        oplog_path: y_oplog,
        classification: OntologyType::Type3ProjectSpecific,
        verified_state_root_hex: None,
    });

    // ── Project A fetches a witness for X's x-entity/x-attr cell. ──────────
    let query = CellQuery::new("x-entity", "x-attr");
    let response = fetch_cell_witness(&registry, &x_id, &query).expect("fetch ok");

    // Witness verifies against the returned state root.
    assert!(
        verify_cell(&response.witness, &response.state_root),
        "witness must verify against its own state root"
    );

    // The witness contains the canonical encoding of "x-value".
    let expected =
        ontoref_types::canonical::encode(&Value::Str("x-value".into())).expect("encode value");
    assert_eq!(response.witness.value, expected);

    // ── Negative: the witness must NOT verify under Y's state root. ────────
    // Rebuild Y's commit layer and compute its root WITHOUT going
    // through fetch_ontology — this proves the negative case is real.
    let y_log = OpLog::open(x_oplog.with_file_name("y.oplog")).expect("open y oplog");
    let y_state_ops = y_log.all_ops_in_hlc_order().expect("y ops");
    let mut y_commit = BinaryMerkle::new();
    for op in &y_state_ops {
        y_commit.apply(op);
    }
    let y_root = y_commit.root();
    assert_ne!(
        response.state_root, y_root,
        "X and Y must have different roots"
    );
    assert!(
        !verify_cell(&response.witness, &y_root),
        "witness from X must not verify under Y's state root"
    );

    // ── Hard ADR-028 surface check: the response type is narrow. ───────────
    // CellWitnessResponse carries exactly { witness, state_root } — no
    // Vec<Operation>, no full commit layer, no cell map. The size of the
    // struct is bounded by the witness's Merkle path depth.
    let response_size = std::mem::size_of_val(&response);
    let full_oplog_size_bound = x_ops.len() * 1024; // pessimistic per-op size
    assert!(
        response_size < full_oplog_size_bound,
        "response must be bounded — full oplog leakage would exceed {full_oplog_size_bound} \
         bytes; got {response_size}"
    );

    // Fetching a cell that does not exist in X returns CellMissing,
    // NOT a leak of Y's content.
    let unknown_query = CellQuery::new("y-entity", "y-attr");
    let err = fetch_cell_witness(&registry, &x_id, &unknown_query)
        .expect_err("y-entity must not exist in X");
    assert!(matches!(err, Error::CellMissing(_)));
}

#[test]
fn fetch_unknown_ontology_id_returns_error() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let oplog = tmp.path().join("x.oplog");
    let _ = build_ontology(&oplog, &[assert_op("e", "a", Value::Int(1), 1, 0xCC)]);

    let registry = OntologyRegistry::empty();
    let unknown_id = ontoref_ontology_content::OntologyId([0xFFu8; 32]);
    let err = fetch_cell_witness(&registry, &unknown_id, &CellQuery::new("e", "a"))
        .expect_err("unknown id must error");
    assert!(matches!(err, Error::UnknownOntology(_)));
}

#[test]
fn ontology_id_is_deterministic_across_independent_rebuilds() {
    let tmp = tempfile::tempdir().expect("tempdir");
    let path_a = tmp.path().join("a.oplog");
    let path_b = tmp.path().join("b.oplog");
    let ops = vec![
        assert_op("e1", "a1", Value::Str("v1".into()), 1, 0xDD),
        assert_op("e2", "a2", Value::Int(7), 2, 0xDD),
    ];
    let id_a = build_ontology(&path_a, &ops);
    let id_b = build_ontology(&path_b, &ops);
    assert_eq!(
        id_a, id_b,
        "two oplogs with identical op sequence must produce the same OntologyId"
    );
}
