//! See [`Welp`] for the overview.

use std::error::Error as StdError;
use std::fmt;

use crate::{Backtrace, Capturable as _, Diagnostic, SpanTrace};

/// A boxed `std::error::Error` that's `Send + Sync + 'static`.
type BoxError = Box<dyn StdError + Send + Sync + 'static>;

/// A string-shaped error type for prototypes, one-off errors, and quick
/// string contexts.
///
/// The typed-selector flow generated by `#[oopsie]` is the project's primary
/// error-handling story. `Welp` covers the cases where a structured variant
/// would be overkill: prototyping, exactly-once failure modes, and adding a
/// plain-string context to a foreign error.
///
/// ```
/// use oopsie_core::{Welp, WelpResultExt as _};
///
/// fn read_config(path: &str) -> Result<String, Welp> {
///     std::fs::read_to_string(path).welp_context("could not read config")
/// }
/// ```
///
/// # Construction
///
/// - [`Welp::new`] — fresh string error, captures a backtrace (and a span-trace
///   when the `tracing` feature is enabled).
/// - [`Welp::wrap`] — wrap an existing error with a string message.
/// - [`Welp::wrap_boxed`] — same, for an already-boxed `dyn Error`.
/// - [`Welp::from_error`] — wrap an existing error with *no* message; its
///   `Display` delegates to the source.
///
/// # Adding context to a `Result` / `Option`
///
/// Bring [`WelpResultExt`] / [`WelpOptionExt`] into scope (or import from
/// `oopsie::prelude`) and use [`welp_context`](WelpResultExt::welp_context) /
/// [`with_welp_context`](WelpResultExt::with_welp_context).
///
/// Use [`welp`](WelpResultExt::welp) for the message-free conversion — the
/// `?`-style escape hatch `anyhow`/`eyre` give you for free. `result.welp()?`
/// turns any foreign `Error` into a `Welp` without inventing a string;
/// `result.welp_context("…")?` is for when a message genuinely adds information
/// the source doesn't already carry. Together they make
/// `fn main() -> Report<Welp>` (with `Welp` from the prelude) the catch-all
/// top-level error story.
///
/// # Layout
///
/// `Welp` is a small enum. Every variant carries a boxed `(Backtrace, _)`
/// captured at construction (the second slot is the span-trace under the
/// `tracing` feature and is otherwise empty) and an inline `&'static Location`
/// of the call site. `Traced` adds a `Box<str>` message; `Sourced` adds an
/// optional `Box<str>` message and the wrapped `Box<dyn Error>`. The
/// message-free form ([`from_error`](Self::from_error) /
/// [`welp`](WelpResultExt::welp)) is a `Sourced` with no message, whose
/// `Display` delegates to the source.
///
/// A `Sourced` `Welp`'s [`Diagnostic`] accessors prefer the source's traces
/// (reached via the Provider API under the
/// `unstable-error-generic-member-access` feature) and fall back to the
/// wrap-site ones.
#[must_use = "this `Welp` error should be returned or propagated, not discarded"]
pub struct Welp(WelpRepr);

enum WelpRepr {
    Sourced {
        // `None` is the message-free form (`from_error` / `.welp()`): `Display`
        // delegates to `source`. The `Box<str>` data pointer's null niche
        // absorbs the `Option` tag, so this stays one machine word.
        message: Option<Box<str>>,
        source: BoxError,
        traces: Box<(Backtrace, SpanTrace)>,
        location: &'static std::panic::Location<'static>,
    },
    Traced {
        message: Box<str>,
        traces: Box<(Backtrace, SpanTrace)>,
        location: &'static std::panic::Location<'static>,
    },
}

impl Welp {
    /// Build a fresh `Welp` from a string message. Captures a backtrace (and a
    /// span-trace when the `tracing` feature is enabled) at the call site.
    ///
    /// ```
    /// use oopsie_core::Welp;
    ///
    /// let err = Welp::new(format!("port {} out of range", 70_000));
    /// assert_eq!(err.to_string(), "port 70000 out of range");
    /// ```
    #[track_caller]
    pub fn new(message: impl Into<String>) -> Self {
        Self(WelpRepr::Traced {
            message: message.into().into_boxed_str(),
            traces: Box::new((Backtrace::capture(), SpanTrace::capture())),
            location: std::panic::Location::caller(),
        })
    }

