//! LOT D1 (ARCHITECTURE §17.5 / §17.9 ligne D1) — tests unitaires des registres
//! structurés et de l'agrégateur de liveness, **100 % fakes**.
//!
//! Couvre :
//! - [`StructuredSessions`] : `insert` + résolution `session_for_agent` /
//!   `session_id_for_agent` / `node_for_agent` / `session` (`None` si inconnu) ;
//!   invariant « 1 session vivante par agent » ; `rebind_agent_node` (change la
//!   cellule, pas la session) ; `remove` (retire + renvoie la session) ;
//!   `live_agents` (triplets `(AgentId, NodeId, SessionId)`) ; impl
//!   [`LiveAgentRegistry`] (`is_agent_live` / `is_node_live`).
//! - [`LiveSessions`] (agrégateur) : OR des deux registres pour `is_agent_live` /
//!   `is_node_live` ; `live_agents` agrège (PTY puis structuré) ;
//!   `node_for_agent` / `session_id_for_agent` trouvent dans l'un ou l'autre.
//!
//! Style calqué sur `terminal_usecases.rs` (constructeurs validants du domaine,
//! pas de littéraux fragiles). Le fake `AgentSession` est local et minimal :
//! ces tests n'exercent QUE le registre, pas `send`.

use std::sync::Arc;

use async_trait::async_trait;

