//! Tree Retriever — Phase-91 (T91.1 + T91.2)
//!
//! Implements `Retriever` trait for tree-indexed documents, enabling
//! hierarchical, structure-aware retrieval for RAG pipelines.
//!
//! # Design
//!
//! Unlike vector or BM25 retrievers that treat documents as flat chunks,
//! `TreeRetriever` leverages the document's hierarchical structure:
//!
//! 1. **List indexed documents** → build a table-of-contents overview
//! 2. **Format tree structure** → present headings/summaries to the LLM
//! 3. **Text-match scoring** → score nodes by query relevance
//! 4. **Shadow filtering** → skip demoted (shadow) nodes from Phase-95
//! 5. **Access tracking** → call `record_access()` on retrieved nodes
//!
//! # Integration with Dynamic Tree (Phase-95)
//!
//! When a `DynamicTreeManager` is provided:
//! - Shadow nodes (`TREE_SECTION_SHADOW`) are filtered out during retrieval
//! - Merged parent nodes (with `has_shadows=true`) are annotated in results
//! - Each retrieved node triggers `record_access()` to update hit statistics
//! - Hot nodes may be promoted; cold nodes may be demoted in background scans

use std::collections::HashMap;
use std::sync::Arc;

use super::error::RagResult;
use super::traits::Retriever;
use super::types::{RagConfig, RetrievalResult};

use crate::tree_index::store::TreeIndexStore;
use crate::tree_index::dynamic::DynamicTreeManager;
use crate::tree_index::types::{
    TreeDocument, TreeNode,
    tree_entity_types, tree_property_keys,
};

// ============================================================================
// Configuration
// ============================================================================

/// Configuration for tree-based retrieval.
#[derive(Debug, Clone)]
pub struct TreeRetrieverConfig {
    /// Maximum number of tree nodes to return as results.
    pub max_results: usize,

    /// Maximum depth to traverse in the tree (0 = unlimited).
    pub max_depth: usize,

    /// Whether to include the full node content in results.
    /// If false, only summary/title is returned (lighter for LLM context).
    pub include_content: bool,

    /// Maximum characters of content to include per node.
    pub max_content_chars: usize,

    /// Whether to include table-of-contents as a bonus result.
    pub include_toc: bool,

    /// Source label for score_details (used in RRF fusion).
    pub source_label: String,

    /// Minimum text-match score to include a node (0.0 = include all).
    pub min_score: f64,
}

impl Default for TreeRetrieverConfig {
    fn default() -> Self {
        Self {
            max_results: 10,
            max_depth: 0, // unlimited
            include_content: true,
            max_content_chars: 1000,
            include_toc: true,
            source_label: "tree".to_string(),
            min_score: 0.0,
        }
    }
}

// ============================================================================
// TreeRetriever
// ============================================================================

/// A `Retriever` that searches tree-indexed documents by structure and content.
///
/// Pipeline:
/// 1. Load all indexed documents from `TreeIndexStore`
/// 2. For each document, traverse the tree structure
/// 3. Score each node against the query using text matching
/// 4. Filter shadow nodes (if DynamicTreeManager is present)
/// 5. Record access on retrieved nodes
/// 6. Format results with tree context (T91.2)
pub struct TreeRetriever {
    /// Reference to the tree index store
    store: Arc<TreeIndexStore>,

    /// Optional dynamic tree manager (Phase-95 integration)
    dynamic_manager: Option<Arc<DynamicTreeManager>>,

    /// Configuration
    config: TreeRetrieverConfig,
}

impl TreeRetriever {
    /// Create a new TreeRetriever.
    pub fn new(store: Arc<TreeIndexStore>) -> Self {
        Self {
            store,
            dynamic_manager: None,
            config: TreeRetrieverConfig::default(),
        }
    }

    /// Create with custom configuration.
    pub fn with_config(store: Arc<TreeIndexStore>, config: TreeRetrieverConfig) -> Self {
        Self {
            store,
            dynamic_manager: None,
            config,
        }
    }

