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

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

fn assert_op(entity: &str, attrs: Vec<(&str, Value)>, tx: Hlc) -> Operation {
    let key = signing_key();
    let body = OpBody {
        actor: PublicKey::from_signing_key(&key),
        parents: vec![],
        payload: OpPayload::EntityAssert {
            attrs: attrs
                .into_iter()
                .map(|(name, value)| (AttrId::new(name), value))
                .collect(),
            entity: EntityId::new(entity),
            valid_from: None,
        },
        timestamp: tx,
    };
    Operation::sign(body, &key)
}

fn noop_op(label: &str, tx: Hlc) -> Operation {
    let key = signing_key();
    let body = OpBody {
        actor: PublicKey::from_signing_key(&key),
        parents: vec![],
        payload: OpPayload::Noop {
            label: label.into(),
        },
        timestamp: tx,
    };
    Operation::sign(body, &key)
}

#[test]
fn apply_pipeline_returns_complete_receipt() {
    let dir = tempfile::tempdir().unwrap();
    let mut substrate = Substrate::open(dir.path()).unwrap();

    let op = assert_op(
        "alice",
        vec![
            ("name", Value::Str("Alice".into())),
            ("age", Value::Int(30)),
        ],
        Hlc::new(1, 0),
    );
    let receipt = substrate.apply(&op).unwrap();

    assert_eq!(receipt.op_id, op.id());
    assert_eq!(receipt.state_root, substrate.state_root());
    assert_eq!(receipt.witnesses.len(), 2);
    for witness in &receipt.witnesses {
        assert!(
            verify_witness(witness, &receipt.state_root),
            "every receipt witness must verify under its declared state root"
        );
    }
}

#[test]
fn apply_pipeline_persists_blob_with_canonical_bytes() {
    let dir = tempfile::tempdir().unwrap();
    let mut substrate = Substrate::open(dir.path()).unwrap();

    let op = assert_op("e", vec![("v", Value::Int(7))], Hlc::new(1, 0));
    let receipt = substrate.apply(&op).unwrap();

    let bytes = substrate.blobs().get(&receipt.blob_id).unwrap();
    let decoded: Operation = ontoref_types::canonical::decode(&bytes).unwrap();
    assert_eq!(decoded, op);
}

#[test]
fn apply_pipeline_persists_op_to_oplog() {
    let dir = tempfile::tempdir().unwrap();
    let mut substrate = Substrate::open(dir.path()).unwrap();

    let op = assert_op("e", vec![("v", Value::Int(7))], Hlc::new(1, 0));
    let receipt = substrate.apply(&op).unwrap();

    let from_log = substrate.oplog().get(&receipt.op_id).unwrap();
    assert_eq!(from_log, Some(op));
}

#[test]
fn apply_pipeline_materialises_triples_visible_to_query() {
    let dir = tempfile::tempdir().unwrap();
    let mut substrate = Substrate::open(dir.path()).unwrap();

    substrate
        .apply(&assert_op(
            "alice",
            vec![("name", Value::Str("Alice".into()))],
            Hlc::new(1, 0),
        ))
        .unwrap();

    let attrs = substrate
        .query()
        .triples()
        .entity_attrs(&EntityId::new("alice"), Hlc::new(10, 0), Hlc::new(10, 0))
        .unwrap();
    assert_eq!(attrs.len(), 1);
    assert_eq!(attrs[0].0, AttrId::new("name"));
    assert_eq!(attrs[0].1, Value::Str("Alice".into()));
}

#[test]
fn apply_pipeline_noop_returns_no_witnesses() {
    let dir = tempfile::tempdir().unwrap();
    let mut substrate = Substrate::open(dir.path()).unwrap();

    let receipt = substrate.apply(&noop_op("warmup", Hlc::new(1, 0))).unwrap();
    assert!(receipt.witnesses.is_empty());
    assert_eq!(receipt.state_root, empty_root());
}

#[test]
fn apply_pipeline_state_root_advances_with_each_assert() {
    let dir = tempfile::tempdir().unwrap();
    let mut substrate = Substrate::open(dir.path()).unwrap();

    let r1 = substrate
        .apply(&assert_op("e", vec![("v", Value::Int(1))], Hlc::new(1, 0)))
        .unwrap();
    let r2 = substrate
        .apply(&assert_op("e", vec![("v", Value::Int(2))], Hlc::new(2, 0)))
        .unwrap();
    let r3 = substrate
        .apply(&assert_op("f", vec![("v", Value::Int(2))], Hlc::new(3, 0)))
        .unwrap();

    assert_ne!(r1.state_root, r2.state_root);
    assert_ne!(r2.state_root, r3.state_root);
}

#[test]
fn apply_pipeline_subscriber_invoked_once_per_apply() {
    use std::sync::atomic::{AtomicUsize, Ordering};
    use std::sync::Arc;

    let dir = tempfile::tempdir().unwrap();
    let mut substrate = Substrate::open(dir.path()).unwrap();

    let counter = Arc::new(AtomicUsize::new(0));
    {
        let counter = Arc::clone(&counter);
        substrate.subscribe(move |_op| {
            counter.fetch_add(1, Ordering::SeqCst);
        });
    }

    for i in 0..4u64 {
        substrate
            .apply(&assert_op(
                "e",
                vec![("v", Value::Int(i as i64))],
                Hlc::new(i + 1, 0),
            ))
            .unwrap();
    }
    assert_eq!(counter.load(Ordering::SeqCst), 4);
}

#[test]
fn apply_pipeline_reopen_restores_state_root() {
    // Apply ops, capture state root, drop the handle, reopen the same path,
    // verify the state root is byte-identical. This is the minimum
    // crash-recovery property: in-memory commit state survives a clean
    // close by being re-derived from the durable op log.
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().to_path_buf();

    let pre_root = {
        let mut substrate = Substrate::open(&path).unwrap();
        substrate
            .apply(&assert_op(
                "alice",
                vec![("name", Value::Str("Alice".into()))],
                Hlc::new(1, 0),
            ))
            .unwrap();
        substrate
            .apply(&assert_op(
                "bob",
                vec![("name", Value::Str("Bob".into()))],
                Hlc::new(2, 0),
            ))
            .unwrap();
        substrate
            .apply(&assert_op(
                "alice",
                vec![("name", Value::Str("Alicia".into()))],
                Hlc::new(3, 0),
            ))
            .unwrap();
        substrate.state_root()
    };

    let reopened = Substrate::open(&path).unwrap();
    assert_eq!(reopened.state_root(), pre_root);
}

#[test]
fn apply_pipeline_witness_for_assert_carries_latest_value() {
    let dir = tempfile::tempdir().unwrap();
    let mut substrate = Substrate::open(dir.path()).unwrap();

    substrate
        .apply(&assert_op("e", vec![("v", Value::Int(1))], Hlc::new(1, 0)))
        .unwrap();
    let receipt = substrate
        .apply(&assert_op("e", vec![("v", Value::Int(2))], Hlc::new(2, 0)))
        .unwrap();

    assert_eq!(receipt.witnesses.len(), 1);
    let witness = &receipt.witnesses[0];
    let expected_value = ontoref_types::canonical::encode(&Value::Int(2)).unwrap();
    assert_eq!(witness.value, expected_value);
    assert!(verify_witness(witness, &receipt.state_root));
}
