use thiserror::Error;

/// Parse errors.
#[derive(Debug, Error)]
pub enum ParseError {
    /// The supplied SQL string was empty or contained only whitespace.
    #[error("input SQL was empty")]
    EmptyInput,
    /// A file could not be read from disk.
    #[error("io error reading {path}: {source}")]
    Io {
        /// path that could not be read.
        path: String,
        /// source IO error.
        source: std::io::Error,
    },
    /// The SQL scanner failed to split the input into statements.
    #[error("scanner error: {0}")]
    Scanner(String),
}

/// File-walk errors.
#[derive(Debug, Error)]
pub enum WalkError {
    /// A directory entry could not be read.
    #[error("walk error: {0}")]
    Io(#[from] ignore::Error),
    /// A path contained non-UTF-8 bytes.
    #[error("non-UTF-8 path: {0}")]
    NonUtf8(std::path::PathBuf),
}

/// Expression deparse errors.
#[derive(Debug, Error)]
pub(crate) enum DeparseError {
    #[error("pg_query deparse failed: {0}")]
    PgQuery(#[from] pg_query::Error),
    #[error("deparse output missing SELECT prefix: {output}")]
    UnexpectedShape { output: String },
}

/// SCIP index emission errors.
#[derive(Debug, Error)]
pub enum EmitError {
    /// Writing the index file failed.
    #[error("failed to write index to {path}: {source}")]
    Write {
        /// path that could not be written.
        path: String,
        /// source error.
        source: Box<dyn std::error::Error + 'static>,
    },
    /// File content could not be read for indexing.
    #[error("failed to read {path}: {source}")]
    Read {
        /// path that could not be read.
        path: String,
        /// source IO error.
        #[source]
        source: std::io::Error,
    },
}

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

    #[test]
    fn read_error_display_includes_path() {
        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "no such file");
        let err = EmitError::Read {
            path: "/tmp/missing.sql".to_owned(),
            source: io_err,
        };
        let msg = err.to_string();
        assert!(
            msg.contains("/tmp/missing.sql"),
            "error message must include path: {msg}"
        );
        assert!(
            msg.contains("no such file"),
            "error message must include source: {msg}"
        );
    }
}