    /// Set the dynamic tree manager for Phase-95 integration.
    pub fn with_dynamic_manager(mut self, manager: Arc<DynamicTreeManager>) -> Self {
        self.dynamic_manager = Some(manager);
        self
    }

    /// Get a reference to the configuration.
    pub fn config(&self) -> &TreeRetrieverConfig {
        &self.config
    }

    /// Get the tree index store.
    pub fn store(&self) -> &Arc<TreeIndexStore> {
        &self.store
    }

    // ════════════════════════════════════════════════════════════════════
    // Core retrieval logic
    // ════════════════════════════════════════════════════════════════════

    /// Score a tree node against a query using text matching.
    ///
    /// Returns a relevance score (0.0–1.0) based on:
    /// - Title match (highest weight)
    /// - Summary match (medium weight)
    /// - Content match (lower weight per occurrence)
    fn score_node(&self, node: &TreeNode, query_lower: &str) -> f64 {
        let mut score = 0.0;

        // Title match (weight: 3.0)
        let title_lower = node.title.to_lowercase();
        if title_lower.contains(query_lower) {
            score += 3.0;
        } else {
            // Partial word matching: check if any query word is in the title
            let query_words: Vec<&str> = query_lower.split_whitespace().collect();
            let title_word_matches = query_words
                .iter()
                .filter(|w| w.len() >= 2 && title_lower.contains(**w))
                .count();
            score += title_word_matches as f64 * 0.8;
        }

        // Summary match (weight: 2.0)
        if let Some(ref summary) = node.summary {
            let summary_lower = summary.to_lowercase();
            if summary_lower.contains(query_lower) {
                score += 2.0;
            } else {
                let query_words: Vec<&str> = query_lower.split_whitespace().collect();
                let summary_word_matches = query_words
                    .iter()
                    .filter(|w| w.len() >= 2 && summary_lower.contains(**w))
                    .count();
                score += summary_word_matches as f64 * 0.5;
            }
        }

        // Content match (weight: based on density)
        let content_lower = node.content.text.to_lowercase();
        if !content_lower.is_empty() {
            let query_words: Vec<&str> = query_lower.split_whitespace().collect();
            let total_matches: usize = query_words
                .iter()
                .filter(|w| w.len() >= 2)
                .map(|w| content_lower.matches(w).count())
                .sum();

            if total_matches > 0 {
                // Density-based scoring: matches / content_length * normalization factor
                let density = total_matches as f64 / content_lower.len().max(1) as f64 * 100.0;
                score += density.min(2.0); // Cap content score contribution
            }
        }

        score
    }

    /// Check if a node is a shadow node (demoted by DynamicTreeManager).
    fn is_shadow_node(&self, node: &TreeNode) -> bool {
        node.properties
            .get("entity_type")
            .and_then(|v| v.as_str())
            .map(|t| t == tree_entity_types::TREE_SECTION_SHADOW)
            .unwrap_or(false)
    }

    /// Check if a node has merged shadow children.
    fn has_shadows(&self, node: &TreeNode) -> bool {
        node.properties
            .get(tree_property_keys::HAS_SHADOWS)
            .and_then(|v| v.as_bool())
            .unwrap_or(false)
    }

    /// Get the number of merged shadow children for a node.
    fn shadow_count(&self, node: &TreeNode) -> usize {
        node.properties
            .get("shadow_child_count")
            .and_then(|v| v.as_u64())
            .unwrap_or(0) as usize
    }

    /// Traverse a tree and collect scored nodes.
    ///
    /// Filters out shadow nodes and respects max_depth.
    fn collect_scored_nodes(
        &self,
        node: &TreeNode,
        query_lower: &str,
        current_depth: usize,
        path: &[String],
        doc_id: &str,
        results: &mut Vec<ScoredNode>,
    ) {
        // Depth limit check
        if self.config.max_depth > 0 && current_depth > self.config.max_depth {
            return;
        }

        // Skip shadow nodes (Phase-95)
        if self.is_shadow_node(node) {
            return;
        }

        // Score this node
        let score = self.score_node(node, query_lower);

        // Build path string for context
        let mut current_path = path.to_vec();
        current_path.push(node.title.clone());

        if score > self.config.min_score {
            results.push(ScoredNode {
                node: node.clone(),
                score,
                path: current_path.clone(),
                doc_id: doc_id.to_string(),
                depth: current_depth,
                has_shadows: self.has_shadows(node),
                shadow_count: self.shadow_count(node),
            });
        }

        // Recurse into children
        for child in &node.children {
            self.collect_scored_nodes(
                child,
                query_lower,
                current_depth + 1,
                &current_path,
                doc_id,
                results,
            );
        }
    }

