//! RagPipeline - 串联完整的 RAG 对话式查询流程
//!
//! Orchestrates: Embed -> Retrieve -> Rerank -> Assemble -> Generate
//!
//! # Usage
//!
//! ```ignore
//! use aifastdb_core::rag::{RagPipeline, RagConfig};
//!
//! let pipeline = RagPipeline::builder()
//!     .retriever(my_retriever)
//!     .assembler(my_assembler)
//!     .generator(my_generator)
//!     .embedder(my_embedder)
//!     .session_manager(my_sessions)
//!     .config(RagConfig::default())
//!     .build()?;
//!
//! let response = pipeline.chat("session-1", "What is Rust?")?;
//! ```

use std::sync::Arc;
use std::time::Instant;

use super::assembler::DefaultContextAssembler;
use super::error::{RagError, RagResult};
use super::session::InMemorySessionManager;
use super::traits::{ContextAssembler, Generator, Retriever, SessionManager};
use super::types::{
    ChatMessage, RagConfig, RagResponse, RagTokenUsage, RetrievalResult, SourceReference,
};

// ============================================================================
// RagPipeline
// ============================================================================

/// The main RAG pipeline that orchestrates the full conversational query flow.
///
/// Connects all components: embedding, retrieval, context assembly, generation,
/// and session management into a single `chat()` entry point.
pub struct RagPipeline {
    /// Embedding function (query text -> Vec<f32>)
    embedder: Option<Arc<dyn crate::perception::PerceptionEngine>>,
    /// Retriever (vector + text search)
    retriever: Box<dyn Retriever>,
    /// Context assembler
    assembler: Box<dyn ContextAssembler>,
    /// LLM generator
    generator: Box<dyn Generator>,
    /// Session manager (Phase-82 T82.4: now trait-based, supports ConversationManager backend)
    sessions: Arc<dyn SessionManager>,
    /// Pipeline configuration
    config: RagConfig,
    /// Stream generator for streaming responses (Phase-82: T82.2).
    /// Only available when `llm-gateway` feature is enabled.
    #[cfg(feature = "llm-gateway")]
    stream_generator: Option<Box<dyn super::traits::StreamGenerator>>,
}

// Manual Debug impl because trait objects don't auto-derive Debug
impl std::fmt::Debug for RagPipeline {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let mut s = f.debug_struct("RagPipeline");
        s.field("has_embedder", &self.embedder.is_some())
            .field("config", &self.config);
        #[cfg(feature = "llm-gateway")]
        s.field("has_stream_generator", &self.stream_generator.is_some());
        s.finish_non_exhaustive()
    }
}

impl RagPipeline {
    /// Create a builder for constructing a RagPipeline.
    pub fn builder() -> RagPipelineBuilder {
        RagPipelineBuilder::default()
    }

    /// Process a user query in a conversation session.
    ///
    /// This is the main entry point for RAG chat:
    /// 1. Get or create session
    /// 2. Embed the query (if embedder available)
    /// 3. Retrieve relevant documents
    /// 4. Assemble context with history
    /// 5. Generate answer via LLM
    /// 6. Update session history
    /// 7. Return response with sources
    pub fn chat(&self, session_id: &str, question: &str) -> RagResult<RagResponse> {
        let start = Instant::now();

        // 1. Get or create session
        let _session = self.sessions.get_or_create(session_id)?;

        // 2. Embed the query
        let embedding = if let Some(ref embedder) = self.embedder {
            match embedder.conceptualize(question) {
                Ok(emb) => Some(emb),
                Err(e) => {
                    tracing::warn!("Embedding failed, falling back to text-only: {}", e);
                    None
                }
            }
        } else {
            None
        };

        // 3. Retrieve relevant documents
        let retrieved = self.retriever.retrieve(
            question,
            embedding.as_deref(),
            &self.config,
        )?;

        // 4. Get recent history
        let history = self
            .sessions
            .get_recent_history(session_id, self.config.max_history_turns)
            .unwrap_or_default();

        // 5. Assemble context
        let (messages, context_tokens) =
            self.assembler
                .assemble(question, &retrieved, &history, &self.config)?;

        // 6. Generate answer
        let mut response = self.generator.generate(&messages, &self.config)?;

        // 7. Enrich response
        response.retrieval_count = retrieved.len();
        response.session_id = Some(session_id.to_string());
        response.latency_ms = Some(start.elapsed().as_millis() as u64);

        // Add source references
        if self.config.include_sources {
            response.sources = retrieved.iter().map(SourceReference::from).collect();
        }

        // Update context tokens in usage
        if let Some(ref mut usage) = response.usage {
            usage.context_tokens = context_tokens;
        }

        // 8. Update session history
        self.sessions
            .push_message(session_id, ChatMessage::user(question))?;
        self.sessions
            .push_message(session_id, ChatMessage::assistant(&response.answer))?;

        Ok(response)
    }

