//! LOT R0b (cadrage orchestration v5 §3.3, Trou B) — `list_live_agents` lit
//! l'agrégateur `LiveSessions` (PTY **+** structuré), plus seulement le registre
//! PTY. Un agent chat (structuré) vivant doit apparaître dans la liste, comme un
//! agent PTY ; les deux ensemble sans doublon ; aucune session ⇒ liste vide.
//!
//! Ces tests reproduisent **exactement** ce que fait la commande
//! `list_live_agents` : construire un `LiveSessions` à partir des deux registres
//! partagés et passer `live_agent_snapshots()` à `LiveAgentListDto::from_snapshots`. Ils
//! exercent donc le câblage de l'agrégateur **et** la dé-duplication par agent
//! portée par le DTO (contrat de liveness pour l'UI). 100 % fakes, sans process.

use std::sync::Arc;

use async_trait::async_trait;

use app_tauri_lib::dto::LiveAgentListDto;
use application::{LiveSessions, StructuredSessions, TerminalSessions};
use domain::ports::{AgentSession, AgentSessionError, PtyHandle, ReplyStream};
use domain::{AgentId, NodeId, ProjectPath, PtySize, SessionId, SessionKind, TerminalSession};
use uuid::Uuid;

// --- petits constructeurs déterministes ------------------------------------

fn sid(n: u128) -> SessionId {
    SessionId::from_uuid(Uuid::from_u128(n))
}
fn aid(n: u128) -> AgentId {
    AgentId::from_uuid(Uuid::from_u128(n))
}
fn nid(n: u128) -> NodeId {
    NodeId::from_uuid(Uuid::from_u128(n))
}

/// Fake minimal d'`AgentSession` : porte juste l'id (ce que le registre clé).
struct FakeSession {
    id: SessionId,
}

#[async_trait]
impl AgentSession for FakeSession {
    fn id(&self) -> SessionId {
        self.id
    }
    fn conversation_id(&self) -> Option<String> {
        None
    }
    async fn send(&self, _prompt: &str) -> Result<ReplyStream, AgentSessionError> {
        Ok(Box::new(std::iter::empty()))
    }
    async fn shutdown(&self) -> Result<(), AgentSessionError> {
        Ok(())
    }
}

fn fake(id: SessionId) -> Arc<dyn AgentSession> {
    Arc::new(FakeSession { id })
}

/// Insère un agent PTY dans `TerminalSessions` (jumeau du helper de D1).
fn insert_pty(pty: &TerminalSessions, s: SessionId, a: AgentId, n: NodeId) {
    let session = TerminalSession::starting(
        s,
        n,
        ProjectPath::new("/p").unwrap(),
        SessionKind::Agent { agent_id: a },
        PtySize::new(24, 80).unwrap(),
    );
    pty.insert(PtyHandle { session_id: s }, session);
}

/// Reproduit le corps de la commande `list_live_agents` (hors validation d'id).
fn dto_for(pty: &Arc<TerminalSessions>, structured: &Arc<StructuredSessions>) -> LiveAgentListDto {
    let live = LiveSessions::new(Arc::clone(pty), Arc::clone(structured));
    LiveAgentListDto::from_snapshots(live.live_agent_snapshots())
}

// ===========================================================================
// 1. Un agent PTY vivant apparaît.
// ===========================================================================
#[test]
fn pty_live_agent_is_listed() {
    let pty = Arc::new(TerminalSessions::new());
    let structured = Arc::new(StructuredSessions::new());

    let a = aid(10);
    insert_pty(&pty, sid(1), a, nid(100));

    let dto = dto_for(&pty, &structured);
    assert_eq!(dto.0.len(), 1);
    assert_eq!(dto.0[0].agent_id, a.to_string());
    assert_eq!(dto.0[0].node_id, nid(100).to_string());
    assert_eq!(dto.0[0].session_id, sid(1).to_string());
    let json = serde_json::to_value(&dto).unwrap();
    assert_eq!(json[0]["kind"], "pty");
}