    /// Record access on retrieved nodes (Phase-95 integration).
    fn record_accesses(&self, entity_ids: &[String]) {
        if let Some(ref manager) = self.dynamic_manager {
            for entity_id in entity_ids {
                let _ = manager.record_access(entity_id);
            }
        }
    }

    // ════════════════════════════════════════════════════════════════════
    // T91.2: Prompt Engineering — Tree Structure Formatting
    // ════════════════════════════════════════════════════════════════════

    /// Format a tree document as a table-of-contents string for LLM context.
    ///
    /// Produces an indented outline like:
    /// ```text
    /// 📄 Document: "My Paper" (15 sections, 3 levels)
    /// ├── 1. Introduction [summary: "Overview of the topic"]
    /// │   ├── 1.1 Background
    /// │   └── 1.2 Motivation
    /// ├── 2. Methods [⊕ contains 3 merged sub-sections]
    /// │   └── 2.1 Experiment Setup
    /// └── 3. Results
    /// ```
    pub fn format_toc(doc: &TreeDocument) -> String {
        let mut lines = Vec::new();
        let node_count = doc.total_nodes();
        let max_depth = doc.max_depth();

        lines.push(format!(
            "Document: \"{}\" ({} sections, {} levels deep)",
            doc.metadata.doc_name, node_count, max_depth
        ));

        if let Some(ref desc) = doc.metadata.description {
            lines.push(format!("  Description: {}", desc));
        }

        for (i, root) in doc.structure.iter().enumerate() {
            let is_last = i == doc.structure.len() - 1;
            Self::format_toc_node(root, "", is_last, &mut lines);
        }

        lines.join("\n")
    }

    /// Recursively format a single tree node for the TOC.
    fn format_toc_node(
        node: &TreeNode,
        prefix: &str,
        is_last: bool,
        lines: &mut Vec<String>,
    ) {
        let connector = if is_last { "└── " } else { "├── " };
        let child_prefix = if is_last {
            format!("{}    ", prefix)
        } else {
            format!("{}│   ", prefix)
        };

        // Build the node line
        let mut parts = Vec::new();

        // Title
        parts.push(node.title.clone());

        // Summary annotation
        if let Some(ref summary) = node.summary {
            let truncated = if summary.len() > 60 {
                format!("{}...", &summary[..57])
            } else {
                summary.clone()
            };
            parts.push(format!("[summary: \"{}\"]", truncated));
        }

        // Shadow/merged annotation (Phase-95)
        let has_shadows = node
            .properties
            .get(tree_property_keys::HAS_SHADOWS)
            .and_then(|v| v.as_bool())
            .unwrap_or(false);

        if has_shadows {
            let count = node
                .properties
                .get("shadow_child_count")
                .and_then(|v| v.as_u64())
                .unwrap_or(0);
            if count > 0 {
                parts.push(format!("[+{} merged sub-sections]", count));
            } else {
                parts.push("[has merged sub-sections]".to_string());
            }
        }

        // Token count hint for large nodes
        if node.content.token_count > 500 {
            parts.push(format!("(~{} tokens)", node.content.token_count));
        }

        lines.push(format!("{}{}{}", prefix, connector, parts.join(" ")));

        // Children
        // Skip shadow children in TOC
        let visible_children: Vec<&TreeNode> = node
            .children
            .iter()
            .filter(|c| {
                c.properties
                    .get("entity_type")
                    .and_then(|v| v.as_str())
                    != Some(tree_entity_types::TREE_SECTION_SHADOW)
            })
            .collect();

        for (i, child) in visible_children.iter().enumerate() {
            let child_is_last = i == visible_children.len() - 1;
            Self::format_toc_node(child, &child_prefix, child_is_last, lines);
        }
    }

