use std::collections::HashSet;

use ed25519_dalek::SigningKey;
use ontoref_oplog::{Error, OpLog};
use ontoref_types::{
    operation::{OpBody, OpPayload, Operation},
    Hlc, OpId, PublicKey,
};

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

fn make_op(parents: Vec<OpId>, timestamp: Hlc, label: &str) -> Operation {
    let key = signing_key();
    let body = OpBody {
        actor: PublicKey::from_signing_key(&key),
        parents,
        payload: OpPayload::Noop {
            label: label.into(),
        },
        timestamp,
    };
    Operation::sign(body, &key)
}

#[test]
fn replay_yields_same_heads_for_linear_chain() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("oplog.redb");

    let original_heads: Vec<OpId> = {
        let log = OpLog::open(&path).unwrap();
        let mut prev_id = None;
        let mut prev_hlc = Hlc::genesis();
        for i in 0..100 {
            prev_hlc = Hlc::tick(prev_hlc, 0);
            let parents = prev_id.map(|id| vec![id]).unwrap_or_default();
            let op = make_op(parents, prev_hlc, &format!("op-{i}"));
            prev_id = Some(log.append(&op).unwrap());
        }
        assert_eq!(log.len().unwrap(), 100);
        log.heads().unwrap()
    };

    // Drop the OpLog (closes the redb handle), reopen the file, re-read heads.
    let reopened_heads = OpLog::open(&path).unwrap().heads().unwrap();
    assert_eq!(original_heads, reopened_heads);
    assert_eq!(
        original_heads.len(),
        1,
        "a linear chain has exactly one head"
    );
}

#[test]
fn replay_yields_same_heads_for_branching_dag() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("oplog.redb");

    let original_heads: HashSet<OpId> = {
        let log = OpLog::open(&path).unwrap();
        let mut clock = Hlc::genesis();

        // Root op.
        clock = Hlc::tick(clock, 0);
        let root = log.append(&make_op(vec![], clock, "root")).unwrap();

        // Two children sharing the same root.
        clock = Hlc::tick(clock, 0);
        let left = log.append(&make_op(vec![root], clock, "left")).unwrap();
        clock = Hlc::tick(clock, 0);
        let right = log.append(&make_op(vec![root], clock, "right")).unwrap();

        // Merge: a child of both left and right.
        clock = Hlc::tick(clock, 0);
        let merged = log
            .append(&make_op(vec![left, right], clock, "merged"))
            .unwrap();

        // Extra branch from the root that stays a head.
        clock = Hlc::tick(clock, 0);
        let stray = log.append(&make_op(vec![root], clock, "stray")).unwrap();

        // Expected heads: {merged, stray}.
        let expected: HashSet<OpId> = [merged, stray].into_iter().collect();
        assert_eq!(
            log.heads().unwrap().into_iter().collect::<HashSet<_>>(),
            expected
        );
        expected
    };

    let reopened: HashSet<OpId> = OpLog::open(&path)
        .unwrap()
        .heads()
        .unwrap()
        .into_iter()
        .collect();
    assert_eq!(reopened, original_heads);
}

#[test]
fn parents_validation_rejects_unknown_parent() {
    let dir = tempfile::tempdir().unwrap();
    let log = OpLog::open(dir.path().join("oplog.redb")).unwrap();

    let bogus = OpId([0xAA; 32]);
    let op = make_op(vec![bogus], Hlc::new(1, 0), "orphan");

    match log.append(&op) {
        Err(Error::UnknownParent(parent)) if parent == bogus => {}
        other => panic!("expected UnknownParent({bogus:?}), got {other:?}"),
    }
    assert_eq!(log.len().unwrap(), 0, "rejected op must not be stored");
}

#[test]
fn parents_validation_accepts_known_parent() {
    let dir = tempfile::tempdir().unwrap();
    let log = OpLog::open(dir.path().join("oplog.redb")).unwrap();

    let root = log
        .append(&make_op(vec![], Hlc::new(1, 0), "root"))
        .unwrap();
    let child = log
        .append(&make_op(vec![root], Hlc::new(2, 0), "child"))
        .unwrap();

    assert!(log.get(&root).unwrap().is_some());
    assert!(log.get(&child).unwrap().is_some());
    assert_eq!(log.heads().unwrap(), vec![child]);
}

#[test]
fn append_is_idempotent() {
    let dir = tempfile::tempdir().unwrap();
    let log = OpLog::open(dir.path().join("oplog.redb")).unwrap();

    let op = make_op(vec![], Hlc::new(1, 0), "same");
    let id1 = log.append(&op).unwrap();
    let id2 = log.append(&op).unwrap();
    assert_eq!(id1, id2);
    assert_eq!(log.len().unwrap(), 1);
}

#[test]
fn append_rejects_bad_signature() {
    let dir = tempfile::tempdir().unwrap();
    let log = OpLog::open(dir.path().join("oplog.redb")).unwrap();

    let mut op = make_op(vec![], Hlc::new(1, 0), "tampered");
    // Flip a bit in the signature so it no longer verifies.
    op.signature.0[0] ^= 0x01;

    match log.append(&op) {
        Err(Error::BadSignature(_)) => {}
        other => panic!("expected BadSignature, got {other:?}"),
    }
}

#[test]
fn append_rejects_non_monotonic_timestamp() {
    let dir = tempfile::tempdir().unwrap();
    let log = OpLog::open(dir.path().join("oplog.redb")).unwrap();

    let root = log
        .append(&make_op(vec![], Hlc::new(5, 0), "root"))
        .unwrap();
    // Child with same timestamp as the parent — must be rejected.
    let bad_child = make_op(vec![root], Hlc::new(5, 0), "child-same-time");
    match log.append(&bad_child) {
        Err(Error::NonMonotonicTimestamp(_)) => {}
        other => panic!("expected NonMonotonicTimestamp, got {other:?}"),
    }
}
