use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::{Mutex, oneshot};
use tokio_util::sync::CancellationToken;

use crate::Result;
use crate::permission::{PermissionMode, PermissionPolicy};

pub const BASH_TOOL_NAME: &str = "Bash";
pub const READ_TOOL_NAME: &str = "Read";
pub const WRITE_TOOL_NAME: &str = "Write";
pub const EDIT_TOOL_NAME: &str = "Edit";
pub const GLOB_TOOL_NAME: &str = "Glob";
pub const GREP_TOOL_NAME: &str = "Grep";

/// Tool source classification — determines the permission matrix column.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ToolSource {
    /// Built-in tools (Bash/Read/Write/Edit/Glob/Grep etc.)
    BuiltIn,
    /// External MCP server tools (untrusted source, Default/TrustProject require Prompt)
    McpExternal {
        /// Name of the MCP server that provides this tool.
        server_name: String,
    },
    /// Sub-agent spawned tools (`TrustProject` auto-allows)
    AgentSpawn,
}

/// Label used when multiple read-only tool calls are collapsed into a
/// single summary line (e.g. "Read 3 files, searched 2 patterns").
///
/// Tools override `Tool::collapsed_group_label()` to provide this.
/// The TUI groups calls by label and renders counts per group — it never
/// matches on tool names directly.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CollapsedGroupLabel {
    /// Active-tense verb shown when results are still pending, capitalized
    /// for sentence start (e.g. "Reading", "Searching for").
    pub active_verb: &'static str,
    /// Past-tense verb shown after all results arrive, capitalized
    /// (e.g. "Read", "Searched for").
    pub past_verb: &'static str,
    /// Singular noun (e.g. "file", "pattern", "notebook").
    pub noun_singular: &'static str,
    /// Plural noun (e.g. "files", "patterns", "notebooks").
    pub noun_plural: &'static str,
}

/// How a tool responds to an interrupt (Ctrl+C / cancel signal).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InterruptBehavior {
    /// Stop immediately when cancelled (default for most tools).
    Cancel,
    /// Let current work finish before acknowledging the interrupt.
    /// Used for tools where partial execution would leave inconsistent state
    /// (e.g. a multi-step file operation).
    Block,
}

/// Core trait for all tools — built-in, MCP-bridged, and plugin-provided.
///
/// Returns `Pin<Box<dyn Future>>` (not `async fn`) to guarantee object safety
/// when stored as `Arc<dyn Tool>`.
pub trait Tool: Send + Sync {
    /// Unique tool identifier (e.g. "bash", "read", `mcp__server__tool`).
    fn name(&self) -> &str;

    /// Human-readable description (included in the system prompt).
    fn description(&self) -> &str;

    /// JSON Schema describing the input parameters.
    fn input_schema(&self) -> Value;

    /// Execute the tool with the given input and context.
    ///
    /// Long-running tools should periodically check `ctx.cancellation_token`
    /// and return early when cancelled.
    fn execute(
        &self,
        input: Value,
        ctx: &ToolContext,
    ) -> Pin<Box<dyn Future<Output = Result<ToolOutput>> + Send + '_>>;

    /// Tool source (default: `BuiltIn`) — affects the permission check matrix.
    fn source(&self) -> ToolSource {
        ToolSource::BuiltIn
    }

    /// Whether user confirmation is required before execution (default: false).
    fn requires_confirmation(&self) -> bool {
        false
    }

    /// Whether this tool is read-only (read-only tools can skip confirmation).
    fn is_read_only(&self) -> bool {
        false
    }

    /// Whether this tool can safely run concurrently with other tools given specific input.
    ///
    /// The default delegates to `is_read_only()`. Tools that are normally write
    /// tools but operate on independent resources (e.g. writing to different files)
    /// can override this to inspect the input and return `true`.
    fn is_concurrency_safe(&self, _input: &Value) -> bool {
        self.is_read_only()
    }

    /// How this tool responds to an interrupt signal (default: `Cancel`).
    ///
    /// `Cancel` — stop immediately when `cancellation_token` fires.
    /// `Block`  — finish current work, then acknowledge the interrupt.
    fn interrupt_behavior(&self) -> InterruptBehavior {
        InterruptBehavior::Cancel
    }

