//! Agent event system for real-time streaming.
//!
//! This module defines the event types emitted during agent execution,
//! which are streamed to clients via Server-Sent Events (SSE).
//!
//! # Event Types
//!
//! - [`AgentEvent`] - All possible agent execution events
//! - [`TokenUsage`] - Token consumption statistics
//! - [`TokenBudgetUsage`] - Detailed token budget information
//!
//! # Event Flow
//!
//! 1. **Token** events stream generated text
//! 2. **ToolStart/ToolComplete** track tool execution
//! 3. **TaskListUpdated** tracks progress
//! 4. **TokenBudgetUpdated** reports context management
//! 5. **Complete**, **Cancelled**, or **Error** ends the stream
//!
//! # Example
//!
//! ```javascript
//! const eventSource = new EventSource('/api/v1/events/session-id');
//! eventSource.onmessage = (event) => {
//!   const data = JSON.parse(event.data);
//!   switch (data.type) {
//!     case 'token':
//!       console.log('Token:', data.content);
//!       break;
//!     case 'complete':
//!       console.log('Done!');
//!       eventSource.close();
//!       break;
//!   }
//! };
//! ```

use crate::tools::ToolResult;
use bamboo_domain::{TaskItemStatus, TaskList};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

/// Represents events emitted during agent execution.
///
/// These events are streamed to clients via SSE to provide real-time
/// feedback on agent progress, tool execution, and completion.
///
/// # Variants
///
/// ## Text Generation
/// - `Token` - Streaming text token
/// - `ReasoningToken` - Streaming reasoning/thinking token (separate channel)
///
/// ## Tool Execution
/// - `ToolStart` - Tool execution started
/// - `ToolComplete` - Tool finished successfully
/// - `ToolError` - Tool execution failed
///
/// ## User Interaction
/// - `NeedClarification` - Agent needs user input
///
/// ## Progress Tracking
/// - `TaskListUpdated` - Task list created or modified
/// - `TaskListItemProgress` - Individual item progress
/// - `TaskListCompleted` - All items completed
/// - `TaskEvaluationStarted` - Task evaluation began
/// - `TaskEvaluationCompleted` - Task evaluation finished
/// - `GoldEvaluationStarted` - Gold observe-only evaluation began
/// - `GoldEvaluationCompleted` - Gold observe-only evaluation finished
///
/// ## Context Management
/// - `TokenBudgetUpdated` - Context budget changed
/// - `ContextCompressionStatus` - Context compression lifecycle progress
/// - `ContextSummarized` - Old messages summarized
///
/// ## Sub-agents (Async Spawn)
/// - `SubAgentStarted` - A child session is created and scheduled to run
/// - `SubAgentEvent` - Forwarded raw child event (full fidelity)
/// - `SubAgentHeartbeat` - Periodic heartbeat while the child is running
/// - `SubAgentCompleted` - Child session finished (completed/cancelled/error)
///
/// ## Terminal Events
/// - `Complete` - Execution finished successfully
/// - `Cancelled` - Execution was cancelled by the user
/// - `Error` - Execution failed
///
/// # Serialization
///
/// Events are serialized as JSON with a `type` field for discrimination:
/// ```json
/// {"type": "token", "content": "Hello"}
/// {"type": "complete", "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}}
/// {"type": "cancelled", "message": "Agent execution cancelled by user"}
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AgentEvent {
    /// Text token generated by the LLM.
    Token {
        /// Generated text content
        content: String,
    },

    /// Reasoning/thinking token generated by the LLM.
    ///
    /// This is streamed separately from assistant answer tokens so the UI can
    /// choose whether and how to display model reasoning traces.
    ReasoningToken {
        /// Generated reasoning content
        content: String,
    },

    /// Streaming output emitted while a specific tool call is running.
    ///
    /// This is used to render "live output" inside a tool-call card in the UI
    /// without mixing tool output into the assistant's main token stream.
    ToolToken {
        /// Tool call identifier that this output belongs to.
        tool_call_id: String,
        /// Output chunk.
        content: String,
    },

    /// Tool execution started.
    ToolStart {
        /// Unique tool call identifier
        tool_call_id: String,
        /// Name of the tool being executed
        tool_name: String,
        /// Tool arguments (JSON)
        arguments: serde_json::Value,
    },

    /// Tool execution completed successfully.
    ToolComplete {
        /// Tool call identifier
        tool_call_id: String,
        /// Tool execution result
        result: ToolResult,
    },

    /// Tool execution failed.
    ToolError {
        /// Tool call identifier
        tool_call_id: String,
        /// Error message
        error: String,
    },

    /// Structured lifecycle event for tool execution tracking.
    ///
    /// These events complement `ToolStart`/`ToolComplete`/`ToolError` with
    /// richer metadata (mutability, auto-approval, wall-clock timing) and
    /// are emitted by `ToolEmitter` (in `bamboo-agent-tools`).
    ToolLifecycle {
        /// Tool call identifier
        tool_call_id: String,
        /// Canonical tool name
        tool_name: String,
        /// Lifecycle phase: "begin", "finished", "error", "cancelled"
        phase: String,
        /// Wall-clock milliseconds since the call began (None for begin)
        #[serde(skip_serializing_if = "Option::is_none")]
        elapsed_ms: Option<u64>,
        /// Whether the tool mutates state (writes files, runs commands)
        is_mutating: bool,
        /// Whether execution was auto-approved (no user prompt needed)
        auto_approved: bool,
        /// Human-readable summary
        #[serde(skip_serializing_if = "Option::is_none")]
        summary: Option<String>,
        /// Error message (if phase == "error")
        #[serde(skip_serializing_if = "Option::is_none")]
        error: Option<String>,
    },

    /// Agent needs clarification from the user.
    NeedClarification {
        /// Question to ask the user
        question: String,
        /// Optional predefined options
        options: Option<Vec<String>>,
        /// Tool call identifier that triggered this clarification
        #[serde(default, skip_serializing_if = "Option::is_none")]
        tool_call_id: Option<String>,
        /// Tool name that triggered this clarification, when known.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        tool_name: Option<String>,
        /// Whether the user can provide a free-text response
        #[serde(default = "default_allow_custom")]
        allow_custom: bool,
    },

    /// Emitted when task list is created or updated.
    TaskListUpdated {
        /// Current task list state.
        task_list: TaskList,
    },

    /// Emitted when a task item makes progress (delta update).
    TaskListItemProgress {
        /// Session identifier
        session_id: String,
        /// Item identifier
        item_id: String,
        /// New item status
        status: TaskItemStatus,
        /// Number of tool calls made
        tool_calls_count: usize,
        /// Item version (for optimistic concurrency)
        version: u64,
    },

    /// Emitted when all task items are completed.
    TaskListCompleted {
        /// Session identifier
        session_id: String,
        /// Completion timestamp
        completed_at: DateTime<Utc>,
        /// Total agent rounds executed
        total_rounds: u32,
        /// Total tool calls made
        total_tool_calls: usize,
    },

    /// Emitted when task evaluation starts.
    TaskEvaluationStarted {
        /// Session identifier
        session_id: String,
        /// Number of items to evaluate
        items_count: usize,
    },

    /// Emitted when task evaluation completes.
    TaskEvaluationCompleted {
        /// Session identifier
        session_id: String,
        /// Number of items updated
        updates_count: usize,
        /// Evaluation reasoning
        reasoning: String,
    },

    /// Emitted when gold observe-only evaluation starts.
    GoldEvaluationStarted {
        /// Session identifier
        session_id: String,
        /// Evaluation checkpoint
        checkpoint: GoldCheckpoint,
        /// Current iteration / round number associated with the evaluation
        iteration: u32,
    },

    /// Emitted when gold observe-only evaluation completes.
    GoldEvaluationCompleted {
        /// Session identifier
        session_id: String,
        /// Evaluation checkpoint
        checkpoint: GoldCheckpoint,
        /// Current iteration / round number associated with the evaluation
        iteration: u32,
        /// Gold decision for the current checkpoint
        decision: GoldDecision,
        /// Confidence in the decision
        confidence: GoldConfidence,
        /// Short reasoning summary
        reasoning: String,
    },

    /// Emitted whenever the runtime goal state changes — a new status
    /// (active/complete/blocked/…), an incremented continuation count, or a
    /// freshly recorded side-channel double-check verdict. Lets the UI reflect
    /// live goal progress without re-fetching history. Ephemeral: it rides only
    /// the per-session `/events/{id}` stream; reconnecting clients read the
    /// authoritative `goal_state` from the history endpoint instead.
    GoalStatusChanged {
        /// Session identifier
        session_id: String,
        /// Full serialized goal state — identical shape to the history
        /// response's `goal_state` field (see `bamboo_engine::runtime::goal_state`).
        goal_state: serde_json::Value,
    },

    /// Emitted when token budget is prepared (after context truncation)
    TokenBudgetUpdated {
        /// Token budget details
        usage: TokenBudgetUsage,
    },

    /// Emitted when host-side context compression lifecycle changes.
    ContextCompressionStatus {
        /// Compression phase label (for example: pre-turn, mid-turn).
        phase: String,
        /// Compression status: started | completed | failed | skipped
        status: String,
    },

    /// Emitted when conversation context is summarized
    ContextSummarized {
        /// Generated summary text
        summary: String,
        /// Number of old messages summarized
        messages_summarized: usize,
        /// Tokens saved by summarization
        tokens_saved: u32,
        /// Context usage percentage before compression
        #[serde(default)]
        usage_before_percent: f64,
        /// Context usage percentage after compression
        #[serde(default)]
        usage_after_percent: f64,
        /// What triggered the compression: "auto" | "manual" | "critical"
        #[serde(default)]
        trigger_type: String,
    },

    /// Emitted when context pressure reaches warning or critical levels.
    /// Frontend should display this to the user as a proactive notification.
    ContextPressureNotification {
        /// Context usage as a percentage of the context window.
        percent: f64,
        /// Severity level: "warning" (70%) or "critical" (90%).
        level: String,
        /// Human-readable message describing the pressure state.
        message: String,
    },

    /// A child session was spawned from a parent session (async background job).
    SubAgentStarted {
        parent_session_id: String,
        child_session_id: String,
        /// Optional title (useful for UI lists).
        #[serde(default, skip_serializing_if = "Option::is_none")]
        title: Option<String>,
    },

    /// Forwarded raw child event to the parent session stream.
    ///
    /// Child sessions are not allowed to spawn further sessions, so this should not nest.
    SubAgentEvent {
        parent_session_id: String,
        child_session_id: String,
        event: Box<AgentEvent>,
    },

    /// Heartbeat emitted while a child session is running.
    SubAgentHeartbeat {
        parent_session_id: String,
        child_session_id: String,
        timestamp: DateTime<Utc>,
    },

    /// Child session finished (completed/cancelled/error).
    SubAgentCompleted {
        parent_session_id: String,
        child_session_id: String,
        /// One of: "completed" | "cancelled" | "error" | "skipped"
        status: String,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        error: Option<String>,
    },

    /// Background Bash shell finished (completed/killed/error).
    ///
    /// Emitted by the background shell runtime when a `run_in_background`
    /// command exits, so clients can react to (and, in later phases, resume
    /// around) long-running commands. Phase 1 (issue #84): completion signal
    /// only — this does not change the default foreground behavior.
    ///
    /// Delivery scope: a *live* signal. It rides the per-session
    /// `/events/{id}` stream and the in-memory late-subscriber replay cache
    /// (`is_critical_event`), but is intentionally **not** a durable change in
    /// Phase 1 — it is not written to the account change journal. Treat it as
    /// ephemeral: a reconnecting client should not rely on seeing a past
    /// `BashCompleted` via the journaled history.
    BashCompleted {
        /// Background shell session identifier (same value returned as `bash_id`).
        bash_id: String,
        /// The command string that was executed.
        command: String,
        /// Process exit code, when available (`None` for signal/killed termination).
        #[serde(default, skip_serializing_if = "Option::is_none")]
        exit_code: Option<i32>,
        /// One of: "completed" | "killed" | "error".
        status: String,
    },

    /// Plan mode was entered.
    PlanModeEntered {
        /// Session identifier
        session_id: String,
        /// Optional reason for entering plan mode
        #[serde(default, skip_serializing_if = "Option::is_none")]
        reason: Option<String>,
        /// Previous permission mode before entering plan mode
        pre_permission_mode: String,
        /// RFC3339 timestamp when plan mode was entered.
        entered_at: chrono::DateTime<chrono::Utc>,
        /// Current plan mode phase/status.
        status: bamboo_domain::PlanModeStatus,
        /// Path to the persisted plan file, if already available.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        plan_file_path: Option<String>,
    },

    /// Plan mode was exited.
    PlanModeExited {
        /// Session identifier
        session_id: String,
        /// Whether the exit was approved by the user
        approved: bool,
        /// The permission mode restored after exiting
        restored_mode: String,
        /// Plan content that was reviewed, if any
        #[serde(default, skip_serializing_if = "Option::is_none")]
        plan: Option<String>,
    },

    /// Plan file was updated.
    PlanFileUpdated {
        /// Session identifier
        session_id: String,
        /// Path to the plan file
        file_path: String,
        /// Summary of the plan content (truncated)
        content_summary: String,
    },

    /// Runner progress update emitted at the start of each agent turn.
    ///
    /// Used to track live execution progress (round count, current activity)
    /// for diagnostic visibility, especially for child sessions.
    RunnerProgress {
        /// Session identifier
        session_id: String,
        /// Current turn/round count
        round_count: u32,
    },

    /// Session title was updated (auto-generated by backend or manually renamed via PATCH).
    SessionTitleUpdated {
        session_id: String,
        title: String,
        title_version: u64,
        source: TitleSource,
        updated_at: chrono::DateTime<chrono::Utc>,
    },

    /// Session pinned flag was toggled via PATCH.
    ///
    /// Replayable metadata event. `pinned` is an idempotent boolean so the
    /// latest event wins; `updated_at` is used by the frontend to suppress
    /// stale replays.
    SessionPinnedUpdated {
        session_id: String,
        pinned: bool,
        updated_at: chrono::DateTime<chrono::Utc>,
    },

    /// A new session was created.
    ///
    /// Change-feed event: durable, journaled, carried on the account `/stream`
    /// feed so other clients can insert the session into their list without a
    /// full `GET /sessions` poll.
    SessionCreated {
        session_id: String,
        title: String,
        kind: bamboo_domain::SessionKind,
        created_at: chrono::DateTime<chrono::Utc>,
    },

    /// A session was deleted.
    ///
    /// Change-feed event: durable, journaled. Clients remove the session from
    /// their local list on receipt.
    SessionDeleted { session_id: String },

    /// A session's message history was cleared (session kept).
    ///
    /// Change-feed event: durable, journaled. Clients drop cached messages for
    /// the session and refetch lazily.
    SessionCleared { session_id: String },

    /// A message was appended to a session.
    ///
    /// Change-feed event: durable, journaled. The `seq` assigned to this event
    /// on the account feed is the message's feed coordinate (used by
    /// `GET /history/{id}?since={seq}` to compute deltas). `content` is the
    /// plain-text body matching what `/history` returns to the UI.
    MessageAppended {
        session_id: String,
        message_id: String,
        role: bamboo_domain::Role,
        content: String,
        created_at: chrono::DateTime<chrono::Utc>,
    },

    /// Execution run has started and the runner is now active.
    ///
    /// Emitted as the first event after a runner reservation succeeds,
    /// before any token or tool events. Carries the `run_id` so the
    /// frontend can correlate subsequent SSE events across reconnects.
    ExecutionStarted {
        /// Unique identifier for this execution run.
        run_id: String,
        /// Session identifier.
        session_id: String,
        /// ISO 8601 timestamp when the run started.
        started_at: String,
    },

    /// Tool execution requires user approval before proceeding.
    ///
    /// Emitted when a permission checker determines that a tool call needs
    /// explicit user confirmation (e.g., mutating operations in restricted
    /// permission mode). The frontend should present the approval request and
    /// either grant or deny it.
    ToolApprovalRequested {
        /// Unique identifier for the tool call awaiting approval.
        tool_call_id: String,
        /// Name of the tool being executed.
        tool_name: String,
        /// Parameters that were passed to the tool.
        parameters: serde_json::Value,
    },

    /// A child sub-agent (out-of-process worker) hit a gated tool and proxied
    /// the approval decision to this parent over the actor protocol (Phase 2).
    /// The parent surfaces it to the human; the decision is routed back to the
    /// waiting child via
    /// `external_agents::live::deliver_approval(child_session_id, request_id, approved)`.
    ChildApprovalRequested {
        /// The child session whose gated tool is blocked awaiting approval.
        child_session_id: String,
        /// Correlates the eventual approve/deny reply back to the blocked tool.
        request_id: String,
        /// Name of the gated tool the child wants to run.
        tool_name: String,
        /// Human-readable description of the permission requested.
        permission: String,
        /// The concrete resource the action targets.
        resource: String,
    },

    /// Agent execution completed successfully.
    Complete {
        /// Final token usage statistics
        usage: TokenUsage,
    },

    /// Agent execution was cancelled.
    Cancelled {
        /// Optional human-readable message explaining the cancellation.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        message: Option<String>,
    },

    /// Agent execution failed.
    Error {
        /// Error message
        message: String,
    },

    /// A user-facing notification derived from agent activity by the backend
    /// notification policy. Clients render this (e.g. an OS desktop notification)
    /// after applying their own presence checks (window focus). The decision of
    /// *whether* to notify — category, priority, preference gating, dedup — is
    /// made server-side in `bamboo-notification`; the client just delivers it.
    Notification {
        /// Unique id (for client-side dedup / dismissal).
        id: String,
        /// Session this notification is about.
        session_id: String,
        /// Category, e.g. `needs_approval` | `needs_clarification` | `run_completed`
        /// | `run_failed` | `subagent_completed` | `context_critical`.
        category: String,
        /// Priority: `high` | `normal` | `low`.
        priority: String,
        /// Short title line.
        title: String,
        /// Body text.
        body: String,
        /// Stable key for client-side coalescing within a short window.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        dedup_key: Option<String>,
        /// RFC3339 creation timestamp.
        created_at: String,
    },
}

