//! Diagnostic tests to investigate the "one behind" issue.
//!
//! The "one behind" bug manifests as: new sedimentrees don't appear until
//! the NEXT one is created. These tests inject messages through channels
//! to observe timing and storage behavior.
//!
//! Tests run for both `Sendable` and `Local` future kinds to ensure
//! behavior is consistent across native and Wasm-like environments.

use std::{collections::BTreeSet, sync::Arc};

use core::time::Duration;
use future_form::{Local, Sendable};
use sedimentree_core::{
    blob::{Blob, BlobMeta},
    id::SedimentreeId,
    loose_commit::{LooseCommit, id::CommitId},
};
use subduction_core::{
    connection::{
        message::SyncMessage,
        test_utils::{ChannelMockConnection, InstantTimeout, TokioSpawn, test_signer},
    },
    peer::id::PeerId,
    policy::open::OpenPolicy,
    remote_heads::RemoteHeads,
    storage::memory::MemoryStorage,
    subduction::builder::SubductionBuilder,
};
use subduction_crypto::signed::Signed;
use testresult::TestResult;

#[allow(clippy::indexing_slicing)]
async fn make_test_commit_with_data(
    id: &SedimentreeId,
    data: &[u8],
) -> (Signed<LooseCommit>, Blob) {
    let blob = Blob::new(data.to_vec());
    let blob_meta = BlobMeta::new(&blob);
    let head = CommitId::new({
        let mut bytes = [0u8; 32];
        let len = data.len().min(32);
        bytes[..len].copy_from_slice(&data[..len]);
        bytes
    });
    let commit = LooseCommit::new(*id, head, BTreeSet::new(), blob_meta);
    let verified = Signed::seal::<Sendable, _>(&test_signer(), commit).await;
    (verified.into_signed(), blob)
}

#[tokio::test]
async fn test_sendable_single_commit() -> TestResult {
    let (subduction, _handler, listener_fut, actor_fut) =
        SubductionBuilder::<_, _, _, _, _, 256>::new()
            .signer(test_signer())
            .storage(MemoryStorage::new(), Arc::new(OpenPolicy))
            .spawner(TokioSpawn)
            .timer(InstantTimeout)
            .build::<Sendable, ChannelMockConnection<SyncMessage>>();

    let (conn, handle) = ChannelMockConnection::new_with_handle(PeerId::new([1u8; 32]));
    subduction.add_connection(conn.authenticated()).await?;

    let actor_task = tokio::spawn(actor_fut);
    let listener_task = tokio::spawn(listener_fut);
    tokio::time::sleep(Duration::from_millis(10)).await;

    let sedimentree_id = SedimentreeId::new([42u8; 32]);
    let (commit, blob) = make_test_commit_with_data(&sedimentree_id, b"test commit").await;

    handle
        .inbound_tx
        .send(SyncMessage::LooseCommit {
            id: sedimentree_id,
            commit,
            blob,
            sender_heads: RemoteHeads::default(),
        })
        .await?;

    tokio::time::sleep(Duration::from_millis(50)).await;

    let ids = subduction.sedimentree_ids().await;
    assert!(
        ids.contains(&sedimentree_id),
        "[SENDABLE] Sedimentree should be visible. Found: {ids:?}"
    );
    assert_eq!(
        subduction
            .get_commits(sedimentree_id)
            .await
            .map(|c| c.len()),
        Some(1)
    );

    actor_task.abort();
    listener_task.abort();
    Ok(())
}