    /// Whether an in-flight invocation can be safely aborted mid-run.
    ///
    /// True when `interrupt_behavior()` is `Cancel`. The TUI uses this to
    /// decide whether pressing Enter during a running turn should abort the
    /// turn and submit the queued message immediately, rather than only
    /// queueing it until the turn finishes on its own.
    fn is_interruptible(&self) -> bool {
        self.interrupt_behavior() == InterruptBehavior::Cancel
    }

    // ── Rendering hooks ─────────────────────────────────────────────
    //
    // All have default implementations so existing tools don't break.
    // Override in individual tool impls for customized TUI display.

    /// One-line summary shown when the tool is invoked.
    ///
    /// E.g. `BashTool` → `"$ ls -la"`, `ReadTool` → `"src/main.rs:1-50"`.
    /// Returns `None` to use the default `"● {tool_name}"`.
    fn format_use_summary(&self, _input: &Value) -> Option<String> {
        None
    }

    /// Custom result formatting for TUI display.
    ///
    /// Returns `None` to use the default plain-text rendering (10-line truncation).
    fn format_result(&self, _output: &ToolOutput) -> Option<ToolDisplayResult> {
        None
    }

    /// Whether the result should be collapsible in the TUI.
    ///
    /// Default: collapse when output exceeds 5 lines.
    fn is_result_collapsible(&self, output: &ToolOutput) -> bool {
        output.text().lines().count() > 5
    }

    /// Summary shown when the tool invocation was rejected by the user.
    ///
    /// E.g. `EditTool` → `"Edit rejected: src/main.rs"`.
    fn format_rejected_summary(&self, _input: &Value) -> Option<String> {
        None
    }

    /// Rich rejection rendering with multi-line preview of what was rejected.
    ///
    /// Returns `None` to fall back to the single-line `format_rejected_summary`.
    fn format_rejected(&self, _input: &Value) -> Option<ToolDisplayResult> {
        None
    }

    /// Whether this tool supports real-time streaming progress display.
    ///
    /// E.g. `BashTool` can stream stdout as it runs.
    fn supports_streaming_progress(&self) -> bool {
        false
    }

    /// Specialized error rendering with contextual hints.
    ///
    /// Called when `output.is_error` is true. Receives the original `input`
    /// so the error message can reference what was attempted (e.g. which
    /// file was not found). Returns `None` to fall back to the default
    /// red-text rendering.
    fn format_error(&self, _output: &ToolOutput, _input: &Value) -> Option<ToolDisplayResult> {
        None
    }

    /// Color hint for the tool-call header icon (the `●` glyph).
    ///
    /// The TUI maps this to a terminal color for the tool icon, giving
    /// each tool category a distinct visual identity.
    fn display_color(&self) -> ToolDisplayStyle {
        ToolDisplayStyle::Normal
    }

    /// Maximum result size in characters before disk persistence kicks in.
    /// Override for tools that need larger results (e.g. `Read` returns `usize::MAX`).
    fn max_result_chars(&self) -> usize {
        100_000
    }

    /// Label for collapsed read-only run summaries.
    ///
    /// When multiple read-only tools fire in parallel, the TUI collapses
    /// them into a single line like "Read 3 files, searched 2 patterns".
    /// This method provides the verb/noun forms for the summary. Return
    /// `None` to fall into a generic "N other calls" bucket.
    fn collapsed_group_label(&self) -> Option<CollapsedGroupLabel> {
        None
    }
}

/// Tool-customized rendering output for TUI display.
#[derive(Debug, Clone)]
pub struct ToolDisplayResult {
    /// Styled lines to render.
    pub lines: Vec<ToolDisplayLine>,
    /// Number of lines to show when collapsed (default 3).
    pub preview_lines: usize,
}

/// A single styled line in a tool display result.
#[derive(Debug, Clone)]
pub struct ToolDisplayLine {
    /// The text content.
    pub text: String,
    /// Optional display style. `None` uses the default style.
    pub style: Option<ToolDisplayStyle>,
}