    /// Format a scored node as a RetrievalResult with rich context.
    ///
    /// The content includes:
    /// - Path breadcrumb (e.g., "Introduction > Background > Experiment A")
    /// - Section title and level
    /// - Summary (if available)
    /// - Merged sub-section annotation (if applicable)
    /// - Truncated content text
    fn format_node_result(&self, scored: &ScoredNode) -> RetrievalResult {
        let node = &scored.node;
        let mut content_parts = Vec::new();

        // Path breadcrumb
        if scored.path.len() > 1 {
            content_parts.push(format!(
                "[Path] {}",
                scored.path.join(" > ")
            ));
        }

        // Section header
        let level_indicator = "#".repeat(node.level as usize);
        content_parts.push(format!("{} {}", level_indicator, node.title));

        // Summary
        if let Some(ref summary) = node.summary {
            content_parts.push(format!("[Summary] {}", summary));
        }

        // Merged sub-sections annotation (Phase-95)
        if scored.has_shadows {
            let annotation = if scored.shadow_count > 0 {
                format!(
                    "[Note: This section contains {} merged sub-sections. \
                     The content below includes text from those merged sections.]",
                    scored.shadow_count
                )
            } else {
                "[Note: This section contains merged sub-sections.]".to_string()
            };
            content_parts.push(annotation);
        }

        // Content text (truncated)
        if self.config.include_content && !node.content.text.is_empty() {
            let text = truncate_content(&node.content.text, self.config.max_content_chars);
            content_parts.push(text);
        }

        let content = content_parts.join("\n\n");

        // Score details
        let mut score_details = HashMap::new();
        score_details.insert(self.config.source_label.clone(), scored.score);

        // Metadata
        let mut metadata = HashMap::new();
        metadata.insert("source".to_string(), serde_json::json!("tree_retriever"));
        metadata.insert("doc_id".to_string(), serde_json::json!(scored.doc_id));
        metadata.insert("node_id".to_string(), serde_json::json!(node.node_id));
        metadata.insert("level".to_string(), serde_json::json!(node.level));
        metadata.insert("depth".to_string(), serde_json::json!(scored.depth));
        metadata.insert("has_shadows".to_string(), serde_json::json!(scored.has_shadows));
        metadata.insert(
            "token_count".to_string(),
            serde_json::json!(node.content.token_count),
        );
        if !scored.path.is_empty() {
            metadata.insert(
                "path".to_string(),
                serde_json::json!(scored.path.join(" > ")),
            );
        }

        RetrievalResult {
            id: format!("{}:{}", scored.doc_id, node.node_id),
            content,
            score: scored.score,
            collection: Some("tree_index".to_string()),
            score_details,
            metadata,
        }
    }
}

// ============================================================================
// Retriever trait implementation
// ============================================================================

