mostro 0.17.5

Lightning Network peer-to-peer nostr platform
use crate::app::context::AppContext;
use crate::db::is_user_present;
use crate::util::send_dm;
use mostro_core::prelude::*;
use nostr_sdk::prelude::*;

/// Handles a `last_trade_index` action request from a user.
///
/// This function retrieves the requester's last trade index from the database and sends it back
/// via a direct message. The last trade index represents the most recent trade identifier
/// associated with the user's identity key.
///
/// # Parameters
///
/// * `ctx` - Application context containing the database pool and other dependencies
/// * `msg` - The incoming message containing the request details, including the request ID
/// * `event` - The unwrapped gift event containing the sender's public key
/// * `my_keys` - The daemon's keys used for signing and sending the response
///
/// # Returns
///
/// * `Ok(())` - If the last trade index was successfully retrieved and sent
/// * `Err(MostroCantDo(CantDoReason::NotFound))` - If the requester is not found in the database
/// * `Err(MostroCantDo(CantDoReason::InvalidTradeIndex))` - If the user's last_trade_index is 0
///   (which should never occur as it's related to identity key)
/// * `Err(MostroError::MostroInternalErr(ServiceError::MessageSerializationError))` - If message
///   serialization fails
///
/// # Behavior
///
/// The function performs the following steps:
/// 1. Extracts the requester's public key from the unwrapped gift event
/// 2. Retrieves the request ID from the message
/// 3. Queries the database to verify the user exists and get their last trade index
/// 4. Validates that the last trade index is not zero (invalid state)
/// 5. Constructs a response message with `Action::LastTradeIndex` containing the trade index
/// 6. Serializes the message to JSON and sends it as a direct message to the requester
///
/// # Errors
///
/// Errors are logged but not propagated for message sending failures. All other errors are returned
/// to the caller.
pub async fn last_trade_index(
    ctx: &AppContext,
    msg: Message,
    event: &UnwrappedMessage,
    my_keys: &Keys,
) -> Result<(), MostroError> {
    let pool = ctx.pool();
    // Get requester pubkey (sender of the message)
    let requester_pubkey = event.identity.to_string();
    // Get trade key from the event rumor
    let trade_key = event.sender;

    // Get request id
    let request_id = msg.get_inner_message_kind().request_id;

    // Check if user is present in the database
    // If not, return a not found error
    let user = match is_user_present(pool, requester_pubkey).await {
        Ok(user) => user,
        Err(_) => {
            return Err(MostroCantDo(CantDoReason::NotFound));
        }
    };

    // Zero should never be returned by the database - because it's related to identity key
    if user.last_trade_index == 0 {
        return Err(MostroCantDo(CantDoReason::InvalidTradeIndex));
    }

    // Build response message embedding the last_trade_index in the trade_index field
    let kind = MessageKind::new(
        None,
        request_id,
        Some(user.last_trade_index),
        Action::LastTradeIndex,
        None,
    );
    let last_trade_index_message = Message::Restore(kind);
    let message_json = last_trade_index_message
        .as_json()
        .map_err(|_| MostroError::MostroInternalErr(ServiceError::MessageSerializationError))?;

    // Print the last trade index message
    tracing::info!(
        "User with pubkey: {} requested last trade index",
        user.pubkey
    );
    tracing::info!("Last trade index: {}", user.last_trade_index);

    // Send message back to the requester
    if let Err(e) = send_dm(trade_key, my_keys, &message_json, None).await {
        tracing::error!("Error sending message with last trade index: {:?}", e);
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use nostr_sdk::{Keys, Timestamp};
    use sqlx::sqlite::SqlitePoolOptions;
    use sqlx::SqlitePool;

    // Helper function to create test keys
    fn create_test_keys() -> Keys {
        Keys::generate()
    }

    // Helper function to create UnwrappedMessage for testing. `sender_keys`
    // stands in for the long-lived identity key; the trade key is generated
    // separately so the two fields stay distinguishable.
    fn create_test_unwrapped_message(sender_keys: &Keys) -> UnwrappedMessage {
        let trade = create_test_keys();
        println!(
            "Creating UnwrappedMessage with identity pubkey: {}",
            sender_keys.public_key()
        );

        UnwrappedMessage {
            message: Message::Restore(MessageKind::new(
                None,
                Some(1),
                None,
                Action::LastTradeIndex,
                None,
            )),
            signature: None,
            sender: trade.public_key(),
            identity: sender_keys.public_key(),
            created_at: Timestamp::now(),
        }
    }

    // Helper function to set up in-memory test database with user table
    async fn setup_test_db() -> SqlitePool {
        let pool = SqlitePoolOptions::new()
            .max_connections(1)
            .connect(":memory:")
            .await
            .unwrap();

        // Create the users table schema matching application expectations
        sqlx::query(
            r#"
            CREATE TABLE IF NOT EXISTS users (
                pubkey CHAR(64) PRIMARY KEY NOT NULL,
                is_admin INTEGER NOT NULL DEFAULT 0,
                admin_password CHAR(64),
                is_solver INTEGER NOT NULL DEFAULT 0,
                is_banned INTEGER NOT NULL DEFAULT 0,
                category INTEGER NOT NULL DEFAULT 0,
                last_trade_index INTEGER NOT NULL DEFAULT 0,
                total_reviews INTEGER NOT NULL DEFAULT 0,
                total_rating REAL NOT NULL DEFAULT 0.0,
                last_rating INTEGER NOT NULL DEFAULT 0,
                max_rating INTEGER NOT NULL DEFAULT 0,
                min_rating INTEGER NOT NULL DEFAULT 0,
                created_at INTEGER NOT NULL DEFAULT 0
            )
            "#,
        )
        .execute(&pool)
        .await
        .unwrap();

        pool
    }

    // Helper function to insert a test user
    async fn insert_test_user(pool: &SqlitePool, pubkey: &str, last_trade_index: i64) {
        sqlx::query(
            r#"
            INSERT INTO users (pubkey, last_trade_index)
            VALUES (?, ?)
            "#,
        )
        .bind(pubkey)
        .bind(last_trade_index)
        .execute(pool)
        .await
        .unwrap();
    }

    #[tokio::test]
    async fn test_last_trade_index_user_not_found() {
        // Setup: Create empty database (no users)
        let pool = setup_test_db().await;
        use crate::app::context::test_utils::{test_settings, TestContextBuilder};
        let ctx = TestContextBuilder::new()
            .with_pool(std::sync::Arc::new(pool.clone()))
            .with_settings(test_settings())
            .build();
        let sender_keys = create_test_keys();

        // Create test event for non-existent user
        let event = create_test_unwrapped_message(&sender_keys);

        // Create test message kind
        let kind = MessageKind::new(None, Some(1234567890), None, Action::LastTradeIndex, None);

        // Execute function
        let result = last_trade_index(&ctx, Message::Restore(kind), &event, &sender_keys).await;

        // Should fail because user doesn't exist
        assert!(
            result.is_err(),
            "Should return error when user doesn't exist"
        );

        // Verify it's the right kind of error (user not found)
        match result {
            Err(MostroError::MostroCantDo(CantDoReason::NotFound)) => {
                // Expected error type for user not found
            }
            Err(e) => {
                panic!("Expected MostroInternalErr(DbAccessError), got: {:?}", e);
            }
            Ok(_) => {
                panic!("Should have failed when user doesn't exist");
            }
        }
    }

    #[tokio::test]
    async fn test_last_trade_index_correct_value() {
        // Setup: Create database with multiple users with different trade indexes
        let pool = setup_test_db().await;

        // User 1 with trade_index = 10
        let user1_keys = create_test_keys();
        let user1_pubkey = user1_keys.public_key().to_string();
        insert_test_user(&pool, &user1_pubkey, 10).await;

        // User 2 with trade_index = 99
        let user2_keys = create_test_keys();
        let user2_pubkey = user2_keys.public_key().to_string();
        insert_test_user(&pool, &user2_pubkey, 99).await;

        // Verify that is_user_present returns correct last_trade_index for each user
        let user1 = is_user_present(&pool, user1_pubkey.clone()).await.unwrap();
        assert_eq!(
            user1.last_trade_index, 10,
            "User 1 should have last_trade_index = 10"
        );

        let user2 = is_user_present(&pool, user2_pubkey.clone()).await.unwrap();
        assert_eq!(
            user2.last_trade_index, 99,
            "User 2 should have last_trade_index = 99"
        );

        // Test message construction with correct trade_index
        let message = MessageKind::new(
            None,
            None,
            Some(user1.last_trade_index),
            Action::LastTradeIndex,
            None,
        );

        // Verify the message contains the correct trade_index
        assert_eq!(
            message.trade_index(),
            10i64,
            "Message should contain correct trade_index"
        );
    }

    #[tokio::test]
    async fn test_last_trade_index_message_serialization() {
        // Test that MessageKind can be serialized to JSON successfully
        let pool = setup_test_db().await;
        let sender_keys = create_test_keys();
        let pubkey = sender_keys.public_key().to_string();
        let trade_index = 42i64;

        insert_test_user(&pool, &pubkey, trade_index).await;

        let user = is_user_present(&pool, pubkey).await.unwrap();

        // Test message serialization
        let message = MessageKind::new(
            None,
            None,
            Some(user.last_trade_index),
            Action::LastTradeIndex,
            None,
        );

        let json_result = message.as_json();
        assert!(json_result.is_ok(), "Message serialization should succeed");

        // Verify JSON structure
        let json = json_result.unwrap();
        assert!(
            json.contains("last-trade-index"),
            "JSON should contain action field"
        );
        assert!(
            json.contains("trade_index"),
            "JSON should contain trade_index field"
        );
    }
}