//! Domain events published on the [`crate::ports::EventBus`] and relayed to the
//! presentation layer (ARCHITECTURE §3.2).

use crate::conversation::ConversationParty;
use crate::device::DeviceId;
use crate::ids::{
    AgentId, IssueId, LocalModelServerId, ProfileId, ProjectId, SessionId, SkillId, SprintId,
    TaskId, TemplateId,
};
use crate::issue::{IssueLinkKind, IssuePriority, IssueRef, IssueStatus, IssueVersion};
use crate::mailbox::TicketId;
use crate::memory::MemorySlug;
use crate::sprint::{SprintOrder, SprintVersion};
use crate::template::TemplateVersion;

/// Which entry door a processed orchestration request arrived through.
///
/// IdeA exposes the *same* [`crate::OrchestratorService::dispatch`] behind two
/// substitutable driving adapters (ARCHITECTURE §14.3 + cadrage `orchestration-v3`):
/// the `.ideai/requests` filesystem watcher and the MCP server. This tag — set by
/// the adapter that received the request, never inferred in the application — lets
/// the presentation layer surface *how* a delegation came in.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OrchestrationSource {
    /// A JSON request file dropped under `.ideai/requests/`.
    File,
    /// A `tools/call` on the MCP server.
    Mcp,
}

/// Events emitted by the domain/application as state changes occur.
///
/// Deliberately *not* `Serialize`/`Deserialize`: events are an in-process
/// concern relayed to IPC by an infrastructure adapter, which owns the wire
/// format. `PtyOutput` in particular is usually short-circuited to a Tauri
/// channel rather than serialised here.
#[derive(Debug, Clone, PartialEq)]
pub enum DomainEvent {
    /// A project was created.
    ProjectCreated {
        /// The new project.
        project_id: ProjectId,
    },
    /// An agent was launched in a terminal.
    AgentLaunched {
        /// The agent.
        agent_id: AgentId,
        /// The session it runs in.
        session_id: SessionId,
    },
    /// An agent launch failed before a runtime session could be created.
    AgentLaunchFailed {
        /// The agent whose launch failed.
        agent_id: AgentId,
        /// Failure cause namespace.
        cause: String,
        /// Stable error code.
        code: String,
        /// Human-readable message.
        message: String,
    },
    /// A local model-server status changed while preparing a launch.
    ModelServerStatusChanged {
        /// The local model server.
        server_id: LocalModelServerId,
        /// New lifecycle status.
        status: crate::model_server::ModelServerLifecycleStatus,
    },
    /// A first-class background task started running.
    BackgroundTaskStarted {
        /// The owning project.
        project_id: ProjectId,
        /// The task.
        task_id: TaskId,
        /// The agent that owns completion delivery.
        owner_agent_id: AgentId,
    },
    /// A first-class background task changed state.
    BackgroundTaskStateChanged {
        /// The owning project.
        project_id: ProjectId,
        /// The task.
        task_id: TaskId,
        /// The agent that owns completion delivery.
        owner_agent_id: AgentId,
        /// New state.
        state: crate::background_task::BackgroundTaskState,
    },
    /// A first-class background task completed successfully.
    BackgroundTaskCompleted {
        /// The owning project.
        project_id: ProjectId,
        /// The task.
        task_id: TaskId,
        /// The agent that owns completion delivery.
        owner_agent_id: AgentId,
    },
    /// A first-class background task failed.
    BackgroundTaskFailed {
        /// The owning project.
        project_id: ProjectId,
        /// The task.
        task_id: TaskId,
        /// The agent that owns completion delivery.
        owner_agent_id: AgentId,
    },
    /// A first-class background task was cancelled.
    BackgroundTaskCancelled {
        /// The owning project.
        project_id: ProjectId,
        /// The task.
        task_id: TaskId,
        /// The agent that owns completion delivery.
        owner_agent_id: AgentId,
    },
    /// A first-class background task has a terminal result not yet delivered.
    BackgroundTaskCompletionDeliveryPending {
        /// The owning project.
        project_id: ProjectId,
        /// The task.
        task_id: TaskId,
        /// The agent that owns completion delivery.
        owner_agent_id: AgentId,
    },
    /// A first-class background task completion was delivered.
    BackgroundTaskCompletionDelivered {
        /// The owning project.
        project_id: ProjectId,
        /// The task.
        task_id: TaskId,
        /// The agent that owns completion delivery.
        owner_agent_id: AgentId,
    },
    /// An item was queued in an agent inbox.
    AgentInboxQueued {
        /// Target agent.
        agent_id: AgentId,
        /// Queue depth after enqueue.
        depth: usize,
    },
    /// An item was drained from an agent inbox.
    AgentInboxDrained {
        /// Target agent.
        agent_id: AgentId,
        /// Queue depth after drain.
        depth: usize,
    },
    /// A wake was scheduled for an agent.
    AgentWakeScheduled {
        /// Owning project.
        project_id: ProjectId,
        /// Target agent.
        agent_id: AgentId,
    },
    /// A wake turn started for an agent.
    AgentWakeStarted {
        /// Owning project.
        project_id: ProjectId,
        /// Target agent.
        agent_id: AgentId,
    },
    /// A wake attempt failed.
    AgentWakeFailed {
        /// Owning project.
        project_id: ProjectId,
        /// Target agent.
        agent_id: AgentId,
        /// Human-readable reason.
        reason: String,
    },
    /// A target agent produced a synchronous reply to an inter-agent `ask`
    /// (ARCHITECTURE §17.4): the requester sent a task via `agent.message`, IdeA
    /// drove the target's structured session to its turn `Final`, and this is the
    /// observability beacon for that completed rendezvous. Carries the target id and
    /// the reply size (a lightweight metric); the full body is returned to the
    /// requester out-of-band, not in the event payload (the bus stays I/O-free).
    AgentReplied {
        /// The agent that produced the reply.
        agent_id: AgentId,
        /// Number of bytes in the reply content (preview metric, not the payload).
        reply_len: usize,
    },
    /// Intermediate assistant announcement emitted live during an inter-agent
    /// structured turn. Ephemeral observability event: never persisted, never terminal.
    AgentAnnouncement {
        /// Owning project.
        project_id: ProjectId,
        /// Human or source agent that requested the turn.
        requester: ConversationParty,
        /// Target agent producing the announcement.
        target: AgentId,
        /// FIFO ticket correlating this turn.
        ticket: TicketId,
        /// Announcement text.
        text: String,
        /// Wall-clock timestamp in epoch milliseconds.
        at_ms: u64,
    },
    /// An agent's process exited.
    AgentExited {
        /// The agent.
        agent_id: AgentId,
        /// Exit code.
        code: i32,
    },
    /// A ticket assistant session was opened for an issue.
    TicketAssistantOpened {
        /// Bound issue.
        issue_ref: IssueRef,
        /// Runtime profile used by the assistant.
        profile_id: ProfileId,
    },
    /// A ticket assistant session was closed for an issue.
    TicketAssistantClosed {
        /// Bound issue.
        issue_ref: IssueRef,
    },
    /// An agent's **busy/idle** state changed (cadrage C4 §4.2): it went `Busy` on
    /// the enqueue that started a turn, or `Idle` on `mark_idle` (prompt-ready or an
    /// explicit signal). Discrete, low-frequency beacon relayed to the front so the
    /// mediated-input view can dim "Envoyer" while a turn is in flight (it never
    /// blocks the enqueue — the FIFO keeps accepting; the flag is purely advisory).
    AgentBusyChanged {
        /// The agent whose state changed.
        agent_id: AgentId,
        /// `true` when a turn is in flight, `false` when the agent is idle.
        busy: bool,
    },
    /// An agent's **liveness** (alive/stalled) changed (lot 2, chantier
    /// readiness/heartbeat). Emitted **once per transition** by the stall detector:
    /// `Alive→Stalled` when no proof of liveness arrived for longer than the profile's
    /// `stall_after_ms`, and `Stalled→Alive` when a late battement (delta / tool
    /// activity / heartbeat) revives it or the agent returns to `Idle`. Discrete,
    /// low-frequency beacon (no spam) relayed to the front so the mediated-input view
    /// can badge a frozen agent. Purely advisory — the FIFO and the turn keep running.
    AgentLivenessChanged {
        /// The agent whose liveness changed.
        agent_id: AgentId,
        /// The new liveness state.
        liveness: crate::input::AgentLiveness,
    },
    /// An agent's runtime profile was changed (hot-swap of the AI engine).
    AgentProfileChanged {
        /// The agent.
        agent_id: AgentId,
        /// The new runtime profile it now uses.
        profile_id: ProfileId,
    },
    /// A template was updated (content changed, version bumped).
    TemplateUpdated {
        /// The template.
        template_id: TemplateId,
        /// New version.
        version: TemplateVersion,
    },
    /// A synchronized agent is behind its template.
    AgentDriftDetected {
        /// The drifting agent.
        agent_id: AgentId,
        /// Version the agent is currently at.
        from: TemplateVersion,
        /// Version available from the template.
        to: TemplateVersion,
    },
    /// A synchronized agent received its template update.
    AgentSynced {
        /// The agent.
        agent_id: AgentId,
        /// Version it was brought up to.
        to: TemplateVersion,
    },
    /// A paired device was revoked and any live transport for it must be closed.
    DeviceRevoked {
        /// Revoked device.
        device_id: DeviceId,
    },
    /// All paired devices were revoked and every live paired transport must close.
    AllDevicesRevoked,
    /// A skill was assigned to (or unassigned from) an agent.
    SkillAssigned {
        /// The agent whose skill set changed.
        agent_id: AgentId,
        /// The skill involved.
        skill_id: SkillId,
        /// `true` if assigned, `false` if unassigned.
        assigned: bool,
    },
    /// A tab's layout changed.
    LayoutChanged {
        /// The project whose layout changed.
        project_id: ProjectId,
    },
    /// A remote host connection was established.
    RemoteConnected {
        /// The project on that remote.
        project_id: ProjectId,
    },
    /// Git state for a project changed.
    GitStateChanged {
        /// The project.
        project_id: ProjectId,
    },
    /// An orchestrator request (dropped under `.ideai/requests/`) was processed
    /// by IdeA on behalf of a requester agent (ARCHITECTURE §14.3). Relayed so the
    /// frontend can surface orchestration activity; the resulting cell/tab opens
    /// off the [`AgentLaunched`](Self::AgentLaunched) event for `spawn_agent`.
    OrchestratorRequestProcessed {
        /// Id of the requesting (orchestrator) agent — the request subdirectory.
        requester_id: String,
        /// The action that was processed (`spawn_agent`, `stop_agent`, …).
        action: String,
        /// Whether IdeA handled it successfully.
        ok: bool,
        /// Which entry door the request arrived through (file watcher vs MCP),
        /// tagged by the receiving adapter.
        source: OrchestrationSource,
    },
    /// A memory note was created or updated (`.md` written, index upserted).
    MemorySaved {
        /// The saved note's slug.
        slug: MemorySlug,
    },
    /// A memory note was deleted.
    MemoryDeleted {
        /// The deleted note's slug.
        slug: MemorySlug,
    },
    /// The aggregated `MEMORY.md` index was rebuilt for a project.
    MemoryIndexRebuilt {
        /// The project whose index was rebuilt.
        project_id: ProjectId,
    },
    /// An issue was created.
    IssueCreated {
        /// Stable issue id.
        issue_id: IssueId,
        /// Human-friendly issue reference.
        issue_ref: IssueRef,
    },
    /// An issue was updated.
    IssueUpdated {
        /// Human-friendly issue reference.
        issue_ref: IssueRef,
        /// New optimistic version.
        version: IssueVersion,
    },
    /// An issue was deleted.
    IssueDeleted {
        /// Owning project.
        project_id: ProjectId,
        /// Human-friendly issue reference.
        issue_ref: IssueRef,
        /// Sprint membership released by the deletion, when any.
        freed_sprint: Option<SprintId>,
    },
    /// An issue status changed.
    IssueStatusChanged {
        /// Human-friendly issue reference.
        issue_ref: IssueRef,
        /// New status.
        status: IssueStatus,
        /// New optimistic version.
        version: IssueVersion,
    },
    /// An issue priority changed.
    IssuePriorityChanged {
        /// Human-friendly issue reference.
        issue_ref: IssueRef,
        /// New priority.
        priority: IssuePriority,
        /// New optimistic version.
        version: IssueVersion,
    },
    /// An issue carnet was updated.
    IssueCarnetUpdated {
        /// Human-friendly issue reference.
        issue_ref: IssueRef,
        /// New optimistic version.
        version: IssueVersion,
    },
    /// Two issues were linked.
    IssueLinked {
        /// Source issue.
        issue_ref: IssueRef,
        /// Target issue.
        target: IssueRef,
        /// Link kind.
        kind: IssueLinkKind,
        /// New optimistic version.
        version: IssueVersion,
    },
    /// Two issues were unlinked.
    IssueUnlinked {
        /// Source issue.
        issue_ref: IssueRef,
        /// Target issue.
        target: IssueRef,
        /// Link kind.
        kind: IssueLinkKind,
        /// New optimistic version.
        version: IssueVersion,
    },
    /// An agent was assigned to an issue.
    IssueAgentAssigned {
        /// Human-friendly issue reference.
        issue_ref: IssueRef,
        /// Assigned agent.
        agent_id: AgentId,
        /// New optimistic version.
        version: IssueVersion,
    },
    /// An agent was unassigned from an issue.
    IssueAgentUnassigned {
        /// Human-friendly issue reference.
        issue_ref: IssueRef,
        /// Unassigned agent.
        agent_id: AgentId,
        /// New optimistic version.
        version: IssueVersion,
    },
    /// A sprint was created.
    SprintCreated {
        /// Stable sprint id.
        sprint_id: SprintId,
        /// Reorderable sprint order.
        order: SprintOrder,
    },
    /// A sprint was renamed.
    SprintRenamed {
        /// Stable sprint id.
        sprint_id: SprintId,
        /// New sprint name.
        name: String,
        /// New optimistic version.
        version: SprintVersion,
    },
    /// Sprint orders changed.
    SprintReordered {
        /// Stable sprint id.
        sprint_id: SprintId,
        /// New sprint order.
        order: SprintOrder,
        /// New optimistic version.
        version: SprintVersion,
    },
    /// A sprint was deleted.
    SprintDeleted {
        /// Stable sprint id.
        sprint_id: SprintId,
    },
    /// An issue was moved into, out of, or between sprints.
    IssueSprintChanged {
        /// Human-friendly issue reference.
        issue_ref: IssueRef,
        /// Previous sprint.
        from: Option<SprintId>,
        /// New sprint.
        to: Option<SprintId>,
        /// New optimistic version.
        version: IssueVersion,
    },
    /// A project's memory grew past the recall budget while no embedder is
    /// configured (strategy `none`): the first moment a semantic embedder would
    /// help (ARCHITECTURE §14.5.5, LOT C3). Published **at most once per session per
    /// project** (and never again once the user chose "ne plus demander"); the
    /// frontend surfaces a one-time, dismissible suggestion. Carries the detected
    /// local environment and the compiled-in capabilities so the UI never proposes
    /// a strategy this binary cannot run.
    EmbedderSuggested {
        /// The project whose memory crossed the budget.
        project_id: ProjectId,
        /// Whether an Ollama-style local embedding server was detected (best-effort).
        ollama_detected: bool,
        /// Ids of the recommended ONNX models already present in the local cache.
        onnx_cached: Vec<String>,
        /// Whether the HTTP capability (`localServer`/`api`) is compiled in.
        vector_http_enabled: bool,
        /// Whether the in-process ONNX capability (`localOnnx`) is compiled in.
        vector_onnx_enabled: bool,
    },
    /// A delegation is ready to be injected into the agent's **native terminal**
    /// (ARCHITECTURE §20.2/§20.3). Published when a turn *starts* (Idle→Busy); the
    /// backend stays the authority of the FIFO/busy state and **no longer writes the
    /// turn into the PTY** — the frontend cell runs the write-portal handshake and
    /// writes `text` + `submit_sequence` through the single PTY writer (the front).
    /// Discrete, low-frequency (one per delegation). Relayed to the front as
    /// `delegationReady` like every other [`DomainEvent`].
    DelegationReady {
        /// The target agent whose terminal will receive the delegation.
        agent_id: AgentId,
        /// The mailbox ticket correlating this turn (acked back via
        /// `delegation_delivered`); the requester's `ask` is woken by `idea_reply`.
        ticket: TicketId,
        /// The task text to inject (written without a trailing `\n` by the portal).
        text: String,
        /// Target profile's submit sequence (the cell applies it after the text).
        /// `None` ⇒ the front applies its default (`"\r"`); never hard-coded in the
        /// domain.
        submit_sequence: Option<String>,
        /// Target profile's submit delay in ms. `None` ⇒ front default (~60 ms).
        submit_delay_ms: Option<u32>,
    },
    /// Un agent vient d'entrer en **limite de session/débit** (ARCHITECTURE §21).
    /// Publié quand le service de limite enregistre une nouvelle `SessionLimit` (niveau
    /// 1 structuré ou niveau 2 motif). Balise discrète, basse fréquence, relayée au
    /// front pour afficher le badge « limité jusqu'à HH:MM ». Model-agnostique : ne
    /// porte que le fait neutre « limité, reset à T (peut-être) ».
    AgentRateLimited {
        /// L'agent entré en limite.
        agent_id: AgentId,
        /// Instant de reset en **époche-millisecondes**. `None` ⇒ heure inconnue
        /// (pas de reprise auto, filet humain).
        resets_at_ms: Option<i64>,
    },
    /// Une **reprise automatique** a été armée pour un agent limité (ARCHITECTURE §21).
    /// Publié après que le service a calculé le plan ([`crate::session_limit::plan_resume`])
    /// et armé le `Scheduler`. Relayé au front pour afficher le compte à rebours + le
    /// bouton « Annuler la reprise » (fenêtre annulable).
    AgentResumeScheduled {
        /// L'agent dont la reprise est programmée.
        agent_id: AgentId,
        /// Échéance du réveil en **époche-millisecondes**.
        fire_at_ms: i64,
    },
    /// La **reprise automatique** d'un agent a été **annulée** (ARCHITECTURE §21) :
    /// l'utilisateur a cliqué « Annuler la reprise » dans la fenêtre annulable. Relayé
    /// au front pour retirer le compte à rebours.
    AgentResumeCancelled {
        /// L'agent dont la reprise a été annulée.
        agent_id: AgentId,
    },
    /// Un agent a effectivement été **relancé** après une limite (ARCHITECTURE §21) :
    /// le réveil a tiré (ou reprise immédiate), l'agent a redémarré via
    /// [`crate::ports::SessionPlan::Resume`] avec un prompt de reprise court. Relayé au
    /// front pour effacer l'état « limité ».
    AgentResumed {
        /// L'agent relancé.
        agent_id: AgentId,
    },
    /// **Filet humain (niveau 3)** : une limite de session est **suspectée** sans
    /// qu'aucune heure de reset fiable ne soit connue (ARCHITECTURE §21.1 niveau 3) —
    /// typiquement un agent passé `Stalled` (lot 2) sans `SessionLimit` connue. IdeA ne
    /// reprend **jamais** à l'aveugle : ce signal demande au front de **solliciter
    /// l'utilisateur** (« limite détectée mais heure inconnue — reprendre à ? »).
    AgentRateLimitSuspected {
        /// L'agent dont la limite est suspectée.
        agent_id: AgentId,
        /// Instant de reset en **époche-millisecondes** si une estimation existe,
        /// sinon `None` (l'utilisateur fournira l'heure).
        resets_at_ms: Option<i64>,
    },
    /// The project's **orchestrator designation** changed (cadrage « orchestrateur
    /// du projet », T1). Emitted when an agent is designated (radio selection) or the
    /// designation is cleared back to the default (e.g. the designated agent was
    /// deleted — lazy succession to the oldest agent). Relayed to the front so the UI
    /// can move the radio / refresh the orchestrator badge. Model-agnostic: carries
    /// only the neutral fact « who orchestrates now ».
    OrchestratorChanged {
        /// The project whose designation changed.
        project_id: ProjectId,
        /// The newly designated orchestrator agent, or `None` for the default
        /// (the oldest agent orchestrates).
        orchestrator: Option<AgentId>,
    },
    /// Raw PTY output (usually routed to a dedicated channel, not this bus).
    PtyOutput {
        /// The session.
        session_id: SessionId,
        /// Output bytes.
        bytes: Vec<u8>,
    },
}