impl Retriever for TreeRetriever {
    fn retrieve(
        &self,
        query: &str,
        _embedding: Option<&[f32]>,
        config: &RagConfig,
    ) -> RagResult<Vec<RetrievalResult>> {
        let query_lower = query.to_lowercase();
        let top_k = config.top_k.min(self.config.max_results);

        // 1. Get all indexed documents
        let documents = self.store.list_documents();

        if documents.is_empty() {
            return Ok(Vec::new());
        }

        let mut all_scored: Vec<ScoredNode> = Vec::new();
        let mut toc_results: Vec<RetrievalResult> = Vec::new();

        // 2. For each document, load tree and score nodes
        for (doc_id, _metadata) in &documents {
            let doc = match self.store.get_tree(doc_id) {
                Ok(d) => d,
                Err(_) => continue,
            };

            // Optionally include TOC as a context result
            if self.config.include_toc {
                let toc = Self::format_toc(&doc);
                let mut toc_score_details = HashMap::new();
                toc_score_details.insert(self.config.source_label.clone(), 0.5);

                let mut toc_metadata = HashMap::new();
                toc_metadata.insert("source".to_string(), serde_json::json!("tree_toc"));
                toc_metadata.insert("doc_id".to_string(), serde_json::json!(doc_id));

                toc_results.push(RetrievalResult {
                    id: format!("toc:{}", doc_id),
                    content: toc,
                    score: 0.5, // TOC gets a moderate base score
                    collection: Some("tree_index".to_string()),
                    score_details: toc_score_details,
                    metadata: toc_metadata,
                });
            }

            // Traverse and score all nodes
            for root in &doc.structure {
                self.collect_scored_nodes(
                    root,
                    &query_lower,
                    1,
                    &[],
                    doc_id,
                    &mut all_scored,
                );
            }
        }

        // 3. Sort by score descending
        all_scored.sort_by(|a, b| {
            b.score
                .partial_cmp(&a.score)
                .unwrap_or(std::cmp::Ordering::Equal)
        });

        // 4. Take top_k results
        all_scored.truncate(top_k);

        // 5. Record access on retrieved nodes (Phase-95)
        let entity_ids: Vec<String> = all_scored
            .iter()
            .map(|s| format!("{}:{}", s.doc_id, s.node.node_id))
            .collect();
        self.record_accesses(&entity_ids);

        // 6. Format results
        let mut results: Vec<RetrievalResult> = all_scored
            .iter()
            .map(|scored| self.format_node_result(scored))
            .collect();

        // 7. Prepend TOC results (they provide structural context)
        if !toc_results.is_empty() {
            // Only include TOC if we have actual node results too
            if !results.is_empty() {
                results.splice(0..0, toc_results);
            }
        }

        // 8. Apply min_score filter from RagConfig
        if let Some(min_score) = config.min_score {
            results.retain(|r| r.score >= min_score);
        }

        Ok(results)
    }
}

// ============================================================================
// Internal types
// ============================================================================

/// A tree node scored against a query, with path context.
struct ScoredNode {
    node: TreeNode,
    score: f64,
    path: Vec<String>,
    doc_id: String,
    depth: usize,
    has_shadows: bool,
    shadow_count: usize,
}

// ============================================================================
// Utility functions
// ============================================================================

/// Truncate content to a maximum number of characters, preserving char boundaries.
fn truncate_content(content: &str, max_chars: usize) -> String {
    if content.len() <= max_chars {
        content.to_string()
    } else {
        let mut end = max_chars;
        while !content.is_char_boundary(end) && end > 0 {
            end -= 1;
        }
        format!("{}...", &content[..end])
    }
}

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

#[cfg(test)]
mod tests {
    use super::*;
    use crate::social::{SocialStoreV2, SocialStoreV2Config};
    use crate::tree_index::types::{TreeDocument, TreeMetadata, TreeNode};

    fn create_test_env() -> (TreeRetriever, Arc<TreeIndexStore>, tempfile::TempDir) {
        let tmp = tempfile::tempdir().unwrap();
        let config = SocialStoreV2Config::new(tmp.path());
        let graph = Arc::new(SocialStoreV2::new(config));
        let store = Arc::new(TreeIndexStore::new(graph));
        let retriever = TreeRetriever::new(store.clone());
        (retriever, store, tmp)
    }

    fn create_sample_doc() -> TreeDocument {
        let meta = TreeMetadata::new("test-paper")
            .with_description("A test research paper");

        let mut root = TreeNode::new("0001", "Introduction", 1)
            .with_content("This paper discusses machine learning approaches.");

        let child1 = TreeNode::new("0002", "Background", 2)
            .with_content("Machine learning has evolved significantly.")
            .with_summary("ML evolution overview");

        let mut child2 = TreeNode::new("0003", "Methods", 2)
            .with_content("We use deep learning methods for classification.");

        let grandchild = TreeNode::new("0004", "Neural Network Architecture", 3)
            .with_content("The neural network consists of 3 layers with attention mechanism.");

        child2.add_child(grandchild);
        root.add_child(child1);
        root.add_child(child2);

        let results_node = TreeNode::new("0005", "Results", 1)
            .with_content("Our method achieved 95% accuracy on the benchmark dataset.");

        TreeDocument::new(meta, vec![root, results_node])
    }