    /// One-shot query without session management (stateless).
    pub fn query(&self, question: &str) -> RagResult<RagResponse> {
        let start = Instant::now();

        // Embed
        let embedding = if let Some(ref embedder) = self.embedder {
            embedder.conceptualize(question).ok()
        } else {
            None
        };

        // Retrieve
        let retrieved =
            self.retriever
                .retrieve(question, embedding.as_deref(), &self.config)?;

        // Assemble (no history for one-shot)
        let (messages, context_tokens) =
            self.assembler
                .assemble(question, &retrieved, &[], &self.config)?;

        // Generate
        let mut response = self.generator.generate(&messages, &self.config)?;

        // Enrich
        response.retrieval_count = retrieved.len();
        response.latency_ms = Some(start.elapsed().as_millis() as u64);

        if self.config.include_sources {
            response.sources = retrieved.iter().map(SourceReference::from).collect();
        }

        if let Some(ref mut usage) = response.usage {
            usage.context_tokens = context_tokens;
        }

        Ok(response)
    }

    /// Get the session manager (for external session operations).
    ///
    /// Phase-82 T82.4: Now returns `&dyn SessionManager` instead of
    /// `&InMemorySessionManager`. Use `downcast_ref` if you need the
    /// concrete type.
    pub fn sessions(&self) -> &dyn SessionManager {
        &*self.sessions
    }

    /// Get the current configuration.
    pub fn config(&self) -> &RagConfig {
        &self.config
    }

    /// Update the configuration.
    pub fn set_config(&mut self, config: RagConfig) {
        self.config = config;
    }

    /// Process a user query and report token/cost to `ConversationManager`.
    ///
    /// This is the **unified chat entry point** (Phase-82: T82.3) that:
    /// 1. Runs the standard RAG `chat()` flow
    /// 2. Creates a `StoredMessage` from the `RagResponse`
    /// 3. Feeds it to `ConversationManager::add_message()` for centralized tracking
    ///
    /// This ensures that **all** RAG pipeline calls contribute to the user's
    /// aggregated token usage and cost statistics in `ConversationManager`.
    ///
    /// # Arguments
    /// * `session_id` - RAG session ID (for history management)
    /// * `question` - User's question
    /// * `conv_id` - LLM Gateway conversation ID (for cost tracking)
    /// * `conv_manager` - Reference to the ConversationManager
    ///
    /// # Requires
    /// - `llm-gateway` feature enabled
    #[cfg(feature = "llm-gateway")]
    pub fn chat_with_tracking(
        &self,
        session_id: &str,
        question: &str,
        conv_id: &str,
        conv_manager: &crate::llm::conversation_manager::ConversationManager,
    ) -> RagResult<RagResponse> {
        // Run normal RAG chat
        let response = self.chat(session_id, question)?;

        // Store user message in ConversationManager
        let user_msg = crate::llm::conversation::StoredMessage::user(conv_id, question);
        if let Err(e) = conv_manager.add_message(user_msg) {
            tracing::warn!(
                conv_id = %conv_id,
                error = %e,
                "Failed to track user message in ConversationManager"
            );
        }

        // Convert RAG response → StoredMessage and feed to ConversationManager
        let assistant_msg =
            crate::llm::conversation::StoredMessage::from_rag_response(conv_id, &response);
        if let Err(e) = conv_manager.add_message(assistant_msg) {
            tracing::warn!(
                conv_id = %conv_id,
                error = %e,
                "Failed to track RAG response in ConversationManager"
            );
        }

        Ok(response)
    }