#[cfg(test)]
mod tests {
    use super::*;

    fn agent(n: u128) -> AgentId {
        AgentId::from_uuid(uuid::Uuid::from_u128(n))
    }

    fn profile(n: u128) -> ProfileId {
        ProfileId::from_uuid(uuid::Uuid::from_u128(n))
    }

    fn issue_ref(n: u64) -> IssueRef {
        IssueRef::from(crate::IssueNumber::new(n).unwrap())
    }

    // -- §21 : constructibilité + égalité PartialEq des 5 variantes --------------

    #[test]
    fn agent_rate_limited_constructs_and_compares() {
        let ev = DomainEvent::AgentRateLimited {
            agent_id: agent(1),
            resets_at_ms: Some(1_700_000_000_000),
        };
        assert_eq!(
            ev,
            DomainEvent::AgentRateLimited {
                agent_id: agent(1),
                resets_at_ms: Some(1_700_000_000_000),
            }
        );
        // Une heure différente ⇒ inégaux.
        assert_ne!(
            ev,
            DomainEvent::AgentRateLimited {
                agent_id: agent(1),
                resets_at_ms: None,
            }
        );
    }

    #[test]
    fn agent_resume_scheduled_constructs_and_compares() {
        let ev = DomainEvent::AgentResumeScheduled {
            agent_id: agent(2),
            fire_at_ms: 1_700_000_000_000,
        };
        assert_eq!(
            ev,
            DomainEvent::AgentResumeScheduled {
                agent_id: agent(2),
                fire_at_ms: 1_700_000_000_000,
            }
        );
        assert_ne!(
            ev,
            DomainEvent::AgentResumeScheduled {
                agent_id: agent(2),
                fire_at_ms: 0,
            }
        );
    }