#[tokio::test]
async fn test_sendable_multiple_sequential() -> TestResult {
    let (subduction, _handler, listener_fut, actor_fut) =
        SubductionBuilder::<_, _, _, _, _, 256>::new()
            .signer(test_signer())
            .storage(MemoryStorage::new(), Arc::new(OpenPolicy))
            .spawner(TokioSpawn)
            .timer(InstantTimeout)
            .build::<Sendable, ChannelMockConnection<SyncMessage>>();

    let (conn, handle) = ChannelMockConnection::new_with_handle(PeerId::new([1u8; 32]));
    subduction.add_connection(conn.authenticated()).await?;

    let actor_task = tokio::spawn(actor_fut);
    let listener_task = tokio::spawn(listener_fut);
    tokio::time::sleep(Duration::from_millis(10)).await;

    for i in 0..3u8 {
        let sedimentree_id = SedimentreeId::new([i; 32]);
        let (commit, blob) =
            make_test_commit_with_data(&sedimentree_id, format!("commit {i}").as_bytes()).await;

        handle
            .inbound_tx
            .send(SyncMessage::LooseCommit {
                id: sedimentree_id,
                commit,
                blob,
                sender_heads: RemoteHeads::default(),
            })
            .await?;

        tokio::time::sleep(Duration::from_millis(20)).await;

        let ids = subduction.sedimentree_ids().await;
        assert_eq!(
            ids.len(),
            (i + 1) as usize,
            "[SENDABLE] After commit {i}: expected {} sedimentrees, found {}. ONE BEHIND BUG!",
            i + 1,
            ids.len()
        );
        assert!(ids.contains(&sedimentree_id));
    }

    actor_task.abort();
    listener_task.abort();
    Ok(())
}

#[tokio::test]
async fn test_sendable_same_sedimentree() -> TestResult {
    let (subduction, _handler, listener_fut, actor_fut) =
        SubductionBuilder::<_, _, _, _, _, 256>::new()
            .signer(test_signer())
            .storage(MemoryStorage::new(), Arc::new(OpenPolicy))
            .spawner(TokioSpawn)
            .timer(InstantTimeout)
            .build::<Sendable, ChannelMockConnection<SyncMessage>>();

    let (conn, handle) = ChannelMockConnection::new_with_handle(PeerId::new([1u8; 32]));
    subduction.add_connection(conn.authenticated()).await?;

    let actor_task = tokio::spawn(actor_fut);
    let listener_task = tokio::spawn(listener_fut);
    tokio::time::sleep(Duration::from_millis(10)).await;

    let sedimentree_id = SedimentreeId::new([99u8; 32]);

    for i in 0..3usize {
        let (commit, blob) =
            make_test_commit_with_data(&sedimentree_id, format!("commit {i}").as_bytes()).await;

        handle
            .inbound_tx
            .send(SyncMessage::LooseCommit {
                id: sedimentree_id,
                commit,
                blob,
                sender_heads: RemoteHeads::default(),
            })
            .await?;

        tokio::time::sleep(Duration::from_millis(20)).await;

        let count = subduction
            .get_commits(sedimentree_id)
            .await
            .map_or(0, |c| c.len());
        assert_eq!(
            count,
            i + 1,
            "[SENDABLE] After commit {i}, expected {} commits but found {}. DELAYED!",
            i + 1,
            count
        );
    }

    actor_task.abort();
    listener_task.abort();
    Ok(())
}

#[tokio::test]
async fn test_local_single_commit() -> TestResult {
    tokio::task::LocalSet::new()
        .run_until(async {
            let (subduction, _handler, listener_fut, actor_fut) =
                SubductionBuilder::<_, _, _, _, _, 256>::new()
                    .signer(test_signer())
                    .storage(MemoryStorage::new(), Arc::new(OpenPolicy))
                    .spawner(TokioSpawn)
                    .timer(InstantTimeout)
                    .build::<Local, ChannelMockConnection<SyncMessage>>();

            let (conn, handle) = ChannelMockConnection::new_with_handle(PeerId::new([1u8; 32]));
            subduction.add_connection(conn.authenticated()).await?;

            let actor_task = tokio::task::spawn_local(actor_fut);
            let listener_task = tokio::task::spawn_local(listener_fut);
            tokio::time::sleep(Duration::from_millis(10)).await;

            let sedimentree_id = SedimentreeId::new([42u8; 32]);
            let (commit, blob) = make_test_commit_with_data(&sedimentree_id, b"test commit").await;

            handle
                .inbound_tx
                .send(SyncMessage::LooseCommit {
                    id: sedimentree_id,
                    commit,
                    blob,
                    sender_heads: RemoteHeads::default(),
                })
                .await?;

            tokio::time::sleep(Duration::from_millis(50)).await;

            let ids = subduction.sedimentree_ids().await;
            assert!(
                ids.contains(&sedimentree_id),
                "[LOCAL] Sedimentree should be visible. Found: {ids:?}"
            );
            assert_eq!(
                subduction
                    .get_commits(sedimentree_id)
                    .await
                    .map(|c| c.len()),
                Some(1)
            );

            actor_task.abort();
            listener_task.abort();
            Ok::<_, Box<dyn std::error::Error>>(())
        })
        .await?;
    Ok(())
}

