//! Tests for `spawn_stage_notification_subscriber`.
//!
//! Each test triggers real CRDT pipeline transitions so that `TransitionFired`
//! events propagate through the global broadcast channel to the subscriber.
//! Unique story IDs avoid interference between concurrently-running tests.

use super::mock_transport::MockTransport;
use super::stage_subscriber::spawn_stage_notification_subscriber;
use std::path::PathBuf;

fn tmp_root() -> PathBuf {
    tempfile::tempdir().unwrap().keep()
}

fn setup_story(item_id: &str, stage_dir: &str, name: &str) {
    crate::db::ensure_content_store();
    crate::db::write_item_with_content(
        item_id,
        stage_dir,
        &format!("---\nname: {name}\n---\n"),
        crate::db::ItemMeta::named(name),
    );
}

// ── Backlog → Coding ──────────────────────────────────────────────────────────

/// Transitioning a story from Backlog to Coding must produce a notification
/// that names the story and says "Current".
#[tokio::test]
async fn backlog_to_coding_sends_notification() {
    crate::crdt_state::init_for_test();
    setup_story("9951_story_b2c", "1_backlog", "Backlog to Coding");

    let (transport, calls) = MockTransport::new();
    spawn_stage_notification_subscriber(
        transport,
        || vec!["!room-b2c:example.org".to_string()],
        tmp_root(),
    );

    crate::agents::lifecycle::move_story_to_current("9951_story_b2c")
        .expect("move to current must succeed");

    tokio::time::sleep(std::time::Duration::from_millis(350)).await;

    let calls = calls.lock().unwrap();
    let hit = calls
        .iter()
        .any(|(_, plain, _)| plain.contains("9951") && plain.contains("Current"));
    assert!(
        hit,
        "Expected a 'Current' notification for story 9951; got: {calls:?}"
    );
}

// ── Coding → QA ──────────────────────────────────────────────────────────────

/// Transitioning a story from Coding to QA must produce a notification
/// that names the story and says "QA".
#[tokio::test]
async fn coding_to_qa_sends_notification() {
    crate::crdt_state::init_for_test();
    setup_story("9952_story_c2q", "2_current", "Coding to QA");

    let (transport, calls) = MockTransport::new();
    spawn_stage_notification_subscriber(
        transport,
        || vec!["!room-c2q:example.org".to_string()],
        tmp_root(),
    );

    crate::agents::lifecycle::move_story_to_qa("9952_story_c2q").expect("move to qa must succeed");

    tokio::time::sleep(std::time::Duration::from_millis(350)).await;

    let calls = calls.lock().unwrap();
    let hit = calls
        .iter()
        .any(|(_, plain, _)| plain.contains("9952") && plain.contains("QA"));
    assert!(
        hit,
        "Expected a 'QA' notification for story 9952; got: {calls:?}"
    );
}

// ── Merge → Done ─────────────────────────────────────────────────────────────

/// Transitioning a story to Done must produce a notification with the party
/// emoji and say "Done".
#[tokio::test]
async fn merge_to_done_sends_party_notification() {
    crate::crdt_state::init_for_test();
    // Start in Merge stage so we can call move_story_to_done.
    setup_story("9953_story_m2d", "4_merge", "Merge to Done");

    let (transport, calls) = MockTransport::new();
    spawn_stage_notification_subscriber(
        transport,
        || vec!["!room-m2d:example.org".to_string()],
        tmp_root(),
    );

    crate::agents::lifecycle::move_story_to_done("9953_story_m2d")
        .expect("move to done must succeed");

    tokio::time::sleep(std::time::Duration::from_millis(350)).await;

    let calls = calls.lock().unwrap();
    let hit = calls.iter().any(|(_, plain, _)| {
        plain.contains("9953") && plain.contains("Done") && plain.contains('\u{1f389}')
    });
    assert!(
        hit,
        "Expected a party-emoji Done notification for story 9953; got: {calls:?}"
    );
}

// ── Coding → QA → Merge coalescing ───────────────────────────────────────────