    /// Wrap an existing error with a string message. Captures a backtrace (and a
    /// span-trace when the `tracing` feature is enabled) at the wrap site; when
    /// the source provides its own captured
    /// traces (via the Provider API under the
    /// `unstable-error-generic-member-access` feature) the [`Diagnostic`]
    /// accessors surface those instead, so the origin-most captured trace
    /// wins.
    ///
    /// ```
    /// use oopsie_core::Welp;
    ///
    /// let io_err = std::io::Error::other("disk full");
    /// let err = Welp::wrap(io_err, "could not write");
    /// assert_eq!(err.to_string(), "could not write");
    /// assert!(std::error::Error::source(&err).is_some());
    /// ```
    #[track_caller]
    pub fn wrap<E>(source: E, message: impl Into<String>) -> Self
    where
        E: StdError + Send + Sync + 'static,
    {
        // No construction-time extraction: `wrap` is generic, so the
        // `CaptureProbe` autoref trick cannot see the source's `Diagnostic`
        // impl here (method resolution happens against this body's bounds,
        // not per instantiation). The origin trace is reached at accessor
        // time instead, through the source-first Provider lookup.
        Self(WelpRepr::Sourced {
            message: Some(message.into().into_boxed_str()),
            source: Box::new(source),
            traces: Box::new((Backtrace::capture(), SpanTrace::capture())),
            location: std::panic::Location::caller(),
        })
    }

    /// Wrap an already-boxed trait-object error with a string message.
    ///
    /// Use this when the source comes from an API returning
    /// `Box<dyn Error + Send + Sync + 'static>` — that type is `?Sized` and
    /// can't satisfy [`wrap`](Self::wrap)'s `Sized` bound, but it's exactly
    /// the shape `Welp` stores internally, so no rewrapping is needed.
    ///
    /// ```
    /// use oopsie_core::Welp;
    ///
    /// let boxed: Box<dyn std::error::Error + Send + Sync + 'static> =
    ///     Box::new(std::io::Error::other("disk full"));
    /// let err = Welp::wrap_boxed(boxed, "could not write");
    /// assert_eq!(err.to_string(), "could not write");
    /// assert!(std::error::Error::source(&err).is_some());
    /// ```
    #[track_caller]
    pub fn wrap_boxed(source: BoxError, message: impl Into<String>) -> Self {
        Self(WelpRepr::Sourced {
            message: Some(message.into().into_boxed_str()),
            source,
            traces: Box::new((Backtrace::capture(), SpanTrace::capture())),
            location: std::panic::Location::caller(),
        })
    }

    /// Wrap an existing error with no user message: this `Welp`'s [`Display`]
    /// delegates to the source's, and the source stays reachable through
    /// [`Error::source`]. Use it for the message-free `?`-style conversion of a
    /// foreign error; reach for [`wrap`](Self::wrap) when a string context adds
    /// information.
    ///
    /// Captures a backtrace (and a span-trace under the `tracing` feature) at
    /// the call site; like [`wrap`](Self::wrap), the [`Diagnostic`] accessors
    /// surface the source's captured traces (via the Provider API under the
    /// `unstable-error-generic-member-access` feature) when it has them, so the
    /// origin-most trace wins.
    ///
    /// [`Display`]: fmt::Display
    /// [`Error::source`]: StdError::source
    ///
    /// ```
    /// use oopsie_core::Welp;
    ///
    /// let err = Welp::from_error(std::io::Error::other("disk full"));
    /// assert_eq!(err.to_string(), "disk full");
    /// assert!(std::error::Error::source(&err).is_some());
    /// ```
    #[track_caller]
    pub fn from_error<E>(source: E) -> Self
    where
        E: StdError + Send + Sync + 'static,
    {
        Self(WelpRepr::Sourced {
            message: None,
            source: Box::new(source),
            traces: Box::new((Backtrace::capture(), SpanTrace::capture())),
            location: std::panic::Location::caller(),
        })
    }

    /// The user message attached to this error, or `None` when it carries none
    /// (a `Welp` from [`from_error`](Self::from_error) / [`welp`] delegates its
    /// [`Display`] to the wrapped source instead of owning a message).
    ///
    /// [`welp`]: WelpResultExt::welp
    /// [`Display`]: fmt::Display
    #[must_use]
    #[inline]
    pub fn message(&self) -> Option<&str> {
        match &self.0 {
            WelpRepr::Sourced { message, .. } => message.as_deref(),
            WelpRepr::Traced { message, .. } => Some(message),
        }
    }