    #[test]
    fn test_tree_retriever_basic() {
        let (retriever, store, _tmp) = create_test_env();
        let doc = create_sample_doc();
        store.import_tree(&doc).unwrap();

        let config = RagConfig::default();
        let results = retriever.retrieve("machine learning", None, &config).unwrap();

        assert!(!results.is_empty(), "Should find results for 'machine learning'");

        // Should have at least a TOC result + node results
        let toc_results: Vec<&RetrievalResult> = results
            .iter()
            .filter(|r| r.id.starts_with("toc:"))
            .collect();
        assert!(!toc_results.is_empty(), "Should include TOC");

        let node_results: Vec<&RetrievalResult> = results
            .iter()
            .filter(|r| !r.id.starts_with("toc:"))
            .collect();
        assert!(!node_results.is_empty(), "Should have node results");
    }

    #[test]
    fn test_tree_retriever_empty_store() {
        let (retriever, _store, _tmp) = create_test_env();

        let config = RagConfig::default();
        let results = retriever.retrieve("anything", None, &config).unwrap();
        assert!(results.is_empty());
    }

    #[test]
    fn test_tree_retriever_scoring() {
        let (retriever, store, _tmp) = create_test_env();
        let doc = create_sample_doc();
        store.import_tree(&doc).unwrap();

        let config = RagConfig::default();
        let results = retriever.retrieve("neural network", None, &config).unwrap();

        // Filter out TOC results
        let node_results: Vec<&RetrievalResult> = results
            .iter()
            .filter(|r| !r.id.starts_with("toc:"))
            .collect();

        assert!(!node_results.is_empty());

        // The "Neural Network Architecture" node should have the highest score
        let top_result = &node_results[0];
        assert!(
            top_result.content.contains("Neural Network Architecture"),
            "Top result should be the Neural Network node, got: {}",
            &top_result.content[..80.min(top_result.content.len())]
        );
    }

    #[test]
    fn test_tree_retriever_metadata() {
        let (retriever, store, _tmp) = create_test_env();
        let doc = create_sample_doc();
        store.import_tree(&doc).unwrap();

        let config = RagConfig::default();
        let results = retriever.retrieve("methods", None, &config).unwrap();

        let node_results: Vec<&RetrievalResult> = results
            .iter()
            .filter(|r| !r.id.starts_with("toc:"))
            .collect();

        assert!(!node_results.is_empty());

        let result = &node_results[0];
        assert!(result.metadata.contains_key("source"));
        assert!(result.metadata.contains_key("doc_id"));
        assert!(result.metadata.contains_key("node_id"));
        assert!(result.metadata.contains_key("level"));
        assert_eq!(result.collection.as_deref(), Some("tree_index"));
    }

    #[test]
    fn test_format_toc() {
        let doc = create_sample_doc();
        let toc = TreeRetriever::format_toc(&doc);

        assert!(toc.contains("test-paper"));
        assert!(toc.contains("Introduction"));
        assert!(toc.contains("Background"));
        assert!(toc.contains("Methods"));
        assert!(toc.contains("Neural Network Architecture"));
        assert!(toc.contains("Results"));
    }

    #[test]
    fn test_format_toc_with_summary() {
        let meta = TreeMetadata::new("doc-with-summary");
        let root = TreeNode::new("0001", "Chapter 1", 1)
            .with_content("Content here")
            .with_summary("This chapter introduces the topic");

        let doc = TreeDocument::new(meta, vec![root]);
        let toc = TreeRetriever::format_toc(&doc);

        assert!(toc.contains("summary:"));
        assert!(toc.contains("introduces the topic"));
    }

    #[test]
    fn test_score_node_title_match() {
        let (retriever, _store, _tmp) = create_test_env();

        let node = TreeNode::new("0001", "Machine Learning Basics", 1)
            .with_content("Some generic content about the topic.");

        let score = retriever.score_node(&node, "machine learning");
        assert!(score > 2.0, "Title match should give high score, got {}", score);
    }