impl AgentEvent {
    /// Returns the session this event pertains to, when it carries one.
    ///
    /// Used by the account change-feed to route each event to the right
    /// client-side session without a per-session connection. For sub-agent
    /// events the *parent* session id is returned (that is the session a client
    /// observes in its list). Pure streaming/diagnostic variants (`Token`,
    /// `Complete`, …) return `None`; those are ephemeral and never ride the
    /// account feed anyway.
    pub fn session_id(&self) -> Option<&str> {
        match self {
            AgentEvent::TaskListUpdated { task_list } => Some(task_list.session_id.as_str()),
            AgentEvent::TaskListItemProgress { session_id, .. }
            | AgentEvent::TaskListCompleted { session_id, .. }
            | AgentEvent::TaskEvaluationStarted { session_id, .. }
            | AgentEvent::TaskEvaluationCompleted { session_id, .. }
            | AgentEvent::GoldEvaluationStarted { session_id, .. }
            | AgentEvent::GoldEvaluationCompleted { session_id, .. }
            | AgentEvent::GoalStatusChanged { session_id, .. }
            | AgentEvent::PlanModeEntered { session_id, .. }
            | AgentEvent::PlanModeExited { session_id, .. }
            | AgentEvent::PlanFileUpdated { session_id, .. }
            | AgentEvent::RunnerProgress { session_id, .. }
            | AgentEvent::SessionTitleUpdated { session_id, .. }
            | AgentEvent::SessionPinnedUpdated { session_id, .. }
            | AgentEvent::SessionCreated { session_id, .. }
            | AgentEvent::SessionDeleted { session_id, .. }
            | AgentEvent::SessionCleared { session_id, .. }
            | AgentEvent::MessageAppended { session_id, .. }
            | AgentEvent::ExecutionStarted { session_id, .. }
            | AgentEvent::Notification { session_id, .. } => Some(session_id.as_str()),
            AgentEvent::SubAgentStarted {
                parent_session_id, ..
            }
            | AgentEvent::SubAgentEvent {
                parent_session_id, ..
            }
            | AgentEvent::SubAgentHeartbeat {
                parent_session_id, ..
            }
            | AgentEvent::SubAgentCompleted {
                parent_session_id, ..
            } => Some(parent_session_id.as_str()),
            _ => None,
        }
    }