    /// Borrow the wrapped source as `&E` when it is exactly that type.
    ///
    /// Inspects the *direct* source only, not the whole chain. A `Welp` with no
    /// source ([`new`](Self::new) / [`welp_context`](crate::WelpOptionExt::welp_context)
    /// on an `Option`) returns `None`. To search the whole chain, compose with
    /// [`chain`](crate::ErrorChainExt::chain):
    ///
    /// ```
    /// use oopsie_core::{ErrorChainExt as _, Welp};
    /// use std::error::Error as _;
    ///
    /// let inner = std::io::Error::other("disk full");
    /// let err = Welp::wrap(Welp::wrap(inner, "mid"), "outer");
    ///
    /// // The direct source is the middle `Welp`, not the io::Error.
    /// assert!(err.downcast_ref::<std::io::Error>().is_none());
    ///
    /// // Reach the buried io::Error by walking the chain.
    /// let io = err
    ///     .chain()
    ///     .find_map(|e| e.downcast_ref::<std::io::Error>())
    ///     .expect("io::Error is somewhere in the chain");
    /// assert_eq!(io.to_string(), "disk full");
    /// ```
    #[must_use]
    #[inline]
    pub fn downcast_ref<E: StdError + 'static>(&self) -> Option<&E> {
        match &self.0 {
            WelpRepr::Sourced { source, .. } => source.downcast_ref::<E>(),
            WelpRepr::Traced { .. } => None,
        }
    }

    /// Mutably borrow the wrapped source as `&mut E` when it is exactly that
    /// type. Like [`downcast_ref`](Self::downcast_ref), inspects the direct
    /// source only.
    #[must_use]
    #[inline]
    pub fn downcast_mut<E: StdError + 'static>(&mut self) -> Option<&mut E> {
        match &mut self.0 {
            WelpRepr::Sourced { source, .. } => source.downcast_mut::<E>(),
            WelpRepr::Traced { .. } => None,
        }
    }

    /// Consume this `Welp` and recover the wrapped source as `E` when it is
    /// exactly that type, returning the `Welp` unchanged otherwise.
    ///
    /// Targets the *direct* source only. A message-free [`Welp`] is the source's
    /// transparent wrapper, so this recovers the original foreign error:
    ///
    /// ```
    /// use oopsie_core::{Welp, WelpResultExt as _};
    ///
    /// let r: Result<(), std::io::Error> = Err(std::io::Error::other("disk full"));
    /// let welp = r.welp().unwrap_err();
    /// let io: std::io::Error = welp.downcast().expect("source is an io::Error");
    /// assert_eq!(io.to_string(), "disk full");
    /// ```
    ///
    /// # Errors
    ///
    /// Returns `Err(self)` — message, location, and captured traces intact —
    /// when there is no source or it is not of type `E`.
    pub fn downcast<E: StdError + 'static>(self) -> Result<E, Self> {
        match self.0 {
            WelpRepr::Sourced {
                message,
                source,
                traces,
                location,
            } => match source.downcast::<E>() {
                Ok(source) => Ok(*source),
                Err(source) => Err(Self(WelpRepr::Sourced {
                    message,
                    source,
                    traces,
                    location,
                })),
            },
            traced @ WelpRepr::Traced { .. } => Err(Self(traced)),
        }
    }
}

impl fmt::Debug for Welp {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut dbg = f.debug_struct("Welp");
        if let Some(message) = self.message() {
            dbg.field("message", &message);
        }
        match &self.0 {
            WelpRepr::Sourced { source, .. } => {
                dbg.field("source", source);
            }
            WelpRepr::Traced { .. } => {}
        }
        dbg.finish()
    }
}

impl fmt::Display for Welp {
    #[inline]
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.0 {
            // No message is the delegating form: render the source as if it
            // were the top error.
            WelpRepr::Sourced {
                message: None,
                source,
                ..
            } => source.fmt(f),
            WelpRepr::Sourced {
                message: Some(message),
                ..
            }
            | WelpRepr::Traced { message, .. } => f.write_str(message),
        }
    }
}

impl StdError for Welp {
    fn source(&self) -> Option<&(dyn StdError + 'static)> {
        match &self.0 {
            WelpRepr::Sourced { source, .. } => Some(&**source),
            WelpRepr::Traced { .. } => None,
        }
    }

    #[cfg(feature = "unstable-error-generic-member-access")]
    fn provide<'a>(&'a self, request: &mut core::error::Request<'a>) {
        let traces = match &self.0 {
            WelpRepr::Sourced { source, traces, .. } => {
                // `Request` is first-wins: forwarding the source first keeps
                // its deeper trace ahead of the wrap-site one. An empty trace
                // is never provided, so it cannot shadow a captured one in an
                // outer layer.
                source.provide(request);
                traces
            }
            WelpRepr::Traced { traces, .. } => traces,
        };
        if traces.0.is_captured() {
            request.provide_ref::<Backtrace>(&traces.0);
        }
        if traces.1.is_captured() {
            request.provide_ref::<SpanTrace>(&traces.1);
        }
    }
}

impl Diagnostic for Welp {
    fn oopsie_backtrace(&self) -> Option<&Backtrace> {
        match &self.0 {
            WelpRepr::Sourced { source, traces, .. } => {
                crate::__private::source_backtrace(&**source).or(Some(&traces.0))
            }
            WelpRepr::Traced { traces, .. } => Some(&traces.0),
        }
    }

    fn oopsie_spantrace(&self) -> Option<&SpanTrace> {
        match &self.0 {
            WelpRepr::Sourced { source, traces, .. } => {
                crate::__private::source_spantrace(&**source).or(Some(&traces.1))
            }
            WelpRepr::Traced { traces, .. } => Some(&traces.1),
        }
    }

