use std::{sync::Arc, time::Duration};

use anyhow::Result;
use assign::assign;
use matrix_sdk::{
    event_handler::Ctx,
    room::Room,
    ruma::{
        api::client::room::create_room::v3::Request as CreateRoomRequest,
        events::room::member::{MembershipState, StrippedRoomMemberEvent},
    },
    Client, RoomMemberships, RoomState,
};
use tokio::sync::Notify;

use crate::helpers::get_client_for_user;

#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_repeated_join_leave() -> Result<()> {
    let peter = get_client_for_user("peter".to_owned(), true).await?;
    // FIXME: Run once with memory, once with SQLite
    let karl = get_client_for_user("karl".to_owned(), false).await?;
    let karl_id = karl.user_id().expect("karl has a userid!").to_owned();

    // Create a room and invite karl.
    let invite = vec![karl_id.clone()];
    let request = assign!(CreateRoomRequest::new(), {
        invite,
        is_direct: true,
    });

    // Sync after 1 second to so that create_room receives the event it is waiting
    // for.
    let peter_clone = peter.clone();
    tokio::spawn(async move {
        tokio::time::sleep(Duration::from_secs(1)).await;
        peter_clone.sync_once(Default::default()).await
    });

    let created_room = peter.create_room(request).await?;
    let room_id = created_room.room_id();

    // Sync karl once to ensure he got the invite.
    karl.sync_once(Default::default()).await?;

    // Continuously sync karl from now on.
    let karl_clone = karl.clone();
    let join_handle = tokio::spawn(async move {
        karl_clone.sync(Default::default()).await.unwrap();
    });
    let invite_signal = Arc::new(Notify::new());
    karl.add_event_handler_context(invite_signal.clone());
    karl.add_event_handler(signal_on_invite);

    for i in 0..3 {
        println!("Iteration {i}");

        // Test that karl has the expected state in its client.
        assert!(karl.get_invited_room(room_id).is_some());
        assert!(karl.get_joined_room(room_id).is_none());
        assert!(karl.get_left_room(room_id).is_none());

        let room = karl.get_room(room_id).expect("karl has the room");
        let membership = room.get_member_no_sync(&karl_id).await?.expect("karl was invited");
        assert_eq!(*membership.membership(), MembershipState::Invite);

        // Join the room
        println!("Joining..");
        let room =
            karl.get_invited_room(room_id).expect("karl has the room").accept_invitation().await?;
        println!("Done");
        let membership = room.get_member(&karl_id).await?.expect("karl joined");
        assert_eq!(*membership.membership(), MembershipState::Join);

        assert!(karl.get_invited_room(room_id).is_none());
        assert!(karl.get_joined_room(room_id).is_some());
        assert!(karl.get_left_room(room_id).is_none());

        // Syncs can overwrite the internal state. If the sync lags behind because we
        // change so often so fast, we can get errors in the asserts here. So we have to
        // wait here a bit till the sync happened.
        room.sync_up().await;

        // Leave the room
        println!("Leaving..");
        let room = room.leave().await?;
        println!("Done");
        let membership = room.get_member(&karl_id).await?.expect("karl left");
        assert_eq!(*membership.membership(), MembershipState::Leave);

        assert!(karl.get_invited_room(room_id).is_none());
        assert!(karl.get_joined_room(room_id).is_none());
        assert!(karl.get_left_room(room_id).is_some());

        // Invite karl again and wait for karl to receive the invite.
        println!("Inviting..");
        let room = peter.get_joined_room(room_id).expect("peter created the room!");
        room.invite_user_by_id(&karl_id).await?;
        println!("Waiting to receive invite..");
        invite_signal.notified().await;
    }

    // Stop the sync.
    join_handle.abort();

    // Now check the underlying state store that it also has the correct information
    // (for when the client restarts).
    let invited = karl.store().get_user_ids(room_id, RoomMemberships::INVITE).await?;
    assert_eq!(invited.len(), 1);
    assert_eq!(invited[0], karl_id);

    let joined = karl.store().get_user_ids(room_id, RoomMemberships::JOIN).await?;
    assert!(!joined.contains(&karl_id));

    let event = karl
        .store()
        .get_member_event(room_id, &karl_id)
        .await?
        .expect("member event should exist")
        .deserialize()
        .unwrap();
    assert_eq!(*event.membership(), MembershipState::Invite);

    // Yay, test succeeded
    Ok(())
}

async fn signal_on_invite(
    event: StrippedRoomMemberEvent,
    room: Room,
    client: Client,
    sender: Ctx<Arc<Notify>>,
) {
    let own_id = client.user_id().expect("client is logged in");
    if event.sender == own_id {
        return;
    }

    if room.state() != RoomState::Invited {
        return;
    }

    if event.content.membership != MembershipState::Invite {
        return;
    }

    let invited = &event.state_key;
    if invited != own_id {
        return;
    }

    // Send signal that we received an invite.
    sender.notify_one();
}