    /// Whether this event belongs on the durable account change feed.
    ///
    /// Durable change events are low-volume, journaled to disk, and resumable
    /// via the account `/stream` feed. Ephemeral events — token-by-token
    /// streaming (`Token`/`ReasoningToken`/`ToolToken`), heartbeats, live
    /// budget/pressure gauges, and raw forwarded sub-agent events — return
    /// `false`: they stay exclusively on the per-session `/events/{id}` stream.
    /// Keeping them off the journal and the multiplexed feed is the core
    /// data-transfer win. This method lives in core so both the server and the
    /// engine forwarder can filter before cloning onto the feed.
    pub fn is_durable_change(&self) -> bool {
        matches!(
            self,
            AgentEvent::MessageAppended { .. }
                | AgentEvent::SessionCreated { .. }
                | AgentEvent::SessionDeleted { .. }
                | AgentEvent::SessionCleared { .. }
                | AgentEvent::SessionTitleUpdated { .. }
                | AgentEvent::SessionPinnedUpdated { .. }
                | AgentEvent::TaskListUpdated { .. }
                | AgentEvent::TaskListItemProgress { .. }
                | AgentEvent::TaskListCompleted { .. }
                | AgentEvent::TaskEvaluationCompleted { .. }
                | AgentEvent::PlanModeEntered { .. }
                | AgentEvent::PlanModeExited { .. }
                | AgentEvent::PlanFileUpdated { .. }
                | AgentEvent::SubAgentStarted { .. }
                | AgentEvent::SubAgentCompleted { .. }
                | AgentEvent::NeedClarification { .. }
                | AgentEvent::ToolApprovalRequested { .. }
                | AgentEvent::ExecutionStarted { .. }
                | AgentEvent::Complete { .. }
                | AgentEvent::Cancelled { .. }
                | AgentEvent::Error { .. }
        )
    }
}

