//! Typed error returned by [`crate::EpisodeProjector`] operations.
//!
//! `#[non_exhaustive]` so additional failure modes (e.g. timeout,
//! cancellation) can land in a 0.x minor without forcing match-arm
//! updates on external callers. Each variant carries a boxed source
//! so [`std::error::Error::source`] walks the chain.

use klieo_core::error::MemoryError;
use thiserror::Error;

/// Error returned by [`crate::EpisodeProjector`] operations.
///
/// Variant docs name the call site that failed; access the wrapped
/// error via [`std::error::Error::source`] (rather than the `source`
/// field directly) so callers stay decoupled from the concrete inner
/// type, which may evolve under the 0.x carve-out.
#[derive(Debug, Error)]
#[non_exhaustive]
#[allow(missing_docs)] // variant `source` fields are reached via `Error::source`; no per-field contract beyond the variant doc above
pub enum ProjectionError {
    /// Entity extraction (the
    /// [`klieo_memory_graph::EntityExtractor`] supplied to
    /// [`crate::EpisodeProjector::new`]) returned an error.
    #[error("entity extraction failed")]
    Extraction {
        #[source]
        source: Box<dyn std::error::Error + Send + Sync + 'static>,
    },
    /// Indexing into the [`klieo_memory_graph::KnowledgeGraph`]
    /// returned an error.
    #[error("graph index failed")]
    Graph {
        #[source]
        source: Box<dyn std::error::Error + Send + Sync + 'static>,
    },
    /// Replay against the supplied
    /// [`klieo_core::memory::EpisodicMemory`] (only used by
    /// [`crate::EpisodeProjector::project_run`]) returned an error.
    #[error("episodic replay failed")]
    Replay {
        #[source]
        source: Box<dyn std::error::Error + Send + Sync + 'static>,
    },
}

impl ProjectionError {
    pub(crate) fn extraction(err: MemoryError) -> Self {
        Self::Extraction {
            source: Box::new(err),
        }
    }
    pub(crate) fn graph(err: MemoryError) -> Self {
        Self::Graph {
            source: Box::new(err),
        }
    }
    pub(crate) fn replay(err: MemoryError) -> Self {
        Self::Replay {
            source: Box::new(err),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::error::Error as _;

    fn sample_memory_error(label: &str) -> MemoryError {
        MemoryError::Store(label.to_string())
    }

    #[test]
    fn extraction_variant_chains_source_to_memory_error() {
        let err = ProjectionError::extraction(sample_memory_error("extractor exploded"));
        let source = err.source().expect("Extraction must expose a source");
        let inner = source
            .downcast_ref::<MemoryError>()
            .expect("source must downcast to MemoryError");
        assert!(matches!(inner, MemoryError::Store(s) if s.contains("extractor exploded")));
        assert_eq!(format!("{err}"), "entity extraction failed");
    }

    #[test]
    fn graph_variant_chains_source_to_memory_error() {
        let err = ProjectionError::graph(sample_memory_error("graph backend exploded"));
        let source = err.source().expect("Graph must expose a source");
        let inner = source
            .downcast_ref::<MemoryError>()
            .expect("source must downcast to MemoryError");
        assert!(matches!(inner, MemoryError::Store(s) if s.contains("graph backend exploded")));
        assert_eq!(format!("{err}"), "graph index failed");
    }

    #[test]
    fn replay_variant_chains_source_to_memory_error() {
        let err = ProjectionError::replay(sample_memory_error("episodic exploded"));
        let source = err.source().expect("Replay must expose a source");
        let inner = source
            .downcast_ref::<MemoryError>()
            .expect("source must downcast to MemoryError");
        assert!(matches!(inner, MemoryError::Store(s) if s.contains("episodic exploded")));
        assert_eq!(format!("{err}"), "episodic replay failed");
    }
}