impl ToolDisplayLine {
    /// Create a new line with the given text and style.
    pub fn new(text: impl Into<String>, style: ToolDisplayStyle) -> Self {
        Self {
            text: text.into(),
            style: Some(style),
        }
    }

    /// Create a new line with default styling.
    pub fn plain(text: impl Into<String>) -> Self {
        Self {
            text: text.into(),
            style: None,
        }
    }
}

/// Display style hint for tool output lines.
///
/// The TUI maps these to actual terminal colors/attributes.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolDisplayStyle {
    /// Normal text.
    Normal,
    /// Error text (typically red).
    Error,
    /// Diff addition (typically green).
    DiffAdd,
    /// Diff removal (typically red).
    DiffRemove,
    /// Diff context (unchanged lines).
    DiffContext,
    /// Muted / de-emphasized text (typically dim gray).
    Muted,
    /// Highlighted / emphasized text.
    Highlight,
}

/// Real-time progress data emitted by long-running tools (e.g. Bash).
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ToolProgress {
    pub elapsed_secs: f64,
    pub total_lines: usize,
    pub total_bytes: usize,
    /// Last few lines of output for preview.
    pub tail_output: String,
    pub timeout_secs: Option<u64>,
}

/// Execution context passed to every tool invocation.
#[derive(Debug, Clone)]
pub struct ToolContext {
    /// Current working directory for the tool.
    pub working_dir: PathBuf,
    /// Permission mode in effect.
    pub permission_mode: PermissionMode,
    /// Session identifier.
    pub session_id: String,
    /// Cancellation token — long-running tools (e.g. Bash) should check this
    /// periodically and exit early when triggered.
    pub cancellation_token: CancellationToken,
    /// Permission policy (merged result from all config layers).
    pub permission_policy: PermissionPolicy,
    /// Extended context for tools that need richer application state.
    pub ext: ToolContextExt,
    /// Background task registry — shared with the runtime. `None` when
    /// no runtime is attached (e.g. unit tests).
    #[doc(hidden)]
    pub task_registry: Option<Arc<std::sync::Mutex<crate::task::TaskRegistry>>>,
    /// Paths of files read/written during tool execution. The engine loop
    /// checks this after each tool batch and injects nested AGENTS.md content
    /// for directories between CWD and the accessed files.
    pub nested_memory_triggers: Arc<Mutex<HashSet<PathBuf>>>,
}

/// File-snapshot callback for Edit/Write tools.
///
/// Wired to the session-scoped file-history store by the agent runtime;
/// receives the absolute file path and the current on-disk contents.
pub type TrackEditFn = Arc<dyn Fn(&Path, &[u8]) + Send + Sync>;

/// A recorded read of a file, used to gate edits on a prior read.
#[derive(Clone, Debug)]
pub struct ReadRecord {
    /// File modification time at read time, if it could be stat'd. Used to
    /// detect a change between the read and a subsequent edit.
    pub mtime: Option<std::time::SystemTime>,
}

/// Records that a file was read (Read tool, and after a successful Write/Edit).
pub type RecordReadFn = Arc<dyn Fn(&Path, ReadRecord) + Send + Sync>;

/// Looks up the last recorded read of a file, used by Edit/Write to enforce
/// read-before-edit.
pub type ReadStateFn = Arc<dyn Fn(&Path) -> Option<ReadRecord> + Send + Sync>;

/// Canonical key for read-state tracking: the canonicalized path, falling back
/// to the path as-given when it cannot be resolved (e.g. a not-yet-created
/// file). Record and lookup must use the same key.
#[must_use]
pub fn canonical_read_key(path: &Path) -> PathBuf {
    path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
}