fn default_allow_custom() -> bool {
    true
}

/// Gold evaluation checkpoint.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GoldCheckpoint {
    PostRound,
    Terminal,
}

impl GoldCheckpoint {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::PostRound => "post_round",
            Self::Terminal => "terminal",
        }
    }
}

/// Gold evaluator decision.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GoldDecision {
    Continue,
    Achieved,
    Blocked,
    NeedInput,
    Exhausted,
}

impl GoldDecision {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Continue => "continue",
            Self::Achieved => "achieved",
            Self::Blocked => "blocked",
            Self::NeedInput => "need_input",
            Self::Exhausted => "exhausted",
        }
    }
}

/// Confidence level for a Gold evaluation result.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GoldConfidence {
    Low,
    Medium,
    High,
}

impl GoldConfidence {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Low => "low",
            Self::Medium => "medium",
            Self::High => "high",
        }
    }

    /// Ordinal rank for threshold comparisons (`Low` < `Medium` < `High`).
    pub fn rank(self) -> u8 {
        match self {
            Self::Low => 0,
            Self::Medium => 1,
            Self::High => 2,
        }
    }

    /// Whether this confidence meets or exceeds the given floor.
    pub fn meets(self, floor: GoldConfidence) -> bool {
        self.rank() >= floor.rank()
    }
}