/// Rapid successive transitions for the same item are coalesced so only the
/// final stage is announced in a single notification.
#[tokio::test]
async fn rapid_transitions_are_coalesced() {
    crate::crdt_state::init_for_test();
    setup_story("9954_story_coalesce", "2_current", "Coalesce Test");

    let (transport, calls) = MockTransport::new();
    spawn_stage_notification_subscriber(
        transport,
        || vec!["!room-coal:example.org".to_string()],
        tmp_root(),
    );

    // Coding → QA → Merge in rapid succession (no sleep between).
    crate::agents::lifecycle::move_story_to_qa("9954_story_coalesce")
        .expect("move to qa must succeed");
    crate::agents::lifecycle::move_story_to_merge("9954_story_coalesce")
        .expect("move to merge must succeed");

    // Wait for the debounce to flush.
    tokio::time::sleep(std::time::Duration::from_millis(350)).await;

    let calls = calls.lock().unwrap();
    // Filter to only this story's notifications.
    let story_calls: Vec<_> = calls
        .iter()
        .filter(|(_, plain, _)| plain.contains("9954"))
        .collect();

    // Exactly one notification for this story (the coalesced final stage).
    assert_eq!(
        story_calls.len(),
        1,
        "Rapid transitions must be coalesced into one notification; got: {story_calls:?}"
    );
    // Final destination must be Merge.
    assert!(
        story_calls[0].1.contains("Merge"),
        "Coalesced notification must mention the final stage (Merge); got: {}",
        story_calls[0].1
    );
}

// ── Dynamic room IDs ──────────────────────────────────────────────────────────

/// The subscriber calls the room-ID closure at notification time, so rooms
/// added after the subscriber is spawned are still reached.
#[tokio::test]
async fn dynamic_room_ids_are_resolved_at_notification_time() {
    use std::sync::Arc;
    crate::crdt_state::init_for_test();
    setup_story("9955_story_dynroom", "1_backlog", "Dynamic Room");

    let (transport, calls) = MockTransport::new();
    let rooms: Arc<std::sync::Mutex<Vec<String>>> = Arc::new(std::sync::Mutex::new(Vec::new()));
    let rooms_for_closure = Arc::clone(&rooms);

    spawn_stage_notification_subscriber(
        transport,
        move || rooms_for_closure.lock().unwrap().clone(),
        tmp_root(),
    );

    // Add a room AFTER spawning the subscriber (simulates WhatsApp first message).
    rooms.lock().unwrap().push("phone:+15559990001".to_string());

    crate::agents::lifecycle::move_story_to_current("9955_story_dynroom")
        .expect("move to current must succeed");

    tokio::time::sleep(std::time::Duration::from_millis(350)).await;

    let calls = calls.lock().unwrap();
    let hit = calls
        .iter()
        .any(|(room, plain, _)| room == "phone:+15559990001" && plain.contains("9955"));
    assert!(
        hit,
        "Must deliver to the dynamically-added room; got: {calls:?}"
    );
}

// ── No rooms → silent ────────────────────────────────────────────────────────

/// When no rooms are registered the subscriber must not panic and must send
/// nothing.
#[tokio::test]
async fn no_rooms_produces_no_notifications() {
    crate::crdt_state::init_for_test();
    setup_story("9956_story_noroom", "1_backlog", "No Room Test");

    let (transport, calls) = MockTransport::new();
    spawn_stage_notification_subscriber(transport, Vec::new, tmp_root());

    crate::agents::lifecycle::move_story_to_current("9956_story_noroom")
        .expect("move to current must succeed");

    tokio::time::sleep(std::time::Duration::from_millis(350)).await;

    let calls = calls.lock().unwrap();
    let hits: Vec<_> = calls
        .iter()
        .filter(|(_, plain, _)| plain.contains("9956"))
        .collect();
    assert!(hits.is_empty(), "No rooms means no notifications");
}

// ── story_name included ───────────────────────────────────────────────────────

/// The notification must include the story name read from the CRDT.
#[tokio::test]
async fn notification_includes_story_name() {
    crate::crdt_state::init_for_test();
    setup_story("9957_story_named", "1_backlog", "Named Story Feature");

    let (transport, calls) = MockTransport::new();
    spawn_stage_notification_subscriber(
        transport,
        || vec!["!room-name:example.org".to_string()],
        tmp_root(),
    );

    crate::agents::lifecycle::move_story_to_current("9957_story_named")
        .expect("move to current must succeed");

    tokio::time::sleep(std::time::Duration::from_millis(350)).await;

    let calls = calls.lock().unwrap();
    let hit = calls
        .iter()
        .any(|(_, plain, _)| plain.contains("9957") && plain.contains("Named Story Feature"));
    assert!(
        hit,
        "Notification must include the story name; got: {calls:?}"
    );
}