/// Build a fresh session-scoped read-state tracker, returning the
/// `(record, lookup)` closure pair to wire into [`ToolContextExt`]. Both share
/// one in-memory store keyed by canonicalized path.
#[must_use]
pub fn new_read_state_tracker() -> (RecordReadFn, ReadStateFn) {
    use std::collections::HashMap;
    use std::sync::Mutex;

    let store: Arc<Mutex<HashMap<PathBuf, ReadRecord>>> = Arc::new(Mutex::new(HashMap::new()));
    let record: RecordReadFn = {
        let store = Arc::clone(&store);
        Arc::new(move |path: &Path, record: ReadRecord| {
            if let Ok(mut map) = store.lock() {
                map.insert(canonical_read_key(path), record);
            }
        })
    };
    let lookup: ReadStateFn = {
        let store = Arc::clone(&store);
        Arc::new(move |path: &Path| {
            store
                .lock()
                .ok()
                .and_then(|map| map.get(&canonical_read_key(path)).cloned())
        })
    };
    (record, lookup)
}

/// Channel map type for pending `AskUserQuestion` prompts.
pub type UserPromptChannels = Arc<Mutex<HashMap<String, oneshot::Sender<String>>>>;

/// Extended tool context fields — populated by the agent loop when available.
///
/// These fields are optional enrichment data. Tools that need them should
/// check and degrade gracefully if they are empty.
#[derive(Clone, Default)]
pub struct ToolContextExt {
    /// Pre-rendered tool name+description pairs for `ToolSearch`.
    pub tool_descriptions: Vec<String>,
    /// Recent conversation summary for `BriefTool`.
    pub conversation_summary: Option<String>,
    /// Names of connected MCP servers for `McpAuth`/`McpResource`.
    pub mcp_server_names: Vec<String>,
    /// Snapshot a file before editing. Called by Edit/Write tools with the
    /// absolute path and current file contents; `None` disables tracking.
    pub track_edit: Option<TrackEditFn>,
    /// Record that a file was read. Called by the Read tool (and after a
    /// successful Write/Edit); `None` disables read-before-edit tracking.
    pub record_read: Option<RecordReadFn>,
    /// Look up the last recorded read of a file. Used by Edit/Write to gate on
    /// a prior read; `None` disables the gate (edits proceed unchecked).
    pub read_state: Option<ReadStateFn>,
    /// Shared map of pending user-prompt channels, keyed by request ID.
    ///
    /// `AskUserQuestionTool` registers a `oneshot::Sender` here so the UI
    /// event loop can deliver the user's response. `None` disables the
    /// ask-user flow (headless / unit-test mode).
    pub user_prompt_channels: Option<UserPromptChannels>,
}

impl std::fmt::Debug for ToolContextExt {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ToolContextExt")
            .field("tool_descriptions", &self.tool_descriptions)
            .field("conversation_summary", &self.conversation_summary)
            .field("mcp_server_names", &self.mcp_server_names)
            .field("track_edit", &self.track_edit.as_ref().map(|_| "<fn>"))
            .field("record_read", &self.record_read.as_ref().map(|_| "<fn>"))
            .field("read_state", &self.read_state.as_ref().map(|_| "<fn>"))
            .field(
                "user_prompt_channels",
                &self.user_prompt_channels.as_ref().map(|_| "<channels>"),
            )
            .finish()
    }
}

/// A single content block within a tool output.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ToolOutputContent {
    /// Plain text output.
    Text { text: String },
    /// Base64-encoded image output.
    Image { media_type: String, data: String },
    /// Structured JSON output.
    Json { value: Value },
}

/// Result of a tool execution.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolOutput {
    /// Output content blocks.
    pub content: Vec<ToolOutputContent>,
    /// Whether this output represents an error.
    pub is_error: bool,
}

impl ToolOutput {
    /// Create a successful text output.
    pub fn success(text: impl Into<String>) -> Self {
        Self {
            content: vec![ToolOutputContent::Text { text: text.into() }],
            is_error: false,
        }
    }

    /// Create an error text output.
    pub fn error(text: impl Into<String>) -> Self {
        Self {
            content: vec![ToolOutputContent::Text { text: text.into() }],
            is_error: true,
        }
    }

    /// Create an output with multiple content blocks.
    pub fn with_content(content: Vec<ToolOutputContent>, is_error: bool) -> Self {
        Self { content, is_error }
    }