/// Source that triggered a session title update.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TitleSource {
    Auto,
    Manual,
    Fallback,
}

/// Re-exported shared token usage type.
///
/// See [`bamboo_domain::TokenUsage`] for the canonical definition.
pub use bamboo_domain::TokenUsage;

pub use bamboo_domain::budget_types::TokenBudgetUsage;

#[cfg(test)]
mod tests {
    use super::*;
    use bamboo_domain::{TaskItem, TaskItemStatus, TaskList};

    fn sample_task_list() -> TaskList {
        TaskList {
            session_id: "session-1".to_string(),
            title: "Task List".to_string(),
            items: vec![TaskItem {
                id: "task_1".to_string(),
                description: "Implement event rename".to_string(),
                status: TaskItemStatus::InProgress,
                depends_on: Vec::new(),
                notes: "Implementing".to_string(),
                ..TaskItem::default()
            }],
            created_at: Utc::now(),
            updated_at: Utc::now(),
        }
    }

    #[test]
    fn task_list_updated_serializes_with_task_names() {
        let event = AgentEvent::TaskListUpdated {
            task_list: sample_task_list(),
        };

        let value = serde_json::to_value(event).expect("event should serialize");
        assert_eq!(value["type"], "task_list_updated");
        assert!(value.get("task_list").is_some());
        assert!(value.get("todo_list").is_none());
    }