    /// Process a user query with streaming response (Phase-82: T82.2).
    ///
    /// Like [`chat()`](Self::chat), but returns a `Stream<StreamEvent>` instead
    /// of a complete `RagResponse`. The stream emits events in this order:
    ///
    /// 1. `RagContext { sources, relevance_scores }` — retrieval results
    /// 2. `TextDelta { content }` / `ThinkingDelta { content }` — LLM output
    /// 3. `Usage(TokenUsage)` — token counts (emitted by the LLM provider)
    /// 4. `Done { message_id, finish_reason }` — completion signal
    ///
    /// Session history is automatically updated after the stream finishes.
    ///
    /// # Requires
    ///
    /// - `llm-gateway` feature enabled
    /// - `stream_generator` configured via [`RagPipelineBuilder::stream_generator()`]
    ///
    /// # Example
    ///
    /// ```ignore
    /// use tokio_stream::StreamExt;
    ///
    /// let mut stream = pipeline.chat_stream("session-1", "What is Rust?").await?;
    /// while let Some(event) = stream.next().await {
    ///     match event? {
    ///         StreamEvent::RagContext { sources, .. } => { /* show sources */ }
    ///         StreamEvent::TextDelta { content } => print!("{}", content),
    ///         StreamEvent::Done { .. } => break,
    ///         _ => {}
    ///     }
    /// }
    /// ```
    #[cfg(feature = "llm-gateway")]
    pub async fn chat_stream(
        &self,
        session_id: &str,
        question: &str,
    ) -> RagResult<
        std::pin::Pin<
            Box<
                dyn futures_core::Stream<
                        Item = RagResult<crate::llm::types::StreamEvent>,
                    > + Send,
            >,
        >,
    > {
        use crate::llm::types::StreamEvent;

        // 1. Get or create session
        let _session = self.sessions.get_or_create(session_id)?;

        // 2. Embed the query
        let embedding = if let Some(ref embedder) = self.embedder {
            match embedder.conceptualize(question) {
                Ok(emb) => Some(emb),
                Err(e) => {
                    tracing::warn!("Embedding failed, falling back to text-only: {}", e);
                    None
                }
            }
        } else {
            None
        };

        // 3. Retrieve relevant documents
        let retrieved = self.retriever.retrieve(
            question,
            embedding.as_deref(),
            &self.config,
        )?;

        // 4. Get recent history
        let history = self
            .sessions
            .get_recent_history(session_id, self.config.max_history_turns)
            .unwrap_or_default();

        // 5. Assemble context
        let (messages, _context_tokens) =
            self.assembler
                .assemble(question, &retrieved, &history, &self.config)?;

        // 6. Build RagContext event
        let rag_sources: Vec<serde_json::Value> = if self.config.include_sources {
            retrieved
                .iter()
                .map(|r| {
                    serde_json::json!({
                        "id": r.id,
                        "excerpt": if r.content.len() > 200 {
                            format!("{}...", &r.content[..200])
                        } else {
                            r.content.clone()
                        },
                        "collection": r.collection,
                        "score": r.score,
                    })
                })
                .collect()
        } else {
            Vec::new()
        };
        let relevance_scores: Vec<f32> =
            retrieved.iter().map(|r| r.score as f32).collect();
        let include_sources = self.config.include_sources;

        // 7. Get stream generator
        let stream_gen = self.stream_generator.as_ref().ok_or_else(|| {
            RagError::NotInitialized(
                "StreamGenerator not configured. Use .stream_generator() on the builder."
                    .into(),
            )
        })?;

        // 8. Get LLM stream
        let llm_stream =
            stream_gen.generate_stream(&messages, &self.config).await?;

        // 9. Create composite stream via channel:
        //    RagContext → LLM events (with text collection) → session update
        let (tx, rx) =
            tokio::sync::mpsc::channel::<RagResult<StreamEvent>>(32);

        let session_id_owned = session_id.to_string();
        let question_owned = question.to_string();
        let sessions = self.sessions.clone();

        tokio::spawn(async move {
            use tokio_stream::StreamExt;

            // Emit RagContext first
            if include_sources && !rag_sources.is_empty() {
                let _ = tx
                    .send(Ok(StreamEvent::RagContext {
                        sources: rag_sources,
                        relevance_scores,
                    }))
                    .await;
            }

            // Forward LLM stream, collecting full content for session history
            let mut full_content = String::new();
            tokio::pin!(llm_stream);

            while let Some(event) = llm_stream.next().await {
                // Collect text deltas for session history
                if let Ok(StreamEvent::TextDelta { ref content }) = event {
                    full_content.push_str(content);
                }

                if tx.send(event).await.is_err() {
                    break; // Receiver dropped
                }
            }

            // Update session history with the full response
            let _ = sessions.push_message(
                &session_id_owned,
                ChatMessage::user(question_owned),
            );
            let _ = sessions.push_message(
                &session_id_owned,
                ChatMessage::assistant(&full_content),
            );
        });

        Ok(Box::pin(tokio_stream::wrappers::ReceiverStream::new(rx)))
    }
}