    fn oopsie_location(&self) -> Option<&'static std::panic::Location<'static>> {
        match &self.0 {
            WelpRepr::Sourced { location, .. } | WelpRepr::Traced { location, .. } => {
                Some(location)
            }
        }
    }

    fn oopsie_exit_code(&self) -> Option<core::num::NonZeroU8> {
        // Surface the wrapped error's declared exit code, so a `#[oopsie]`
        // origin's code survives `.welp()` wrapping. The source is type-erased,
        // so this reaches it through the Provider API and yields `None` on
        // stable — `Welp` keeps no exit code of its own to fall back to.
        match &self.0 {
            WelpRepr::Sourced { source, .. } => crate::__private::source_exit_code(&**source),
            WelpRepr::Traced { .. } => None,
        }
    }
}

/// Extension trait on [`Result`] for attaching a string message that produces
/// a [`Welp`].
///
/// Bring this into scope via `use oopsie::prelude::*` (or `use
/// oopsie_core::WelpResultExt`).
///
/// ```
/// use oopsie_core::{Welp, WelpResultExt as _};
///
/// fn read(path: &str) -> Result<String, Welp> {
///     std::fs::read_to_string(path).welp_context("could not read")
/// }
/// ```
pub trait WelpResultExt<T, E>: Sized {
    /// Convert the error into a [`Welp`] with no user message — the kind of
    /// message-free `?`-style conversion `anyhow`/`eyre` give you for free. The
    /// resulting `Welp`'s [`Display`](std::fmt::Display) delegates to the
    /// source's, and the source stays reachable through
    /// [`Error::source`](std::error::Error::source). Reach for
    /// [`welp_context`](Self::welp_context) instead when a string adds
    /// information the source doesn't already carry.
    fn welp(self) -> Result<T, Welp>;

    /// Wrap the error in a [`Welp`] with the given message.
    ///
    /// Note: the `message` argument is evaluated at the call site per Rust's
    /// eager-argument rules — `welp_context(format!(...))` allocates the
    /// formatted `String` even on the `Ok` branch. Use [`with_welp_context`]
    /// when the message is built from a non-trivial expression that should
    /// only run on `Err`.
    ///
    /// [`with_welp_context`]: Self::with_welp_context
    fn welp_context(self, message: impl Into<String>) -> Result<T, Welp>;

    /// Wrap the error in a [`Welp`] with a lazily-built message. The closure
    /// runs only on the `Err` branch and receives a shared reference to the
    /// source error.
    fn with_welp_context<S, F>(self, f: F) -> Result<T, Welp>
    where
        S: Into<String>,
        F: FnOnce(&E) -> S;
}

impl<T, E> WelpResultExt<T, E> for Result<T, E>
where
    E: StdError + Send + Sync + 'static,
{
    #[inline]
    #[track_caller]
    fn welp(self) -> Result<T, Welp> {
        // Build directly (no `map_err` closure) so `Welp::from_error`'s
        // `#[track_caller]` reports the `.welp` call site.
        match self {
            Ok(value) => Ok(value),
            Err(e) => Err(Welp::from_error(e)),
        }
    }

    #[inline]
    #[track_caller]
    fn welp_context(self, message: impl Into<String>) -> Result<T, Welp> {
        // Build directly (no `map_err` closure) so `Welp::wrap`'s
        // `#[track_caller]` reports the `.welp_context` call site.
        match self {
            Ok(value) => Ok(value),
            Err(e) => Err(Welp::wrap(e, message)),
        }
    }

    #[inline]
    #[track_caller]
    fn with_welp_context<S, F>(self, f: F) -> Result<T, Welp>
    where
        S: Into<String>,
        F: FnOnce(&E) -> S,
    {
        match self {
            Ok(value) => Ok(value),
            Err(e) => {
                let msg = f(&e);
                Err(Welp::wrap(e, msg))
            }
        }
    }
}

/// Extension trait on [`Option`] for converting `None` into a [`Welp`].
///
/// Bring this into scope via `use oopsie::prelude::*` (or `use
/// oopsie_core::WelpOptionExt`).
///
/// ```
/// use oopsie_core::{Welp, WelpOptionExt as _};
///
/// fn first_word(s: &str) -> Result<&str, Welp> {
///     s.split_whitespace().next().welp_context("string is empty")
/// }
/// ```
pub trait WelpOptionExt<T>: Sized {
    /// Convert `None` into a [`Welp`] with the given message.
    ///
    /// Note: the `message` argument is evaluated at the call site per Rust's
    /// eager-argument rules — `welp_context(format!(...))` allocates the
    /// formatted `String` even on the `Some` branch. Use [`with_welp_context`]
    /// when the message is built from a non-trivial expression that should
    /// only run on `None`.
    ///
    /// [`with_welp_context`]: Self::with_welp_context
    fn welp_context(self, message: impl Into<String>) -> Result<T, Welp>;

    /// Convert `None` into a [`Welp`] with a lazily-built message. The
    /// closure runs only on the `None` branch.
    fn with_welp_context<S, F>(self, f: F) -> Result<T, Welp>
    where
        S: Into<String>,
        F: FnOnce() -> S;
}