    #[test]
    fn cancelled_serializes_with_snake_case_type() {
        let event = AgentEvent::Cancelled {
            message: Some("Agent execution cancelled by user".to_string()),
        };

        let value = serde_json::to_value(event).expect("event should serialize");
        assert_eq!(value["type"], "cancelled");
        assert_eq!(
            value["message"],
            serde_json::Value::String("Agent execution cancelled by user".to_string())
        );
    }

    #[test]
    fn task_evaluation_completed_serializes_with_task_type() {
        let event = AgentEvent::TaskEvaluationCompleted {
            session_id: "session-1".to_string(),
            updates_count: 2,
            reasoning: "Updated statuses".to_string(),
        };

        let value = serde_json::to_value(event).expect("event should serialize");
        assert_eq!(value["type"], "task_evaluation_completed");
    }

    #[test]
    fn gold_evaluation_completed_serializes_with_gold_type_and_fields() {
        let event = AgentEvent::GoldEvaluationCompleted {
            session_id: "session-1".to_string(),
            checkpoint: GoldCheckpoint::PostRound,
            iteration: 3,
            decision: GoldDecision::Continue,
            confidence: GoldConfidence::Medium,
            reasoning: "Need one more iteration".to_string(),
        };

        let value = serde_json::to_value(event).expect("event should serialize");
        assert_eq!(value["type"], "gold_evaluation_completed");
        assert_eq!(value["checkpoint"], "post_round");
        assert_eq!(value["iteration"], 3);
        assert_eq!(value["decision"], "continue");
        assert_eq!(value["confidence"], "medium");
        assert_eq!(value["reasoning"], "Need one more iteration");
    }