// ============================================================================
// Builder
// ============================================================================

/// Builder for constructing a `RagPipeline`.
pub struct RagPipelineBuilder {
    embedder: Option<Arc<dyn crate::perception::PerceptionEngine>>,
    retriever: Option<Box<dyn Retriever>>,
    assembler: Option<Box<dyn ContextAssembler>>,
    generator: Option<Box<dyn Generator>>,
    sessions: Option<Arc<dyn SessionManager>>,
    config: Option<RagConfig>,
    #[cfg(feature = "llm-gateway")]
    stream_generator: Option<Box<dyn super::traits::StreamGenerator>>,
}

impl Default for RagPipelineBuilder {
    fn default() -> Self {
        Self {
            embedder: None,
            retriever: None,
            assembler: None,
            generator: None,
            sessions: None,
            config: None,
            #[cfg(feature = "llm-gateway")]
            stream_generator: None,
        }
    }
}

impl RagPipelineBuilder {
    /// Set the embedding engine (optional; if not set, text-only retrieval is used).
    pub fn embedder(mut self, embedder: Arc<dyn crate::perception::PerceptionEngine>) -> Self {
        self.embedder = Some(embedder);
        self
    }

    /// Set the retriever (required).
    pub fn retriever(mut self, retriever: impl Retriever + 'static) -> Self {
        self.retriever = Some(Box::new(retriever));
        self
    }

    /// Set the context assembler (optional; defaults to `DefaultContextAssembler`).
    pub fn assembler(mut self, assembler: impl ContextAssembler + 'static) -> Self {
        self.assembler = Some(Box::new(assembler));
        self
    }

    /// Set the generator (required).
    pub fn generator(mut self, generator: impl Generator + 'static) -> Self {
        self.generator = Some(Box::new(generator));
        self
    }

    /// Set the session manager (optional; defaults to `InMemorySessionManager`).
    ///
    /// Phase-82 T82.4: Accepts any `Arc<dyn SessionManager>`, including:
    /// - `Arc<InMemorySessionManager>` (in-memory, default)
    /// - `Arc<ConversationSessionAdapter>` (persistent, via `ConversationManager`)
    pub fn session_manager(mut self, sessions: Arc<dyn SessionManager>) -> Self {
        self.sessions = Some(sessions);
        self
    }

    /// Set the RAG configuration (optional; defaults to `RagConfig::default()`).
    pub fn config(mut self, config: RagConfig) -> Self {
        self.config = Some(config);
        self
    }

    /// Set the stream generator for streaming responses (Phase-82: T82.2).
    ///
    /// When set, enables `RagPipeline::chat_stream()` for real-time SSE output.
    /// Typically use `LlmGatewayGenerator` which implements both `Generator`
    /// and `StreamGenerator`.
    ///
    /// # Requires
    ///
    /// - `llm-gateway` feature enabled
    #[cfg(feature = "llm-gateway")]
    pub fn stream_generator(
        mut self,
        gen: impl super::traits::StreamGenerator + 'static,
    ) -> Self {
        self.stream_generator = Some(Box::new(gen));
        self
    }

    /// Build the pipeline.
    pub fn build(self) -> RagResult<RagPipeline> {
        let retriever = self
            .retriever
            .ok_or_else(|| RagError::NotInitialized("Retriever is required".to_string()))?;
        let generator = self
            .generator
            .ok_or_else(|| RagError::NotInitialized("Generator is required".to_string()))?;

        let assembler = self
            .assembler
            .unwrap_or_else(|| Box::new(DefaultContextAssembler::new()));

        let sessions = self
            .sessions
            .unwrap_or_else(|| Arc::new(InMemorySessionManager::default()));

        let config = self.config.unwrap_or_default();

        Ok(RagPipeline {
            embedder: self.embedder,
            retriever,
            assembler,
            generator,
            sessions,
            config,
            #[cfg(feature = "llm-gateway")]
            stream_generator: self.stream_generator,
        })
    }
}