    #[test]
    fn test_score_node_content_match() {
        let (retriever, _store, _tmp) = create_test_env();

        let node = TreeNode::new("0001", "Chapter 1", 1)
            .with_content("This is about machine learning and deep learning approaches.");

        let score = retriever.score_node(&node, "machine learning");
        assert!(score > 0.0, "Content match should give positive score, got {}", score);
    }

    #[test]
    fn test_score_node_no_match() {
        let (retriever, _store, _tmp) = create_test_env();

        let node = TreeNode::new("0001", "Chapter 1", 1)
            .with_content("This is about cooking recipes.");

        let score = retriever.score_node(&node, "quantum physics");
        assert!(
            score < 0.5,
            "Non-matching content should have low score, got {}",
            score
        );
    }

    #[test]
    fn test_truncate_content_fn() {
        assert_eq!(truncate_content("hello", 10), "hello");
        assert_eq!(truncate_content("hello world foo", 11), "hello world...");
    }

    #[test]
    fn test_retriever_with_config() {
        let (_, store, _tmp) = create_test_env();

        let config = TreeRetrieverConfig {
            max_results: 3,
            include_toc: false,
            include_content: true,
            max_content_chars: 50,
            ..Default::default()
        };

        let retriever = TreeRetriever::with_config(store.clone(), config);
        let doc = create_sample_doc();
        store.import_tree(&doc).unwrap();

        let rag_config = RagConfig::default();
        let results = retriever.retrieve("learning", None, &rag_config).unwrap();

        // No TOC results since include_toc = false
        let toc_results: Vec<&RetrievalResult> = results
            .iter()
            .filter(|r| r.id.starts_with("toc:"))
            .collect();
        assert!(toc_results.is_empty(), "Should not include TOC");

        // At most 3 results
        assert!(results.len() <= 3);
    }

    #[test]
    fn test_retriever_path_breadcrumb() {
        let (retriever, store, _tmp) = create_test_env();
        let doc = create_sample_doc();
        store.import_tree(&doc).unwrap();

        let config = RagConfig::default();
        let results = retriever.retrieve("neural network", None, &config).unwrap();

        let nn_results: Vec<&RetrievalResult> = results
            .iter()
            .filter(|r| r.content.contains("Neural Network Architecture"))
            .collect();

        assert!(!nn_results.is_empty());
        let nn_result = nn_results[0];

        // Should have path metadata showing the hierarchy
        if let Some(path) = nn_result.metadata.get("path") {
            let path_str = path.as_str().unwrap_or("");
            assert!(
                path_str.contains("Methods"),
                "Path should include parent 'Methods', got: {}",
                path_str
            );
        }
    }

    // ── Shadow Node Scenario Tests (Phase-95 / T91.7) ────────────────