    #[test]
    fn gold_evaluation_started_deserializes() {
        let json = serde_json::json!({
            "type": "gold_evaluation_started",
            "session_id": "session-1",
            "checkpoint": "terminal",
            "iteration": 7
        });

        let event: AgentEvent = serde_json::from_value(json).expect("should deserialize");
        match event {
            AgentEvent::GoldEvaluationStarted {
                session_id,
                checkpoint,
                iteration,
            } => {
                assert_eq!(session_id, "session-1");
                assert_eq!(checkpoint, GoldCheckpoint::Terminal);
                assert_eq!(iteration, 7);
            }
            other => panic!("unexpected event: {other:?}"),
        }
    }

    #[test]
    fn context_compression_status_serializes_with_phase_and_status() {
        let event = AgentEvent::ContextCompressionStatus {
            phase: "mid-turn".to_string(),
            status: "started".to_string(),
        };

        let value = serde_json::to_value(event).expect("event should serialize");
        assert_eq!(value["type"], "context_compression_status");
        assert_eq!(value["phase"], "mid-turn");
        assert_eq!(value["status"], "started");
    }

    #[test]
    fn need_clarification_serializes_with_new_fields() {
        let event = AgentEvent::NeedClarification {
            question: "Continue?".to_string(),
            options: Some(vec!["Yes".to_string(), "No".to_string()]),
            tool_call_id: Some("tool-1".to_string()),
            tool_name: Some("conclusion_with_options".to_string()),
            allow_custom: false,
        };

        let value = serde_json::to_value(event).expect("event should serialize");
        assert_eq!(value["type"], "need_clarification");
        assert_eq!(value["question"], "Continue?");
        assert_eq!(value["options"], serde_json::json!(["Yes", "No"]));
        assert_eq!(value["tool_call_id"], "tool-1");
        assert_eq!(value["tool_name"], "conclusion_with_options");
        assert_eq!(value["allow_custom"], false);
    }

    #[test]
    fn need_clarification_deserializes_from_old_format_without_new_fields() {
        let json = serde_json::json!({
            "type": "need_clarification",
            "question": "Continue?",
            "options": ["Yes", "No"]
        });

        let event: AgentEvent =
            serde_json::from_value(json).expect("should deserialize old format");
        match event {
            AgentEvent::NeedClarification {
                question,
                options,
                tool_call_id,
                tool_name,
                allow_custom,
            } => {
                assert_eq!(question, "Continue?");
                assert_eq!(options, Some(vec!["Yes".to_string(), "No".to_string()]));
                assert_eq!(tool_call_id, None);
                assert_eq!(tool_name, None);
                assert!(allow_custom); // default_allow_custom returns true
            }
            other => panic!("unexpected event: {other:?}"),
        }
    }

    #[test]
    fn need_clarification_deserializes_with_allow_custom_false() {
        let json = serde_json::json!({
            "type": "need_clarification",
            "question": "Pick one",
            "allow_custom": false
        });

        let event: AgentEvent = serde_json::from_value(json).expect("should deserialize");
        match event {
            AgentEvent::NeedClarification {
                question,
                options,
                tool_call_id,
                tool_name,
                allow_custom,
            } => {
                assert_eq!(question, "Pick one");
                assert_eq!(options, None);
                assert_eq!(tool_call_id, None);
                assert_eq!(tool_name, None);
                assert!(!allow_custom);
            }
            other => panic!("unexpected event: {other:?}"),
        }
    }

    #[test]
    fn plan_mode_entered_serializes_correctly() {
        let entered_at = Utc::now();
        let event = AgentEvent::PlanModeEntered {
            session_id: "sess-1".to_string(),
            reason: Some("Complex refactor".to_string()),
            pre_permission_mode: "default".to_string(),
            entered_at,
            status: bamboo_domain::PlanModeStatus::Exploring,
            plan_file_path: None,
        };

        let value = serde_json::to_value(event).expect("event should serialize");
        assert_eq!(value["type"], "plan_mode_entered");
        assert_eq!(value["session_id"], "sess-1");
        assert_eq!(value["reason"], "Complex refactor");
        assert_eq!(value["pre_permission_mode"], "default");
        assert_eq!(value["status"], "exploring");
        // Compare against serde's own serialization (RFC3339 with `Z` for UTC),
        // not `to_rfc3339()` which emits a `+00:00` offset instead.
        assert_eq!(
            value["entered_at"],
            serde_json::to_value(entered_at).unwrap()
        );
    }