// ============================================================================
// Tests
// ============================================================================

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

    // ---- Mock Retriever ----
    struct MockRetriever {
        docs: Vec<RetrievalResult>,
    }

    impl Retriever for MockRetriever {
        fn retrieve(
            &self,
            _query: &str,
            _embedding: Option<&[f32]>,
            config: &RagConfig,
        ) -> RagResult<Vec<RetrievalResult>> {
            Ok(self.docs.iter().take(config.top_k).cloned().collect())
        }
    }

    // ---- Mock Generator ----
    struct MockGenerator;

    impl Generator for MockGenerator {
        fn generate(
            &self,
            messages: &[ChatMessage],
            _config: &RagConfig,
        ) -> RagResult<RagResponse> {
            // Find the user question from the last message
            let question = messages
                .iter()
                .rev()
                .find(|m| m.role == ChatRole::User)
                .map(|m| m.content.as_str())
                .unwrap_or("?");

            Ok(RagResponse {
                answer: format!("Answer to: {}", question),
                sources: Vec::new(),
                session_id: None,
                usage: Some(RagTokenUsage {
                    prompt_tokens: 100,
                    completion_tokens: 50,
                    total_tokens: 150,
                    context_tokens: 0,
                    cost: None,
                    reasoning_tokens: None,
                }),
                retrieval_count: 0,
                latency_ms: None,
                metadata: Default::default(),
            })
        }
    }

    fn build_test_pipeline() -> RagPipeline {
        let docs = vec![
            RetrievalResult::new("doc-1", "Rust is a systems programming language.", 0.9),
            RetrievalResult::new("doc-2", "Rust has zero-cost abstractions.", 0.8),
        ];

        RagPipeline::builder()
            .retriever(MockRetriever { docs })
            .generator(MockGenerator)
            .config(RagConfig {
                top_k: 5,
                include_sources: true,
                ..Default::default()
            })
            .build()
            .unwrap()
    }

    #[test]
    fn test_pipeline_build() {
        let pipeline = build_test_pipeline();
        assert_eq!(pipeline.config().top_k, 5);
    }

    #[test]
    fn test_pipeline_build_missing_retriever() {
        let result = RagPipeline::builder()
            .generator(MockGenerator)
            .build();
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("Retriever"));
    }

    #[test]
    fn test_pipeline_build_missing_generator() {
        let result = RagPipeline::builder()
            .retriever(MockRetriever { docs: vec![] })
            .build();
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("Generator"));
    }

    #[test]
    fn test_pipeline_one_shot_query() {
        let pipeline = build_test_pipeline();
        let response = pipeline.query("What is Rust?").unwrap();

        assert!(response.answer.contains("What is Rust?"));
        assert_eq!(response.retrieval_count, 2);
        assert_eq!(response.sources.len(), 2);
        assert!(response.latency_ms.is_some());
        assert!(response.session_id.is_none()); // One-shot has no session
        assert!(response.usage.is_some());
    }

    #[test]
    fn test_pipeline_chat_with_session() {
        let pipeline = build_test_pipeline();

        // First message
        let r1 = pipeline.chat("sess-1", "What is Rust?").unwrap();
        assert!(r1.answer.contains("What is Rust?"));
        assert_eq!(r1.session_id.as_deref(), Some("sess-1"));

        // Second message in same session
        let r2 = pipeline.chat("sess-1", "Tell me more").unwrap();
        assert!(r2.answer.contains("Tell me more"));

        // Session should have 4 messages (2 user + 2 assistant)
        let history = pipeline.sessions().get_history("sess-1").unwrap();
        assert_eq!(history.len(), 4);
        assert_eq!(history[0].role, ChatRole::User);
        assert_eq!(history[0].content, "What is Rust?");
        assert_eq!(history[1].role, ChatRole::Assistant);
        assert_eq!(history[2].role, ChatRole::User);
        assert_eq!(history[2].content, "Tell me more");
        assert_eq!(history[3].role, ChatRole::Assistant);
    }

    #[test]
    fn test_pipeline_multiple_sessions() {
        let pipeline = build_test_pipeline();

        pipeline.chat("sess-a", "Hello from A").unwrap();
        pipeline.chat("sess-b", "Hello from B").unwrap();

        assert_eq!(pipeline.sessions().session_count(), 2);

        let history_a = pipeline.sessions().get_history("sess-a").unwrap();
        let history_b = pipeline.sessions().get_history("sess-b").unwrap();

        assert_eq!(history_a.len(), 2);
        assert_eq!(history_b.len(), 2);
        assert_eq!(history_a[0].content, "Hello from A");
        assert_eq!(history_b[0].content, "Hello from B");
    }

    #[test]
    fn test_pipeline_sources_disabled() {
        let docs = vec![
            RetrievalResult::new("d1", "Content", 0.9),
        ];
        let pipeline = RagPipeline::builder()
            .retriever(MockRetriever { docs })
            .generator(MockGenerator)
            .config(RagConfig {
                include_sources: false,
                ..Default::default()
            })
            .build()
            .unwrap();

        let response = pipeline.query("test").unwrap();
        assert!(response.sources.is_empty());
    }

    // =========================================================================
    // Phase-82 integration tests
    // =========================================================================

    #[test]
    fn test_session_manager_trait_with_pipeline() {
        // Phase-82 T82.4: Verify that the SessionManager trait works through the pipeline
        use crate::rag::traits::SessionManager;
        use crate::rag::session::InMemorySessionManager;

        let docs = vec![
            RetrievalResult::new("doc-1", "Test content", 0.9),
        ];

        // Explicitly pass InMemorySessionManager as Arc<dyn SessionManager>
        let session_mgr: Arc<dyn SessionManager> =
            Arc::new(InMemorySessionManager::default());

        let pipeline = RagPipeline::builder()
            .retriever(MockRetriever { docs })
            .generator(MockGenerator)
            .session_manager(session_mgr)
            .build()
            .unwrap();

        let r = pipeline.chat("trait-sess", "Hello via trait").unwrap();
        assert_eq!(r.session_id.as_deref(), Some("trait-sess"));

        let history = pipeline.sessions().get_history("trait-sess").unwrap();
        assert_eq!(history.len(), 2);
        assert_eq!(history[0].content, "Hello via trait");
    }

    #[test]
    fn test_rag_token_usage_cost_field() {
        // Phase-82 T82.3: Verify that RagTokenUsage can carry cost information
        let usage = RagTokenUsage {
            prompt_tokens: 100,
            completion_tokens: 50,
            total_tokens: 150,
            context_tokens: 80,
            cost: Some(0.0015),
            reasoning_tokens: Some(10),
        };
        assert_eq!(usage.cost, Some(0.0015));
        assert_eq!(usage.reasoning_tokens, Some(10));

        // Test serialization round-trip
        let json = serde_json::to_string(&usage).unwrap();
        let parsed: RagTokenUsage = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.cost, Some(0.0015));
        assert_eq!(parsed.reasoning_tokens, Some(10));
        assert_eq!(parsed.total_tokens, 150);
    }

    #[test]
    fn test_rag_token_usage_cost_none_compat() {
        // Phase-82 T82.3: Backward compat — cost = None still works
        let usage = RagTokenUsage {
            prompt_tokens: 50,
            completion_tokens: 25,
            total_tokens: 75,
            context_tokens: 40,
            cost: None,
            reasoning_tokens: None,
        };
        assert!(usage.cost.is_none());

        // Deserialize from JSON without cost/reasoning_tokens fields (backward compat)
        let legacy_json = r#"{"prompt_tokens":50,"completion_tokens":25,"total_tokens":75,"context_tokens":40}"#;
        let parsed: RagTokenUsage = serde_json::from_str(legacy_json).unwrap();
        assert_eq!(parsed.total_tokens, 75);
        assert!(parsed.cost.is_none());
        assert!(parsed.reasoning_tokens.is_none());
    }

    #[test]
    fn test_session_manager_session_count() {
        // Phase-82 T82.4: session_count() on the trait object
        use crate::rag::traits::SessionManager;
        use crate::rag::session::InMemorySessionManager;

        let session_mgr: Arc<dyn SessionManager> =
            Arc::new(InMemorySessionManager::default());

        assert_eq!(session_mgr.session_count(), 0);

        session_mgr.get_or_create("s1").unwrap();
        assert_eq!(session_mgr.session_count(), 1);

        session_mgr.get_or_create("s2").unwrap();
        assert_eq!(session_mgr.session_count(), 2);

        // Idempotent
        session_mgr.get_or_create("s1").unwrap();
        assert_eq!(session_mgr.session_count(), 2);
    }

    #[test]
    fn test_session_manager_delete() {
        // Phase-82 T82.4: delete through trait
        use crate::rag::traits::SessionManager;
        use crate::rag::session::InMemorySessionManager;

        let session_mgr: Arc<dyn SessionManager> =
            Arc::new(InMemorySessionManager::default());

        session_mgr.get_or_create("del-sess").unwrap();
        session_mgr.push_message("del-sess", ChatMessage::user("Hi")).unwrap();

        assert_eq!(session_mgr.session_count(), 1);

        session_mgr.delete_session("del-sess").unwrap();
        assert_eq!(session_mgr.session_count(), 0);
    }
}