use application::{LiveAgentRegistry, LiveSessions, StructuredSessions, TerminalSessions};
use domain::ports::{AgentSession, AgentSessionError, PtyHandle, ReplyStream};
use domain::{
    AgentId, NodeId, ProjectId, 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 pid(n: u128) -> ProjectId {
    ProjectId::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é).
/// `send`/`shutdown` ne sont pas exercés ici.
struct FakeSession {
    id: SessionId,
    conversation_id: Option<String>,
}

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

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

fn fake_with_conversation(id: SessionId, conversation_id: &str) -> Arc<dyn AgentSession> {
    Arc::new(FakeSession {
        id,
        conversation_id: Some(conversation_id.to_owned()),
    })
}

// ===========================================================================
// StructuredSessions
// ===========================================================================

#[test]
fn structured_insert_resolve_and_remove() {
    let reg = StructuredSessions::new();
    assert!(reg.is_empty());
    assert_eq!(reg.len(), 0);

    let s = sid(1);
    let a = aid(10);
    let n = nid(100);
    reg.insert(fake(s), a, n);

    assert_eq!(reg.len(), 1);
    assert!(!reg.is_empty());

    // Résolution par id de session.
    assert!(reg.session(&s).is_some());
    assert_eq!(reg.session(&s).unwrap().id(), s);

    // Résolution par agent.
    assert_eq!(reg.session_id_for_agent(&a), Some(s));
    assert_eq!(reg.node_for_agent(&a), Some(n));
    assert!(reg.session_for_agent(&a).is_some());
    assert_eq!(reg.session_for_agent(&a).unwrap().id(), s);

    // Inconnu ⇒ None partout.
    assert!(reg.session(&sid(999)).is_none());
    assert!(reg.session_for_agent(&aid(999)).is_none());
    assert!(reg.session_id_for_agent(&aid(999)).is_none());
    assert!(reg.node_for_agent(&aid(999)).is_none());

    // remove retire ET renvoie la session.
    let removed = reg.remove(&s).expect("remove returns the session");
    assert_eq!(removed.id(), s);
    assert!(reg.is_empty());
    assert!(reg.remove(&s).is_none(), "second remove is a no-op");
    assert!(reg.session_for_agent(&a).is_none());
}

#[test]
fn structured_meta_for_session_resolves_agent_node_and_conversation() {
    // LS7 (§21.10) : le tap niveau 1 n'a que le `SessionId` du tour et doit retrouver
    // l'agent + la cellule hôte à passer au service de limite. Lookup direct par id.
    let reg = StructuredSessions::new();
    let s = sid(1);
    let a = aid(10);
    let n = nid(100);
    reg.insert(fake_with_conversation(s, "conv-live"), a, n);

    assert_eq!(
        reg.meta_for_session(&s),
        Some((pid(0), a, n, Some("conv-live".to_owned())))
    );

    // Id inconnu (ou retiré) ⇒ None (jamais de panique sur une session morte).
    assert_eq!(reg.meta_for_session(&sid(999)), None);
    reg.remove(&s);
    assert_eq!(reg.meta_for_session(&s), None);
}

#[test]
fn structured_live_agents_lists_triples() {
    let reg = StructuredSessions::new();
    reg.insert(fake(sid(1)), aid(10), nid(100));
    reg.insert(fake(sid(2)), aid(20), nid(200));

    let mut live = reg.live_agents();
    live.sort_by_key(|(a, _, _)| a.as_uuid());

    assert_eq!(
        live,
        vec![(aid(10), nid(100), sid(1)), (aid(20), nid(200), sid(2)),]
    );
}

#[test]
fn structured_one_live_session_per_agent_invariant() {
    // L'agent n'a pas de session vivante avant insertion.
    let reg = StructuredSessions::new();
    let a = aid(10);
    assert!(!reg.is_agent_live(pid(0), &a));
    assert!(reg.session_for_agent(&a).is_none());

    reg.insert(fake(sid(1)), a, nid(100));
    assert!(reg.is_agent_live(pid(0), &a));

    // `session_for_agent` est non ambigu : il rend LA session de l'agent.
    let resolved = reg.session_for_agent(&a).unwrap().id();
    assert_eq!(resolved, sid(1));
    // Et un seul triplet figure pour cet agent dans live_agents.
    let count = reg.live_agents().iter().filter(|(x, _, _)| *x == a).count();
    assert_eq!(count, 1, "one live session per agent");
}

#[test]
fn structured_rebind_changes_cell_not_session() {
    let reg = StructuredSessions::new();
    let a = aid(10);
    reg.insert(fake(sid(1)), a, nid(100));

    let new_cell = nid(200);
    let rebound = reg
        .rebind_agent_node(&a, new_cell)
        .expect("rebind returns the session");

    // Session inchangée (même id), cellule mise à jour.
    assert_eq!(rebound.id(), sid(1));
    assert_eq!(reg.node_for_agent(&a), Some(new_cell));
    assert_eq!(reg.session_id_for_agent(&a), Some(sid(1)));
    // Toujours une seule entrée (pas de duplication).
    assert_eq!(reg.len(), 1);

    // Rebind d'un agent inconnu ⇒ None.
    assert!(reg.rebind_agent_node(&aid(999), nid(300)).is_none());
}

#[test]
fn structured_live_agent_registry_impl() {
    let reg = StructuredSessions::new();
    let a = aid(10);
    let n = nid(100);

    assert!(!reg.is_agent_live(pid(0), &a));
    assert!(!reg.is_node_live(&n));

    reg.insert(fake(sid(1)), a, n);

    assert!(reg.is_agent_live(pid(0), &a));
    assert!(reg.is_node_live(&n));
    assert!(!reg.is_agent_live(pid(0), &aid(999)));
    assert!(!reg.is_node_live(&nid(999)));

    // is_node_live suit le rebind (la cellule vivante change).
    let _ = reg.rebind_agent_node(&a, nid(200));
    assert!(!reg.is_node_live(&n), "old cell no longer live");
    assert!(reg.is_node_live(&nid(200)), "new cell is live");
}

#[test]
fn structured_sessions_snapshot_for_global_shutdown() {
    let reg = StructuredSessions::new();
    reg.insert(fake(sid(1)), aid(10), nid(100));
    reg.insert(fake(sid(2)), aid(20), nid(200));

    let mut ids: Vec<SessionId> = reg.sessions().iter().map(|s| s.id()).collect();
    ids.sort_by_key(|s| s.as_uuid());
    assert_eq!(ids, vec![sid(1), sid(2)]);
}

// ===========================================================================
// LiveSessions — agrégateur (PTY OR structuré)
// ===========================================================================

/// Insère un agent PTY dans `TerminalSessions`.
fn insert_pty(pty: &TerminalSessions, s: SessionId, a: AgentId, n: NodeId) {
    insert_pty_in_project(pty, pid(0), s, a, n);
}

/// Insère un agent PTY dans `TerminalSessions` pour un projet explicite.
fn insert_pty_in_project(
    pty: &TerminalSessions,
    project_id: ProjectId,
    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_in_project(project_id, PtyHandle { session_id: s }, session);
}

#[test]
fn aggregator_agent_live_via_structured_only() {
    let pty = Arc::new(TerminalSessions::new());
    let structured = Arc::new(StructuredSessions::new());
    let agg = LiveSessions::new(Arc::clone(&pty), Arc::clone(&structured));

    let a = aid(10);
    structured.insert(fake(sid(1)), a, nid(100));

    assert!(agg.is_agent_live(pid(0), &a), "live in structured ⇒ true");
    assert!(agg.is_node_live(&nid(100)));
    assert_eq!(agg.session_id_for_agent(pid(0), &a), Some(sid(1)));
    assert_eq!(agg.node_for_agent(pid(0), &a), Some(nid(100)));
}

#[test]
fn aggregator_agent_live_via_pty_only() {
    let pty = Arc::new(TerminalSessions::new());
    let structured = Arc::new(StructuredSessions::new());
    let agg = LiveSessions::new(Arc::clone(&pty), Arc::clone(&structured));

    let a = aid(20);
    insert_pty(&pty, sid(2), a, nid(200));

    assert!(agg.is_agent_live(pid(0), &a), "live in PTY ⇒ true");
    assert!(agg.is_node_live(&nid(200)));
    assert_eq!(agg.session_id_for_agent(pid(0), &a), Some(sid(2)));
    assert_eq!(agg.node_for_agent(pid(0), &a), Some(nid(200)));
}

#[test]
fn aggregator_agent_absent_from_both_is_not_live() {
    let pty = Arc::new(TerminalSessions::new());
    let structured = Arc::new(StructuredSessions::new());
    let agg = LiveSessions::new(pty, structured);

    let a = aid(30);
    assert!(!agg.is_agent_live(pid(0), &a), "absent from both ⇒ false");
    assert!(!agg.is_node_live(&nid(300)));
    assert!(agg.session_id_for_agent(pid(0), &a).is_none());
    assert!(agg.node_for_agent(pid(0), &a).is_none());
    assert!(agg.live_agents(pid(0)).is_empty());
}

#[test]
fn aggregator_live_agents_concatenates_pty_then_structured() {
    let pty = Arc::new(TerminalSessions::new());
    let structured = Arc::new(StructuredSessions::new());
    let agg = LiveSessions::new(Arc::clone(&pty), Arc::clone(&structured));

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

    let all = agg.live_agents(pid(0));
    assert_eq!(all.len(), 2, "both registries contribute");
    // PTY d'abord, structuré ensuite (ordre documenté de l'agrégateur).
    assert_eq!(all[0], (aid(10), nid(100), sid(1)));
    assert_eq!(all[1], (aid(20), nid(200), sid(2)));
}

#[test]
fn aggregator_resolution_prefers_pty_then_falls_back_to_structured() {
    // Deux agents distincts, un par registre : la résolution trouve chacun
    // dans le bon registre (OR / fallback).
    let pty = Arc::new(TerminalSessions::new());
    let structured = Arc::new(StructuredSessions::new());
    let agg = LiveSessions::new(Arc::clone(&pty), Arc::clone(&structured));

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

    // Agent PTY : résolu par le registre PTY.
    assert_eq!(agg.session_id_for_agent(pid(0), &pty_agent), Some(sid(1)));
    assert_eq!(agg.node_for_agent(pid(0), &pty_agent), Some(nid(100)));
    // Agent structuré : fallback sur le registre structuré.
    assert_eq!(
        agg.session_id_for_agent(pid(0), &struct_agent),
        Some(sid(2))
    );
    assert_eq!(agg.node_for_agent(pid(0), &struct_agent), Some(nid(200)));

    // is_node_live : OR sur les deux.
    assert!(agg.is_node_live(&nid(100)));
    assert!(agg.is_node_live(&nid(200)));
    assert!(!agg.is_node_live(&nid(999)));
}

#[test]
fn same_agent_id_in_distinct_projects_has_isolated_live_sessions() {
    let pty = Arc::new(TerminalSessions::new());
    let structured = Arc::new(StructuredSessions::new());
    let agg = LiveSessions::new(Arc::clone(&pty), Arc::clone(&structured));
    let a = aid(10);

    insert_pty_in_project(&pty, pid(1), sid(1), a, nid(100));
    structured.insert_in_project(pid(2), fake(sid(2)), a, nid(200));

    assert!(agg.is_agent_live(pid(1), &a));
    assert!(agg.is_agent_live(pid(2), &a));
    assert_eq!(agg.session_id_for_agent(pid(1), &a), Some(sid(1)));
    assert_eq!(agg.node_for_agent(pid(1), &a), Some(nid(100)));
    assert_eq!(agg.session_id_for_agent(pid(2), &a), Some(sid(2)));
    assert_eq!(agg.node_for_agent(pid(2), &a), Some(nid(200)));
    assert_eq!(agg.live_agents(pid(1)), vec![(a, nid(100), sid(1))]);
    assert_eq!(agg.live_agents(pid(2)), vec![(a, nid(200), sid(2))]);
}