#[tokio::test]
async fn test_local_multiple_sequential() -> TestResult {
    tokio::task::LocalSet::new()
        .run_until(async {
            let (subduction, _handler, listener_fut, actor_fut) =
                SubductionBuilder::<_, _, _, _, _, 256>::new()
                    .signer(test_signer())
                    .storage(MemoryStorage::new(), Arc::new(OpenPolicy))
                    .spawner(TokioSpawn)
                    .timer(InstantTimeout)
                    .build::<Local, ChannelMockConnection<SyncMessage>>();

            let (conn, handle) = ChannelMockConnection::new_with_handle(PeerId::new([1u8; 32]));
            subduction.add_connection(conn.authenticated()).await?;

            let actor_task = tokio::task::spawn_local(actor_fut);
            let listener_task = tokio::task::spawn_local(listener_fut);
            tokio::time::sleep(Duration::from_millis(10)).await;

            for i in 0..3u8 {
                let sedimentree_id = SedimentreeId::new([i; 32]);
                let (commit, blob) =
                    make_test_commit_with_data(&sedimentree_id, format!("commit {i}").as_bytes())
                        .await;

                handle
                    .inbound_tx
                    .send(SyncMessage::LooseCommit {
                        id: sedimentree_id,
                        commit,
                        blob,
                        sender_heads: RemoteHeads::default(),
                    })
                    .await?;

                tokio::time::sleep(Duration::from_millis(20)).await;

                let ids = subduction.sedimentree_ids().await;
                assert_eq!(
                    ids.len(),
                    (i + 1) as usize,
                    "[LOCAL] After commit {i}: expected {} sedimentrees, found {}. ONE BEHIND BUG!",
                    i + 1,
                    ids.len()
                );
                assert!(ids.contains(&sedimentree_id));
            }

            actor_task.abort();
            listener_task.abort();
            Ok::<_, Box<dyn std::error::Error>>(())
        })
        .await?;
    Ok(())
}

#[tokio::test]
async fn test_local_same_sedimentree() -> TestResult {
    tokio::task::LocalSet::new()
        .run_until(async {
            let (subduction, _handler, listener_fut, actor_fut) =
                SubductionBuilder::<_, _, _, _, _, 256>::new()
                    .signer(test_signer())
                    .storage(MemoryStorage::new(), Arc::new(OpenPolicy))
                    .spawner(TokioSpawn)
                    .timer(InstantTimeout)
                    .build::<Local, ChannelMockConnection<SyncMessage>>();

            let (conn, handle) = ChannelMockConnection::new_with_handle(PeerId::new([1u8; 32]));
            subduction.add_connection(conn.authenticated()).await?;

            let actor_task = tokio::task::spawn_local(actor_fut);
            let listener_task = tokio::task::spawn_local(listener_fut);
            tokio::time::sleep(Duration::from_millis(10)).await;

            let sedimentree_id = SedimentreeId::new([99u8; 32]);

            for i in 0..3usize {
                let (commit, blob) =
                    make_test_commit_with_data(&sedimentree_id, format!("commit {i}").as_bytes())
                        .await;

                handle
                    .inbound_tx
                    .send(SyncMessage::LooseCommit {
                        id: sedimentree_id,
                        commit,
                        blob,
                        sender_heads: RemoteHeads::default(),
                    })
                    .await?;

                tokio::time::sleep(Duration::from_millis(20)).await;

                let count = subduction
                    .get_commits(sedimentree_id)
                    .await
                    .map_or(0, |c| c.len());
                assert_eq!(
                    count,
                    i + 1,
                    "[LOCAL] After commit {i}, expected {} commits but found {}. DELAYED!",
                    i + 1,
                    count
                );
            }

            actor_task.abort();
            listener_task.abort();
            Ok::<_, Box<dyn std::error::Error>>(())
        })
        .await?;
    Ok(())
}