    #[test]
    fn plan_mode_exited_serializes_correctly() {
        let event = AgentEvent::PlanModeExited {
            session_id: "sess-1".to_string(),
            approved: true,
            restored_mode: "accept_edits".to_string(),
            plan: Some("# Plan\n1. Step one".to_string()),
        };

        let value = serde_json::to_value(event).expect("event should serialize");
        assert_eq!(value["type"], "plan_mode_exited");
        assert_eq!(value["session_id"], "sess-1");
        assert_eq!(value["approved"], true);
        assert_eq!(value["restored_mode"], "accept_edits");
        assert_eq!(value["plan"], "# Plan\n1. Step one");
    }

    #[test]
    fn plan_file_updated_serializes_correctly() {
        let event = AgentEvent::PlanFileUpdated {
            session_id: "sess-1".to_string(),
            file_path: "/tmp/plans/sess-1.md".to_string(),
            content_summary: "Implementation plan for feature X".to_string(),
        };

        let value = serde_json::to_value(event).expect("event should serialize");
        assert_eq!(value["type"], "plan_file_updated");
        assert_eq!(value["session_id"], "sess-1");
        assert_eq!(value["file_path"], "/tmp/plans/sess-1.md");
        assert_eq!(
            value["content_summary"],
            "Implementation plan for feature X"
        );
    }

    #[test]
    fn tool_approval_requested_serializes_correctly() {
        let event = AgentEvent::ToolApprovalRequested {
            tool_call_id: "call-abc".to_string(),
            tool_name: "Write".to_string(),
            parameters: serde_json::json!({"file_path": "/tmp/test.txt"}),
        };

        let value = serde_json::to_value(event).expect("event should serialize");
        assert_eq!(value["type"], "tool_approval_requested");
        assert_eq!(value["tool_call_id"], "call-abc");
        assert_eq!(value["tool_name"], "Write");
        assert_eq!(
            value["parameters"],
            serde_json::json!({"file_path": "/tmp/test.txt"})
        );
    }

    #[test]
    fn tool_approval_requested_deserializes_correctly() {
        let json = serde_json::json!({
            "type": "tool_approval_requested",
            "tool_call_id": "call-xyz",
            "tool_name": "Bash",
            "parameters": {"command": "ls -la"}
        });

        let event: AgentEvent = serde_json::from_value(json).expect("should deserialize");
        match event {
            AgentEvent::ToolApprovalRequested {
                tool_call_id,
                tool_name,
                parameters,
            } => {
                assert_eq!(tool_call_id, "call-xyz");
                assert_eq!(tool_name, "Bash");
                assert_eq!(parameters, serde_json::json!({"command": "ls -la"}));
            }
            other => panic!("unexpected event: {other:?}"),
        }
    }

    #[test]
    fn session_title_updated_round_trips_with_source_variants() {
        use chrono::Utc;
        let event = AgentEvent::SessionTitleUpdated {
            session_id: "sess-1".to_string(),
            title: "My title".to_string(),
            title_version: 3,
            source: TitleSource::Auto,
            updated_at: Utc::now(),
        };
        let json = serde_json::to_string(&event).unwrap();
        assert!(
            json.contains("\"type\":\"session_title_updated\""),
            "json: {json}"
        );
        assert!(json.contains("\"source\":\"auto\""), "json: {json}");
        let _decoded: AgentEvent = serde_json::from_str(&json).unwrap();
    }

    #[test]
    fn plan_mode_events_deserialize_without_optional_fields() {
        let json = serde_json::json!({
            "type": "plan_mode_entered",
            "session_id": "sess-1",
            "pre_permission_mode": "default",
            "entered_at": "2025-01-01T00:00:00Z",
            "status": "exploring"
        });

        let event: AgentEvent = serde_json::from_value(json).expect("should deserialize");
        match event {
            AgentEvent::PlanModeEntered {
                session_id,
                reason,
                pre_permission_mode,
                entered_at,
                status,
                plan_file_path,
            } => {
                assert_eq!(session_id, "sess-1");
                assert_eq!(reason, None);
                assert_eq!(pre_permission_mode, "default");
                assert_eq!(entered_at.to_rfc3339(), "2025-01-01T00:00:00+00:00");
                assert_eq!(status, bamboo_domain::PlanModeStatus::Exploring);
                assert_eq!(plan_file_path, None);
            }
            other => panic!("unexpected event: {other:?}"),
        }
    }
}