    #[test]
    fn test_retriever_with_dynamic_tree_filters_shadow_nodes() {
        let tmp = tempfile::tempdir().unwrap();
        let config = SocialStoreV2Config::new(tmp.path());
        let graph = Arc::new(SocialStoreV2::new(config));
        let store = Arc::new(TreeIndexStore::new(graph));

        // Import a document with deeply nested structure
        let meta = TreeMetadata::new("shadow-test");
        let grandchild1 = TreeNode::new("gc1", "Deep Section A", 3)
            .with_content("This is a deep section about quantum computing.");
        let grandchild2 = TreeNode::new("gc2", "Deep Section B", 3)
            .with_content("This is another deep section about quantum entanglement.");
        let mut child = TreeNode::new("c1", "Main Section", 2)
            .with_content("Overview of quantum physics.");
        child.add_child(grandchild1);
        child.add_child(grandchild2);
        let mut root = TreeNode::new("r1", "Document Root", 1)
            .with_content("A document about physics.");
        root.add_child(child);
        let doc = TreeDocument::new(meta, vec![root]);
        let doc_id = store.import_tree(&doc).unwrap();

        // Enable dynamic tree and demote the "Main Section" children
        let dynamic_config = crate::tree_index::DynamicTreeConfig {
            enabled: true,
            promote_hit_threshold: 50,
            demote_idle_timeout_secs: 1, // Very short for testing
            ..Default::default()
        };
        let dynamic_mgr = Arc::new(DynamicTreeManager::new(store.clone(), dynamic_config));

        // Find the Main Section entity ID to demote
        let tree_doc = store.get_tree(&doc_id).unwrap();
        let main_section = &tree_doc.structure[0].children[0]; // "Main Section"
        let main_section_entity_id = format!("tree_{}_c1", doc_id);

        // Demote the subtree (gc1, gc2 become shadows)
        let demote_result = dynamic_mgr.demote_subtree(&main_section_entity_id);
        // Only attempt shadow filtering if demote succeeded
        if demote_result.is_ok() {
            // Create retriever WITH dynamic manager
            let retriever = TreeRetriever::new(store.clone())
                .with_dynamic_manager(dynamic_mgr.clone());

            let rag_config = RagConfig::default();
            let results = retriever.retrieve("quantum", None, &rag_config).unwrap();

            // Shadow nodes (gc1, gc2) should NOT appear in results individually
            let shadow_results: Vec<&RetrievalResult> = results
                .iter()
                .filter(|r| r.content.contains("Deep Section A") || r.content.contains("Deep Section B"))
                .filter(|r| !r.id.starts_with("toc:")) // Exclude TOC
                .collect();

            // If shadow nodes are properly filtered, they should not appear as separate results
            // (their content might still appear in the parent's merged content)
            for sr in &shadow_results {
                // Shadow node content should be merged into parent, not standalone
                assert!(
                    sr.metadata.get("node_id").and_then(|v| v.as_str()) != Some("gc1")
                        && sr.metadata.get("node_id").and_then(|v| v.as_str()) != Some("gc2"),
                    "Shadow nodes should not appear as standalone results"
                );
            }
        }
    }

    #[test]
    fn test_retriever_records_access_on_retrieve() {
        let tmp = tempfile::tempdir().unwrap();
        let config = SocialStoreV2Config::new(tmp.path());
        let graph = Arc::new(SocialStoreV2::new(config));
        let store = Arc::new(TreeIndexStore::new(graph.clone()));

        // Import a simple document
        let meta = TreeMetadata::new("access-test");
        let root = TreeNode::new("0001", "Machine Learning", 1)
            .with_content("Machine learning is a field of artificial intelligence.");
        let doc = TreeDocument::new(meta, vec![root]);
        let doc_id = store.import_tree(&doc).unwrap();

        // Enable dynamic tree
        let dynamic_config = crate::tree_index::DynamicTreeConfig {
            enabled: true,
            ..Default::default()
        };
        let dynamic_mgr = Arc::new(DynamicTreeManager::new(store.clone(), dynamic_config));

        let retriever = TreeRetriever::new(store.clone())
            .with_dynamic_manager(dynamic_mgr.clone());

        // Retrieve — this should record access
        let rag_config = RagConfig::default();
        let results = retriever.retrieve("machine learning", None, &rag_config).unwrap();
        assert!(!results.is_empty(), "Should find results");

        // Check that access was recorded on the matching node
        let node_entity_id = format!("tree_{}_0001", doc_id);
        let entity = graph.get_entity_by_id(&node_entity_id);
        if let Some(e) = entity {
            let hit_count = e.properties.get("hit_count")
                .and_then(|v| v.as_u64())
                .unwrap_or(0);
            assert!(
                hit_count > 0,
                "hit_count should be > 0 after retrieve, got {}",
                hit_count
            );
        }
    }

    #[test]
    fn test_retriever_without_dynamic_manager_still_works() {
        // Ensure that when no dynamic manager is set, retriever works normally
        let (retriever, store, _tmp) = create_test_env();
        let doc = create_sample_doc();
        store.import_tree(&doc).unwrap();

        let config = RagConfig::default();
        let results = retriever.retrieve("neural network", None, &config).unwrap();
        assert!(!results.is_empty(), "Retriever should work without dynamic manager");
    }
}