    /// Get the text content, concatenating all text blocks.
    #[must_use]
    pub fn text(&self) -> String {
        self.content
            .iter()
            .filter_map(|c| match c {
                ToolOutputContent::Text { text } => Some(text.as_str()),
                ToolOutputContent::Image { .. } | ToolOutputContent::Json { .. } => None,
            })
            .collect::<Vec<_>>()
            .join("")
    }
}

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

    #[test]
    fn tool_output_success() {
        let out = ToolOutput::success("hello");
        assert!(!out.is_error);
        assert_eq!(out.text(), "hello");
    }

    #[test]
    fn tool_output_error() {
        let out = ToolOutput::error("oops");
        assert!(out.is_error);
        assert_eq!(out.text(), "oops");
    }

    #[test]
    fn tool_output_text_concatenates() {
        let out = ToolOutput::with_content(
            vec![
                ToolOutputContent::Text { text: "a".into() },
                ToolOutputContent::Json { value: json!(42) },
                ToolOutputContent::Text { text: "b".into() },
            ],
            false,
        );
        assert_eq!(out.text(), "ab");
    }

    #[test]
    fn tool_output_empty_content() {
        let out = ToolOutput::with_content(vec![], false);
        assert_eq!(out.text(), "");
        assert!(!out.is_error);
    }

    #[test]
    fn tool_output_serde_roundtrip() {
        let out = ToolOutput::success("test output");
        let json = serde_json::to_string(&out).unwrap();
        let parsed: ToolOutput = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.text(), "test output");
        assert!(!parsed.is_error);
    }

    #[test]
    fn tool_output_content_image() {
        let out = ToolOutput::with_content(
            vec![ToolOutputContent::Image {
                media_type: "image/png".into(),
                data: "base64data".into(),
            }],
            false,
        );
        // Image blocks don't contribute to text()
        assert_eq!(out.text(), "");
    }

    #[test]
    fn tool_source_default_is_builtin() {
        // Verify ToolSource variants construct correctly
        let src = ToolSource::BuiltIn;
        assert!(matches!(src, ToolSource::BuiltIn));

        let mcp = ToolSource::McpExternal {
            server_name: "playwright".into(),
        };
        assert!(matches!(mcp, ToolSource::McpExternal { .. }));
    }

    #[test]
    fn tool_source_serde_roundtrip() {
        let src = ToolSource::McpExternal {
            server_name: "test-server".into(),
        };
        let json = serde_json::to_string(&src).unwrap();
        let parsed: ToolSource = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed, src);
    }

    // ─── Additional tests ───

    #[test]
    fn tool_source_all_variants_serde() {
        let variants = vec![
            ToolSource::BuiltIn,
            ToolSource::McpExternal {
                server_name: "playwright".into(),
            },
            ToolSource::AgentSpawn,
        ];
        for src in variants {
            let json = serde_json::to_string(&src).unwrap();
            let parsed: ToolSource = serde_json::from_str(&json).unwrap();
            assert_eq!(parsed, src);
        }
    }

    #[test]
    fn tool_output_content_text_serde() {
        let content = ToolOutputContent::Text {
            text: "hello".into(),
        };
        let json = serde_json::to_string(&content).unwrap();
        let parsed: ToolOutputContent = serde_json::from_str(&json).unwrap();
        assert!(matches!(parsed, ToolOutputContent::Text { text } if text == "hello"));
    }

    #[test]
    fn tool_output_content_image_serde() {
        let content = ToolOutputContent::Image {
            media_type: "image/png".into(),
            data: "base64data".into(),
        };
        let json = serde_json::to_string(&content).unwrap();
        let parsed: ToolOutputContent = serde_json::from_str(&json).unwrap();
        assert!(
            matches!(parsed, ToolOutputContent::Image { media_type, data }
                if media_type == "image/png" && data == "base64data"
            )
        );
    }

    #[test]
    fn tool_output_content_json_serde() {
        let content = ToolOutputContent::Json {
            value: json!({"key": "value", "count": 42}),
        };
        let json_str = serde_json::to_string(&content).unwrap();
        let parsed: ToolOutputContent = serde_json::from_str(&json_str).unwrap();
        assert!(matches!(parsed, ToolOutputContent::Json { value } if value["key"] == "value"));
    }

    #[test]
    fn tool_output_success_is_not_error() {
        let out = ToolOutput::success("ok");
        assert!(!out.is_error);
        assert_eq!(out.content.len(), 1);
    }

    #[test]
    fn tool_output_error_is_error() {
        let out = ToolOutput::error("fail");
        assert!(out.is_error);
        assert_eq!(out.content.len(), 1);
    }

    #[test]
    fn tool_output_multi_content_serde_roundtrip() {
        let out = ToolOutput::with_content(
            vec![
                ToolOutputContent::Text {
                    text: "header".into(),
                },
                ToolOutputContent::Image {
                    media_type: "image/jpeg".into(),
                    data: "abc123".into(),
                },
                ToolOutputContent::Json {
                    value: json!({"status": "ok"}),
                },
            ],
            false,
        );
        let json = serde_json::to_string(&out).unwrap();
        let parsed: ToolOutput = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.content.len(), 3);
        assert!(!parsed.is_error);
        assert_eq!(parsed.text(), "header");
    }

    /// Mock tool for testing the Tool trait interface.
    struct MockTool {
        name: &'static str,
        read_only: bool,
    }

    impl Tool for MockTool {
        fn name(&self) -> &str {
            self.name
        }

        fn description(&self) -> &'static str {
            "A mock tool for testing"
        }

        fn input_schema(&self) -> Value {
            json!({
                "type": "object",
                "properties": {
                    "arg": {"type": "string"}
                },
                "required": ["arg"]
            })
        }

        fn execute(
            &self,
            input: Value,
            _ctx: &ToolContext,
        ) -> Pin<Box<dyn Future<Output = Result<ToolOutput>> + Send + '_>> {
            let text = input["arg"].as_str().unwrap_or("no arg").to_owned();
            Box::pin(async move { Ok(ToolOutput::success(text)) })
        }

        fn is_read_only(&self) -> bool {
            self.read_only
        }
    }

    #[test]
    fn mock_tool_name_and_description() {
        let tool = MockTool {
            name: "mock_read",
            read_only: true,
        };
        assert_eq!(tool.name(), "mock_read");
        assert_eq!(tool.description(), "A mock tool for testing");
        assert!(tool.is_read_only());
        assert!(tool.is_concurrency_safe(&json!({})));
        assert!(!tool.requires_confirmation());
        assert!(matches!(tool.source(), ToolSource::BuiltIn));
        assert_eq!(tool.interrupt_behavior(), InterruptBehavior::Cancel);
        assert!(tool.is_interruptible());
    }

    #[test]
    fn mock_tool_input_schema_is_valid_json() {
        let tool = MockTool {
            name: "test",
            read_only: false,
        };
        let schema = tool.input_schema();
        assert_eq!(schema["type"], "object");
        assert!(schema["properties"]["arg"].is_object());
        assert!(schema["required"].is_array());
    }

    #[test]
    fn mock_tool_is_object_safe() {
        // Verify Tool can be used as a trait object
        let tool: Box<dyn Tool> = Box::new(MockTool {
            name: "boxed",
            read_only: false,
        });
        assert_eq!(tool.name(), "boxed");
        assert!(!tool.is_read_only());
        assert!(!tool.is_concurrency_safe(&json!({})));
    }

    #[test]
    fn tool_context_construction() {
        let ctx = ToolContext {
            working_dir: std::path::PathBuf::from("/tmp"),
            permission_mode: PermissionMode::Default,
            session_id: "sess_123".into(),
            cancellation_token: CancellationToken::new(),
            permission_policy: PermissionPolicy::default(),
            ext: ToolContextExt::default(),
            task_registry: None,
            nested_memory_triggers: Arc::new(Mutex::new(HashSet::new())),
        };
        assert_eq!(ctx.working_dir, std::path::Path::new("/tmp"));
        assert_eq!(ctx.permission_mode, PermissionMode::Default);
        assert_eq!(ctx.session_id, "sess_123");
    }
}