    #[test]
    fn agent_resume_cancelled_constructs_and_compares() {
        let ev = DomainEvent::AgentResumeCancelled { agent_id: agent(3) };
        assert_eq!(ev, DomainEvent::AgentResumeCancelled { agent_id: agent(3) });
        assert_ne!(ev, DomainEvent::AgentResumeCancelled { agent_id: agent(4) });
    }

    #[test]
    fn agent_resumed_constructs_and_compares() {
        let ev = DomainEvent::AgentResumed { agent_id: agent(5) };
        assert_eq!(ev, DomainEvent::AgentResumed { agent_id: agent(5) });
        assert_ne!(ev, DomainEvent::AgentResumed { agent_id: agent(6) });
    }

    #[test]
    fn agent_rate_limit_suspected_constructs_and_compares() {
        let ev = DomainEvent::AgentRateLimitSuspected {
            agent_id: agent(7),
            resets_at_ms: None,
        };
        assert_eq!(
            ev,
            DomainEvent::AgentRateLimitSuspected {
                agent_id: agent(7),
                resets_at_ms: None,
            }
        );
        assert_ne!(
            ev,
            DomainEvent::AgentRateLimitSuspected {
                agent_id: agent(7),
                resets_at_ms: Some(42),
            }
        );
    }

    #[test]
    fn orchestrator_changed_constructs_and_compares() {
        let project = ProjectId::from_uuid(uuid::Uuid::from_u128(100));
        let ev = DomainEvent::OrchestratorChanged {
            project_id: project,
            orchestrator: Some(agent(1)),
        };
        assert_eq!(
            ev,
            DomainEvent::OrchestratorChanged {
                project_id: project,
                orchestrator: Some(agent(1)),
            }
        );
        // Clearing to the default (None) is a distinct event.
        assert_ne!(
            ev,
            DomainEvent::OrchestratorChanged {
                project_id: project,
                orchestrator: None,
            }
        );
    }

