//! Round20 focused raw coverage for channel bus and presentation paths.
//!
//! Uses debug-only seams plus in-memory web-channel events. No external
//! channel credentials, providers, or network services are required.

use std::time::Duration;

use openhuman_core::core::event_bus::{DomainEvent, EventHandler};
use openhuman_core::openhuman::agent::memory_loader::MemoryCitation;
use openhuman_core::openhuman::channels::bus::ChannelInboundSubscriber;
use openhuman_core::openhuman::channels::providers::presentation::test_support as presentation_test_support;
use openhuman_core::openhuman::channels::providers::web::{
    subscribe_web_channel_events, test_support as web_test_support,
};
use serde_json::json;
use tokio::time::timeout;

#[tokio::test]
async fn presentation_segments_text_and_delivers_single_bubble_with_citations() {
    assert_eq!(
        presentation_test_support::segment_for_delivery_for_test("  Hello there.  "),
        vec!["Hello there.".to_string()]
    );
    assert_eq!(
        presentation_test_support::segment_for_delivery_for_test(
            "Here is code:\n\n```rust\nfn main() {}\n```\n\nKeep it together."
        )
        .len(),
        1
    );
    assert!(presentation_test_support::is_structured_content_for_test(
        "# Heading\n- First\n- Second"
    ));
    assert_eq!(presentation_test_support::segment_delay_for_test(""), 500);

    let citation = MemoryCitation {
        id: "mem-1".to_string(),
        key: "project".to_string(),
        namespace: Some("test".to_string()),
        score: Some(0.91),
        timestamp: "2026-05-29T00:00:00Z".to_string(),
        snippet: "OpenHuman channel presentation citation.".to_string(),
    };

    let mut rx = subscribe_web_channel_events();
    presentation_test_support::deliver_response_for_test(
        "round20-client",
        "round20-thread",
        "round20-single",
        "Short final answer.",
        "",
        std::slice::from_ref(&citation),
    )
    .await;

    // `subscribe_web_channel_events` is a process-global bus shared with the
    // other presentation tests, which run concurrently and emit their own
    // `chat_segment`/`chat_done` events. Filter to this delivery's request id so
    // a sibling's segment event can't be mistaken for our single bubble.
    let event = timeout(Duration::from_secs(5), async {
        loop {
            let event = rx.recv().await.expect("single bubble event");
            if event.request_id == "round20-single" {
                break event;
            }
        }
    })
    .await
    .expect("single bubble event timeout");
    assert_eq!(event.event, "chat_done");
    assert_eq!(event.full_response.as_deref(), Some("Short final answer."));
    assert_eq!(event.reaction_emoji, None);
    assert!(event
        .citations
        .expect("citations")
        .to_string()
        .contains("mem-1"));
}

#[tokio::test]
async fn presentation_delivers_segment_events_then_deduping_done_event() {
    let response = [
        "First paragraph has enough natural language content to stand alone as a separate chat bubble.",
        "Second paragraph also contains enough prose to exercise segmented delivery and delay calculation.",
        "Third paragraph ensures the final chat_done event carries the complete response for deduplication.",
    ]
    .join("\n\n");
    let segments = presentation_test_support::segment_for_delivery_for_test(&response);
    assert!(segments.len() >= 2, "expected segmented delivery");

    let mut rx = subscribe_web_channel_events();
    presentation_test_support::deliver_response_for_test(
        "round20-client",
        "round20-thread",
        "round20-segmented",
        &response,
        "",
        &[],
    )
    .await;

    let mut seen_segments = 0_u32;
    let final_event = timeout(Duration::from_secs(10), async {
        loop {
            let event = rx.recv().await.expect("presentation event");
            if event.request_id != "round20-segmented" {
                continue;
            }
            match event.event.as_str() {
                "chat_segment" => {
                    assert_eq!(event.segment_total, Some(segments.len() as u32));
                    assert_eq!(event.segment_index, Some(seen_segments));
                    assert!(event.full_response.as_deref().unwrap_or("").len() >= 40);
                    seen_segments += 1;
                }
                "chat_done" => break event,
                other => panic!("unexpected presentation event {other}"),
            }
        }
    })
    .await
    .expect("segmented delivery timeout");

    assert_eq!(seen_segments, segments.len() as u32);
    assert_eq!(final_event.segment_total, Some(segments.len() as u32));
    assert_eq!(
        final_event.full_response.as_deref(),
        Some(response.as_str())
    );
}

#[tokio::test]
async fn channel_inbound_subscriber_handles_forced_web_error_without_external_services() {
    let subscriber = ChannelInboundSubscriber::new();
    assert_eq!(subscriber.name(), "channel::inbound_handler");
    assert_eq!(subscriber.domains(), Some(&["channel"][..]));

    web_test_support::set_forced_run_chat_task_error_for_test(Some(
        "openrouter API error (429 Too Many Requests): Retry-After: 3",
    ))
    .await;

    timeout(
        Duration::from_secs(10),
        subscriber.handle(&DomainEvent::ChannelInboundMessage {
            event_name: "discord:message".to_string(),
            channel: "discord:guild-1".to_string(),
            message: "Please summarize the thread.".to_string(),
            sender: Some("user-a".to_string()),
            reply_target: Some("channel-a".to_string()),
            thread_ts: Some("1700000000.001".to_string()),
            raw_data: json!({ "round": 20 }),
        }),
    )
    .await
    .expect("inbound subscriber should finish after forced web error");

    web_test_support::set_forced_run_chat_task_error_for_test(None).await;
}

#[tokio::test]
async fn channel_inbound_subscriber_ignores_unrelated_events() {
    timeout(
        Duration::from_secs(2),
        ChannelInboundSubscriber::default().handle(&DomainEvent::SystemStartup {
            component: "round20".to_string(),
        }),
    )
    .await
    .expect("unrelated event should return immediately");
}