impl<T> WelpOptionExt<T> for Option<T> {
    #[inline]
    #[track_caller]
    fn welp_context(self, message: impl Into<String>) -> Result<T, Welp> {
        // Build directly (no `ok_or_else` closure) so `Welp::new`'s
        // `#[track_caller]` reports the `.welp_context` call site.
        match self {
            Some(value) => Ok(value),
            None => Err(Welp::new(message)),
        }
    }

    #[inline]
    #[track_caller]
    fn with_welp_context<S, F>(self, f: F) -> Result<T, Welp>
    where
        S: Into<String>,
        F: FnOnce() -> S,
    {
        match self {
            Some(value) => Ok(value),
            None => Err(Welp::new(f())),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{RustBacktrace, with_rust_backtrace_override};

    #[cfg(feature = "tracing")]
    fn with_error_subscriber<R>(f: impl FnOnce() -> R) -> R {
        use tracing_subscriber::prelude::*;
        let subscriber = tracing_subscriber::registry().with(tracing_error::ErrorLayer::default());
        tracing::subscriber::with_default(subscriber, f)
    }

    #[test]
    fn new_captures_message() {
        let err = Welp::new("hello");
        assert_eq!(err.message(), Some("hello"));
        assert_eq!(err.to_string(), "hello");
    }

    #[test]
    fn new_accepts_string_and_str() {
        let _: Welp = Welp::new("static");
        let _: Welp = Welp::new(String::from("owned"));
        let _: Welp = Welp::new(format!("formatted {}", 1));
    }

    #[test]
    fn new_captures_traces() {
        let err = Welp::new("oops");
        assert!(err.oopsie_backtrace().is_some());
        assert!(err.oopsie_spantrace().is_some());
    }

    #[test]
    fn new_has_no_source() {
        let err = Welp::new("oops");
        assert!(StdError::source(&err).is_none());
    }

    #[test]
    fn captures_location_at_call_site() {
        let new_line = line!() + 1;
        let traced = Welp::new("solo");
        let loc = traced.oopsie_location().expect("traced location");
        assert!(loc.file().ends_with("welp.rs"));
        assert_eq!(loc.line(), new_line);

        let wrap_line = line!() + 1;
        let sourced = Welp::wrap(std::io::Error::other("x"), "outer");
        let loc = sourced.oopsie_location().expect("sourced location");
        assert_eq!(loc.line(), wrap_line);
    }

    #[test]
    fn wrap_carries_source() {
        let inner = std::io::Error::other("disk full");
        let err = Welp::wrap(inner, "could not write");
        assert_eq!(err.to_string(), "could not write");
        let source = StdError::source(&err).expect("source missing");
        assert_eq!(source.to_string(), "disk full");
    }

    #[test]
    fn wrap_captures_traces_at_wrap_site() {
        let err = Welp::wrap(std::io::Error::other("x"), "msg");
        assert!(err.oopsie_backtrace().is_some());
        assert!(err.oopsie_spantrace().is_some());
    }

    #[test]
    fn wrap_boxed_carries_source() {
        let boxed: BoxError = Box::new(std::io::Error::other("disk full"));
        let err = Welp::wrap_boxed(boxed, "could not write");
        assert_eq!(err.to_string(), "could not write");
        assert_eq!(
            StdError::source(&err).expect("source missing").to_string(),
            "disk full"
        );
    }

    #[test]
    fn wrap_boxed_captures_location_at_call_site() {
        let boxed: BoxError = Box::new(std::io::Error::other("x"));
        let wrap_line = line!() + 1;
        let err = Welp::wrap_boxed(boxed, "outer");
        let loc = err.oopsie_location().expect("wrap_boxed location");
        assert!(loc.file().ends_with("welp.rs"));
        assert_eq!(loc.line(), wrap_line);
    }

    #[test]
    fn wrap_boxed_captures_traces_at_wrap_site() {
        let boxed: BoxError = Box::new(std::io::Error::other("x"));
        let err = Welp::wrap_boxed(boxed, "msg");
        assert!(err.oopsie_backtrace().is_some());
        assert!(err.oopsie_spantrace().is_some());
    }

    #[test]
    fn from_error_delegates_display_to_source() {
        let err = Welp::from_error(std::io::Error::other("disk full"));
        assert_eq!(err.to_string(), "disk full");
        assert!(err.message().is_none());
        let source = StdError::source(&err).expect("source missing");
        assert_eq!(source.to_string(), "disk full");
    }

    #[test]
    fn from_error_source_downcasts_to_original() {
        let err = Welp::from_error(std::io::Error::new(std::io::ErrorKind::NotFound, "missing"));
        let io = StdError::source(&err)
            .and_then(|s| s.downcast_ref::<std::io::Error>())
            .expect("source is the original io::Error");
        assert_eq!(io.kind(), std::io::ErrorKind::NotFound);
    }

    #[test]
    fn welp_on_ok_is_passthrough() {
        let r: Result<i32, std::io::Error> = Ok(42);
        assert_eq!(r.welp().unwrap(), 42);
    }

    #[test]
    fn welp_on_err_delegates_display_and_keeps_source() {
        let r: Result<i32, std::io::Error> = Err(std::io::Error::other("io fail"));
        let err = r.welp().unwrap_err();
        assert_eq!(err.to_string(), "io fail");
        assert!(err.message().is_none());
        assert_eq!(StdError::source(&err).unwrap().to_string(), "io fail");
    }

    #[test]
    fn welp_captures_location_at_call_site() {
        let call_line = line!() + 2;
        let r: Result<i32, std::io::Error> = Err(std::io::Error::other("x"));
        let err = r.welp().unwrap_err();
        let loc = err.oopsie_location().expect("welp location");
        assert!(loc.file().ends_with("welp.rs"));
        assert_eq!(loc.line(), call_line);
    }

    #[cfg(feature = "tracing")]
    #[test]
    fn welp_extracts_origin_trace_from_diagnostic_source() {
        // The inner traces must actually capture (live span + backtraces
        // enabled), otherwise the skip-empty fallback surfaces the wrap-site
        // traces and the ptr-eq checks below are vacuous.
        let outer = with_rust_backtrace_override(RustBacktrace::Enabled, || {
            with_error_subscriber(|| {
                let _g = tracing::info_span!("origin").entered();
                let inner: Result<(), Welp> = Err(Welp::new("inner"));
                inner.welp().unwrap_err()
            })
        });
        let bt = outer.oopsie_backtrace().expect("backtrace");
        assert!(!bt.frames().is_empty());
        #[cfg(feature = "unstable-error-generic-member-access")]
        {
            let source = StdError::source(&outer)
                .and_then(|s| s.downcast_ref::<Welp>())
                .expect("source is the inner Welp");
            let inner_bt = source.oopsie_backtrace().expect("inner backtrace");
            assert!(
                std::ptr::eq(
                    std::ptr::from_ref::<Backtrace>(bt),
                    std::ptr::from_ref::<Backtrace>(inner_bt)
                ),
                "outer should surface the source's (origin-most) backtrace"
            );
        }
    }

    #[test]
    fn welp_context_on_ok_is_passthrough() {
        let r: Result<i32, std::io::Error> = Ok(42);
        let mapped: Result<i32, Welp> = r.welp_context("nope");
        assert_eq!(mapped.unwrap(), 42);
    }

    #[test]
    fn welp_context_on_err_produces_sourced_welp() {
        let r: Result<i32, std::io::Error> = Err(std::io::Error::other("io fail"));
        let err = r.welp_context("read failed").unwrap_err();
        assert_eq!(err.to_string(), "read failed");
        assert_eq!(StdError::source(&err).unwrap().to_string(), "io fail");
    }

    #[test]
    fn with_welp_context_does_not_run_closure_on_ok() {
        let r: Result<i32, std::io::Error> = Ok(1);
        let mapped: Result<i32, Welp> = r.with_welp_context(|_| -> String {
            panic!("should not run");
        });
        mapped.unwrap();
    }

    #[test]
    fn with_welp_context_runs_closure_on_err() {
        let r: Result<i32, std::io::Error> = Err(std::io::Error::other("inner"));
        let err = r
            .with_welp_context(|e| format!("wrapped: {e}"))
            .unwrap_err();
        assert_eq!(err.to_string(), "wrapped: inner");
    }

    #[test]
    fn option_welp_context_on_some_is_passthrough() {
        let opt: Option<i32> = Some(7);
        assert_eq!(opt.welp_context("missing").unwrap(), 7);
    }

    #[test]
    fn option_welp_context_on_none_produces_traced_welp() {
        let opt: Option<i32> = None;
        let err = opt.welp_context("missing").unwrap_err();
        assert_eq!(err.to_string(), "missing");
        assert!(err.oopsie_backtrace().is_some());
        assert!(StdError::source(&err).is_none());
    }

    #[test]
    fn option_with_welp_context_does_not_run_closure_on_some() {
        let opt: Option<i32> = Some(1);
        let mapped: Result<i32, Welp> =
            opt.with_welp_context(|| -> String { panic!("should not run") });
        mapped.unwrap();
    }

    #[test]
    fn option_with_welp_context_runs_closure_on_none() {
        let opt: Option<i32> = None;
        let err = opt
            .with_welp_context(|| format!("missing key {}", "host"))
            .unwrap_err();
        assert_eq!(err.to_string(), "missing key host");
    }

    #[test]
    fn chain_walks_welp_and_its_sources_self_first() {
        use crate::ErrorChainExt as _;

        let err = Welp::wrap(
            Welp::wrap(std::io::Error::other("disk full"), "mid"),
            "outer",
        );
        let messages: Vec<String> = err.chain().map(ToString::to_string).collect();
        assert_eq!(messages, ["outer", "mid", "disk full"]);
        assert_eq!(err.root_cause().to_string(), "disk full");
    }

    #[test]
    fn downcast_ref_hits_matching_direct_source() {
        let err = Welp::wrap(std::io::Error::other("disk full"), "outer");
        let io = err.downcast_ref::<std::io::Error>().expect("source is io");
        assert_eq!(io.to_string(), "disk full");
    }

    #[test]
    fn downcast_ref_misses_on_wrong_type_and_traced() {
        let sourced = Welp::wrap(std::io::Error::other("x"), "outer");
        assert!(sourced.downcast_ref::<std::fmt::Error>().is_none());

        let traced = Welp::new("solo");
        assert!(traced.downcast_ref::<std::io::Error>().is_none());
    }

    #[test]
    fn downcast_mut_borrows_matching_source() {
        let mut err = Welp::wrap(std::io::Error::other("x"), "outer");
        assert!(err.downcast_mut::<std::io::Error>().is_some());
        assert!(err.downcast_mut::<std::fmt::Error>().is_none());
    }

    #[test]
    fn downcast_recovers_matching_source() {
        let err = Welp::wrap(
            std::io::Error::new(std::io::ErrorKind::NotFound, "missing"),
            "outer",
        );
        let io: std::io::Error = err.downcast().expect("source is io");
        assert_eq!(io.kind(), std::io::ErrorKind::NotFound);
    }

    #[test]
    fn downcast_only_targets_the_direct_source() {
        // The direct source is the middle `Welp`; the io::Error is one layer
        // deeper and must not be reached by a direct downcast.
        let err = Welp::wrap(Welp::wrap(std::io::Error::other("x"), "mid"), "outer");
        let err = err
            .downcast::<std::io::Error>()
            .expect_err("io is not the direct source");
        assert!(err.downcast_ref::<Welp>().is_some());
    }

    #[test]
    fn downcast_err_preserves_message_location_and_traces() {
        let line = line!() + 1;
        let err = Welp::wrap(std::io::Error::other("inner"), "outer");
        let recovered = err
            .downcast::<std::fmt::Error>()
            .expect_err("wrong target type");
        assert_eq!(recovered.message(), Some("outer"));
        assert_eq!(recovered.to_string(), "outer");
        let loc = recovered.oopsie_location().expect("location preserved");
        assert_eq!(loc.line(), line);
        assert!(recovered.oopsie_backtrace().is_some());
        // The original source survives the failed round-trip.
        assert_eq!(StdError::source(&recovered).unwrap().to_string(), "inner");
    }

    #[test]
    fn downcast_on_traced_returns_self() {
        let err = Welp::new("solo");
        let recovered = err
            .downcast::<std::io::Error>()
            .expect_err("traced has no source");
        assert_eq!(recovered.message(), Some("solo"));
    }

    #[test]
    fn debug_includes_source_for_sourced() {
        let err = Welp::wrap(std::io::Error::other("inner"), "outer");
        let dbg = format!("{err:?}");
        assert!(dbg.contains("outer"));
        assert!(dbg.contains("inner"));
    }

    #[test]
    fn debug_omits_source_for_traced() {
        let err = Welp::new("solo");
        let dbg = format!("{err:?}");
        assert!(dbg.contains("solo"));
        assert!(!dbg.contains("source"));
    }

    #[cfg(all(feature = "unstable-error-generic-member-access", feature = "tracing"))]
    #[test]
    fn traced_provides_traces_via_request_ref() {
        // Only captured traces are provided, so capture must succeed here.
        let err = with_rust_backtrace_override(RustBacktrace::Enabled, || {
            with_error_subscriber(|| {
                let _g = tracing::info_span!("solo").entered();
                Welp::new("solo")
            })
        });
        assert!(core::error::request_ref::<Backtrace>(&err).is_some());
        assert!(core::error::request_ref::<SpanTrace>(&err).is_some());
    }

    #[cfg(feature = "unstable-error-generic-member-access")]
    #[test]
    fn empty_traces_are_not_provided() {
        // No subscriber and backtraces disabled → both traces are empty and
        // must be withheld from the (first-wins) provider chain. Without the
        // tracing feature the span-trace stub is never captured, so its slot
        // stays empty too.
        let err = with_rust_backtrace_override(RustBacktrace::Disabled, || Welp::new("solo"));
        assert!(core::error::request_ref::<Backtrace>(&err).is_none());
        assert!(core::error::request_ref::<SpanTrace>(&err).is_none());
    }

    #[cfg(feature = "tracing")]
    #[test]
    fn sourced_recovers_traces_from_diagnostic_source() {
        // The inner traces must actually capture something (live span +
        // backtraces enabled), otherwise the skip-empty filters surface the
        // wrap-site traces instead and the ptr-eq assertions are meaningless.
        let outer = with_rust_backtrace_override(RustBacktrace::Enabled, || {
            with_error_subscriber(|| {
                let _g = tracing::info_span!("origin").entered();
                Welp::wrap(Welp::new("inner"), "outer")
            })
        });
        let bt = outer.oopsie_backtrace().expect("backtrace");
        let st = outer.oopsie_spantrace().expect("spantrace");
        // On nightly the Provider API is available, so the source's own
        // (deeper) trace must win over the wrap-site capture.
        #[cfg(feature = "unstable-error-generic-member-access")]
        {
            let source = StdError::source(&outer)
                .and_then(|s| s.downcast_ref::<Welp>())
                .expect("source is the inner Welp");
            let inner_bt = source.oopsie_backtrace().expect("inner backtrace");
            let inner_st = source.oopsie_spantrace().expect("inner spantrace");
            assert!(
                std::ptr::eq(
                    std::ptr::from_ref::<Backtrace>(bt),
                    std::ptr::from_ref::<Backtrace>(inner_bt)
                ),
                "outer should surface the source's backtrace, not the wrap-site one"
            );
            assert!(
                std::ptr::eq(
                    std::ptr::from_ref::<SpanTrace>(st),
                    std::ptr::from_ref::<SpanTrace>(inner_st)
                ),
                "outer should surface the source's spantrace, not the wrap-site one"
            );
        }
        #[cfg(not(feature = "unstable-error-generic-member-access"))]
        {
            let _ = (bt, st);
        }
    }

    #[cfg(all(feature = "unstable-error-generic-member-access", feature = "tracing"))]
    #[test]
    fn sourced_forwards_source_provide() {
        // Only captured traces are provided, so capture must succeed here.
        let outer = with_rust_backtrace_override(RustBacktrace::Enabled, || {
            with_error_subscriber(|| {
                let _g = tracing::info_span!("origin").entered();
                Welp::wrap(Welp::new("inner"), "outer")
            })
        });
        assert!(core::error::request_ref::<Backtrace>(&outer).is_some());
        assert!(core::error::request_ref::<SpanTrace>(&outer).is_some());
    }

    #[test]
    fn sourced_falls_back_to_own_traces_for_foreign_source() {
        let outer = Welp::wrap(std::io::Error::other("x"), "outer");
        assert!(outer.oopsie_backtrace().is_some());
        assert!(outer.oopsie_spantrace().is_some());
    }

    #[cfg(feature = "tracing")]
    #[test]
    fn empty_source_spantrace_does_not_shadow_wrap_site_capture() {
        let inner = Welp::new("inner"); // no subscriber → empty spantrace
        assert!(!inner.oopsie_spantrace().unwrap().is_captured());
        let outer = with_error_subscriber(|| {
            let _g = tracing::info_span!("wrap_site").entered();
            Welp::wrap(inner, "outer")
        });
        assert!(outer.oopsie_spantrace().unwrap().is_captured());
    }

    #[test]
    fn empty_source_backtrace_does_not_shadow_wrap_site_capture() {
        let inner = with_rust_backtrace_override(RustBacktrace::Disabled, || Welp::new("inner"));
        let outer =
            with_rust_backtrace_override(RustBacktrace::Enabled, || Welp::wrap(inner, "outer"));
        assert!(!outer.oopsie_backtrace().unwrap().frames().is_empty());
    }

    // Reaching the source's trace requires the Provider API: `wrap` is
    // generic, so no construction-time extraction is possible (see `wrap`),
    // and on stable the accessors cannot descend into the type-erased source.
    #[cfg(all(feature = "unstable-error-generic-member-access", feature = "tracing"))]
    #[test]
    fn wrap_surfaces_captured_source_spantrace() {
        let inner = with_error_subscriber(|| {
            let _g = tracing::info_span!("origin").entered();
            Welp::new("inner")
        });
        assert!(inner.oopsie_spantrace().unwrap().is_captured());
        let outer = Welp::wrap(inner, "outer"); // no live span here
        assert!(outer.oopsie_spantrace().unwrap().is_captured());
    }

    // Compile-time guarantees about `Welp`'s shape.
    const _: () = {
        const fn assert_send<T: Send>() {}
        const fn assert_sync<T: Sync>() {}
        const fn assert_static<T: 'static>() {}
        assert_send::<Welp>();
        assert_sync::<Welp>();
        assert_static::<Welp>();
    };

    #[test]
    fn welp_size_is_pinned() {
        // `Sourced` dominates: fat `Box<str>` message + fat `Box<dyn Error>`
        // source + thin boxed traces + the inline `&'static Location` — six
        // words, with no spare niche for the discriminant. The message-free form
        // reuses `Sourced` with a `None` message — the `Box<str>` null niche
        // absorbs the `Option` tag — so it adds no width.
        assert_eq!(std::mem::size_of::<Welp>(), 48);
    }
}