    #[test]
    fn ticket_assistant_opened_constructs_and_compares() {
        let ev = DomainEvent::TicketAssistantOpened {
            issue_ref: issue_ref(7),
            profile_id: profile(9),
        };
        assert_eq!(
            ev,
            DomainEvent::TicketAssistantOpened {
                issue_ref: issue_ref(7),
                profile_id: profile(9),
            }
        );
        assert_ne!(
            ev,
            DomainEvent::TicketAssistantOpened {
                issue_ref: issue_ref(8),
                profile_id: profile(9),
            }
        );
    }

    #[test]
    fn ticket_assistant_closed_constructs_and_compares() {
        let ev = DomainEvent::TicketAssistantClosed {
            issue_ref: issue_ref(7),
        };
        assert_eq!(
            ev,
            DomainEvent::TicketAssistantClosed {
                issue_ref: issue_ref(7),
            }
        );
        assert_ne!(
            ev,
            DomainEvent::TicketAssistantClosed {
                issue_ref: issue_ref(8),
            }
        );
    }

    #[test]
    fn distinct_session_limit_variants_are_not_equal() {
        // Les variantes ne se confondent pas entre elles malgré des champs proches.
        assert_ne!(
            DomainEvent::AgentResumeCancelled { agent_id: agent(8) },
            DomainEvent::AgentResumed { agent_id: agent(8) }
        );
    }
}