// ===========================================================================
// 2. Un agent structuré/chat vivant apparaît (le cas qui régressait — Trou B).
// ===========================================================================
#[test]
fn structured_live_agent_is_listed() {
    let pty = Arc::new(TerminalSessions::new());
    let structured = Arc::new(StructuredSessions::new());

    let a = aid(20);
    structured.insert(fake(sid(2)), a, nid(200));

    let dto = dto_for(&pty, &structured);
    assert_eq!(
        dto.0.len(),
        1,
        "un agent chat vivant doit apparaître (il était invisible avant R0b)"
    );
    assert_eq!(dto.0[0].agent_id, a.to_string());
    assert_eq!(dto.0[0].node_id, nid(200).to_string());
    assert_eq!(dto.0[0].session_id, sid(2).to_string());
    let json = serde_json::to_value(&dto).unwrap();
    assert_eq!(json[0]["kind"], "structured");
}

// ===========================================================================
// 3. Les deux types vivants en même temps ⇒ présents, sans doublon.
// ===========================================================================
#[test]
fn both_kinds_live_listed_without_duplicates() {
    let pty = Arc::new(TerminalSessions::new());
    let structured = Arc::new(StructuredSessions::new());

    let pty_agent = aid(10);
    let chat_agent = aid(20);
    insert_pty(&pty, sid(1), pty_agent, nid(100));
    structured.insert(fake(sid(2)), chat_agent, nid(200));

    let dto = dto_for(&pty, &structured);
    assert_eq!(dto.0.len(), 2, "les deux registres contribuent");

    let mut ids: Vec<&str> = dto.0.iter().map(|d| d.agent_id.as_str()).collect();
    ids.sort_unstable();
    let mut expected = vec![pty_agent.to_string(), chat_agent.to_string()];
    expected.sort_unstable();
    assert_eq!(ids, expected);

    // Aucun agent répété (contrat de liveness pour l'UI).
    let mut uniq = ids.clone();
    uniq.dedup();
    assert_eq!(uniq.len(), dto.0.len(), "aucun doublon d'agent");
    let json = serde_json::to_value(&dto).unwrap();
    let mut kinds = json
        .as_array()
        .unwrap()
        .iter()
        .map(|row| row["kind"].as_str().unwrap())
        .collect::<Vec<_>>();
    kinds.sort_unstable();
    assert_eq!(kinds, vec!["pty", "structured"]);
}

// ===========================================================================
// 3bis. Même agent présent dans les DEUX registres ⇒ une seule entrée.
//
// L'invariant « 1 session vivante/agent » l'interdit normalement, mais le DTO
// doit rester dup-free par défense en profondeur : un seul `agent_id` sur le fil.
// ===========================================================================
#[test]
fn same_agent_in_both_registries_is_deduplicated() {
    let pty = Arc::new(TerminalSessions::new());
    let structured = Arc::new(StructuredSessions::new());

    let a = aid(30);
    insert_pty(&pty, sid(1), a, nid(100));
    structured.insert(fake(sid(2)), a, nid(200));

    let dto = dto_for(&pty, &structured);
    assert_eq!(
        dto.0.len(),
        1,
        "un même agent ne doit apparaître qu'une fois"
    );
    assert_eq!(dto.0[0].agent_id, a.to_string());
    // On garde la première occurrence (PTY, listé en premier par l'agrégateur).
    assert_eq!(dto.0[0].node_id, nid(100).to_string());
    assert_eq!(dto.0[0].session_id, sid(1).to_string());
    let json = serde_json::to_value(&dto).unwrap();
    assert_eq!(json[0]["kind"], "pty");
}

// ===========================================================================
// 4. Aucune session ⇒ liste vide.
// ===========================================================================
#[test]
fn no_sessions_yields_empty_list() {
    let pty = Arc::new(TerminalSessions::new());
    let structured = Arc::new(StructuredSessions::new());

    let dto = dto_for(&pty, &structured);
    assert!(dto.0.is_empty(), "aucune session ⇒ liste vide");
}
