#!/bin/sh
# autopkgtest for rust-nostr-memory.
#
# The upstream test target cannot be compiled: the #[cfg(test)] module in
# src/lib.rs imports `nostr_database_test_suite`, a workspace-only crate that
# upstream does not publish (its Cargo.toml says `publish = false`, and the
# published nostr-memory tarball ships an empty [dev-dependencies] section),
# and it also uses `Deref` without importing it.  `cargo-auto-test` is
# therefore useless here, so instead of testing the crate in place we build a
# small downstream crate against the installed librust-nostr-memory-dev and
# check that the in-memory database really honours the NostrDatabase
# contract.  This also proves that the -dev package is usable by consumers.
#
# Modelled on debian/tests/run-tests of the blight package.

set -eu

work=$(mktemp -d "${AUTOPKGTEST_TMP:-${TMPDIR:-/tmp}}/nostr-memory-smoke.XXXXXX")
trap 'rm -rf -- "$work"' EXIT HUP INT TERM

mkdir -p "$work/consumer/src" "$work/cargo-home"

# Build offline against the Debian-managed crate registry.
cat > "$work/cargo-home/config.toml" <<'CONFIG'
[source.crates-io]
replace-with = "debian"

[source.debian]
directory = "/usr/share/cargo/registry"
CONFIG

cat > "$work/consumer/Cargo.toml" <<'CONFIG'
[package]
name = "nostr-memory-smoke"
version = "0.0.0"
edition = "2021"
publish = false

[dependencies]
nostr = { version = "0.45", features = ["std", "rand", "os-rng"] }
nostr-database = "0.45"
nostr-memory = "0.45"
tokio = { version = "1", features = ["macros", "rt"] }
CONFIG

cat > "$work/consumer/src/main.rs" <<'SOURCE'
// Smoke test for the nostr-memory in-memory database.

use core::num::NonZeroUsize;

use nostr::prelude::*;
use nostr_database::prelude::*;
use nostr_memory::MemoryDatabase;

fn text_note(keys: &Keys, content: &str) -> Event {
    EventBuilder::new(Kind::TextNote, content)
        .finalize(keys)
        .expect("failed to finalize text note")
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let keys = Keys::generate();
    let note = text_note(&keys, "hello from the Debian autopkgtest");

    // A fresh database is empty.
    let db = MemoryDatabase::unbounded();
    assert_eq!(
        db.count(Filter::new()).await.unwrap(),
        0,
        "new database is not empty"
    );

    // save_event() + check_id() + count() + query() + event_by_id()
    db.save_event(&note).await.expect("failed to save event");
    assert_eq!(
        db.check_id(&note.id).await.unwrap(),
        DatabaseEventStatus::Saved,
        "a saved event is not reported as saved"
    );
    assert_eq!(
        db.count(Filter::new()).await.unwrap(),
        1,
        "count() != 1 after one save"
    );
    assert_eq!(
        db.query(Filter::new()).await.unwrap().len(),
        1,
        "query() returned wrong size"
    );
    assert_eq!(
        db.event_by_id(&note.id)
            .await
            .unwrap()
            .map(|event| event.id),
        Some(note.id),
        "event_by_id() did not return the saved event"
    );

    // The backend must not store the same event twice.
    db.save_event(&note).await.expect("failed to re-save event");
    assert_eq!(
        db.count(Filter::new()).await.unwrap(),
        1,
        "duplicate event was stored twice"
    );

    // delete() removes the matching events outright, without NIP-09
    // bookkeeping, so the id becomes unknown again.
    db.delete(Filter::new().id(note.id))
        .await
        .expect("failed to delete event");
    assert_eq!(
        db.check_id(&note.id).await.unwrap(),
        DatabaseEventStatus::NotExistent,
        "event is still known after being deleted"
    );
    assert_eq!(
        db.count(Filter::new()).await.unwrap(),
        0,
        "count() != 0 after delete"
    );

    // NIP-09: a kind 5 deletion request removes the referenced events and
    // makes check_id() report them as Deleted.
    let db = MemoryDatabase::unbounded();
    let first = text_note(&keys, "to be deleted 1");
    let second = text_note(&keys, "to be deleted 2");
    for event in [&first, &second] {
        db.save_event(event).await.expect("failed to save event");
    }
    assert_eq!(
        db.query(Filter::new().kind(Kind::TextNote))
            .await
            .unwrap()
            .len(),
        2,
        "both text notes should have been stored"
    );

    let deletion_request = EventDeletionRequest::new()
        .id(first.id)
        .id(second.id)
        .finalize(&keys)
        .expect("failed to finalize deletion request");
    db.save_event(&deletion_request)
        .await
        .expect("failed to save deletion request");

    for event in [&first, &second] {
        assert_eq!(
            db.check_id(&event.id).await.unwrap(),
            DatabaseEventStatus::Deleted,
            "NIP-09 deletion request was not processed"
        );
    }
    assert_eq!(
        db.query(Filter::new().kind(Kind::TextNote))
            .await
            .unwrap()
            .len(),
        0,
        "text notes survived the NIP-09 deletion request"
    );

    // NIP-01: replaceable events (kind 0) of the same author collapse into one.
    let db = MemoryDatabase::unbounded();
    for name in ["v1", "v2"] {
        let metadata = EventBuilder::new(Kind::Metadata, format!("{{\"name\":\"{name}\"}}"))
            .finalize(&keys)
            .expect("failed to finalize metadata");
        db.save_event(&metadata)
            .await
            .expect("failed to save metadata");
    }
    assert_eq!(
        db.count(Filter::new()).await.unwrap(),
        1,
        "replaceable event was not replaced"
    );

    // A bounded database must evict the oldest events (btreecap).
    let db = MemoryDatabase::bounded(NonZeroUsize::new(1).unwrap());
    for content in ["one", "two", "three"] {
        db.save_event(&text_note(&keys, content))
            .await
            .expect("failed to save event");
    }
    assert_eq!(
        db.count(Filter::new()).await.unwrap(),
        1,
        "bounded database grew beyond its limit"
    );

    // wipe() empties the database.
    db.wipe().await.expect("failed to wipe database");
    assert_eq!(
        db.count(Filter::new()).await.unwrap(),
        0,
        "count() != 0 after wipe"
    );

    println!("nostr-memory smoke test: all checks passed");
}
SOURCE

export CARGO_HOME="$work/cargo-home" CARGO_TARGET_DIR="$work/target"
export CARGO_INCREMENTAL=0 CARGO_PROFILE_DEV_DEBUG=0

/usr/bin/cargo run --offline --quiet --manifest-path "$work/consumer/Cargo.toml"
