use core::error;
use std::{panic::Location, rc::Rc, sync::Arc};

/// Builds a target error from a context selector and a source error of type `E`.
///
/// Implemented automatically by the context selector structs generated by
/// `#[derive(Oopsie)]`. You rarely implement this manually, and most code never
/// names it directly — [`ResultExt::context`] and [`OptionExt::context`] drive
/// it. Reach for it only when building an error outside a `Result`/`Option`,
/// e.g. `selector.build_error(source)`.
#[diagnostic::on_unimplemented(
    message = "`{Self}` cannot build an error from a source of type `{E}`",
    label = "this selector does not accept a `{E}` source",
    note = "a selector whose variant has a `source` field builds from that field's exact type — \
            `.context(...)` on a `Result` requires the selector's source type to match the \
            `Result`'s error",
    note = "a selector with no `source` field is a leaf: build it with `.build()` / `.fail()`, or \
            attach it to an `Option` with `.context(...)` — not to a `Result`'s error"
)]
pub trait Contextual<E> {
    /// The destination error type being built by this context selector.
    type Destination;

    /// Build the target error from this context selector and the source error.
    #[track_caller]
    fn build_error(self, source: E) -> Self::Destination;
}

/// A value that fills itself in when an error is constructed.
///
/// A field marked `#[oopsie(capture)]` is populated by calling [`capture`](Self::capture) at
/// construction time, so it is dropped from the context selector and never supplied by the
/// caller. The library implements `Capturable` for backtraces, span-traces (with the `tracing`
/// feature), [`SystemTime`](std::time::SystemTime)/[`Instant`](std::time::Instant) timestamps
/// (plus `chrono::DateTime<Local>` under the `chrono` feature and `jiff::Timestamp`/`jiff::Zoned`
/// under the `jiff` feature), the caller [`Location`], and the environment
/// snapshots in the `extras` module. Implement it for your own type to capture anything
/// else, such as a request or thread id.
///
/// It also covers `Box`, `Rc`, `Arc`, and tuples, so one field can wrap or combine several
/// captured values.
pub trait Capturable {
    /// Capture a fresh value at the point the error is constructed.
    #[track_caller]
    fn capture() -> Self;

    /// Capture for an error that wraps `source`, reusing equivalent data already on `source`
    /// when that is more useful than a fresh capture.
    ///
    /// The default ignores `source` and calls [`capture`](Self::capture); trace types override
    /// it so wrapping another diagnostic preserves the source's original call site instead of
    /// recording the wrap site. Rarely called or overridden by hand.
    #[track_caller]
    #[inline]
    fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self
    where
        Self: Sized,
    {
        let _ = source;
        Self::capture()
    }
}

impl<T: Capturable> Capturable for Box<T> {
    #[track_caller]
    #[inline]
    fn capture() -> Self {
        Self::new(T::capture())
    }

    #[track_caller]
    #[inline]
    fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self {
        Self::new(T::capture_or_extract(source))
    }
}

impl<T: Capturable> Capturable for Rc<T> {
    #[track_caller]
    #[inline]
    fn capture() -> Self {
        Self::new(T::capture())
    }

    #[track_caller]
    #[inline]
    fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self {
        Self::new(T::capture_or_extract(source))
    }
}

impl<T: Capturable> Capturable for Arc<T> {
    #[track_caller]
    #[inline]
    fn capture() -> Self {
        Self::new(T::capture())
    }

    #[track_caller]
    #[inline]
    fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self {
        Self::new(T::capture_or_extract(source))
    }
}

macro_rules! impl_capturable_tuples {
    // entry: kick off with an empty accumulator
    ($($name:ident),* $(,)?) => {
        impl_capturable_tuples!(@acc [] $($name,)*);
    };
    // move one ident into the accumulator, emit for the accumulator + it
    (@acc [$($acc:ident,)*] $head:ident, $($rest:ident,)*) => {
        impl_capturable_tuples!(@impl $($acc,)* $head,);
        impl_capturable_tuples!(@acc [$($acc,)* $head,] $($rest,)*);
    };
    // remaining list empty: done
    (@acc [$($acc:ident,)*]) => {};
    (@impl $($name:ident,)+) => {
        impl<$($name: Capturable),+> Capturable for ($($name,)+) {
            #[track_caller]
            #[inline]
            fn capture() -> Self {
                ($($name::capture(),)+)
            }
            #[track_caller]
            #[inline]
            fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self {
                ($($name::capture_or_extract(source),)+)
            }
        }
    };
}
impl_capturable_tuples!(A, B, C, D, E, F, G, H, I);

impl Capturable for std::time::SystemTime {
    #[inline]
    fn capture() -> Self {
        Self::now()
    }
}

impl Capturable for std::time::Instant {
    #[inline]
    fn capture() -> Self {
        Self::now()
    }
}
impl Capturable for &'static Location<'static> {
    #[inline]
    #[track_caller]
    fn capture() -> Self {
        Location::caller()
    }

    #[track_caller]
    #[inline]
    fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self {
        // Capture eagerly so `#[track_caller]` resolves to this call's caller; a
        // fn-pointer passed to `unwrap_or_else` would lose that and report a
        // stdlib frame instead.
        let here = Location::caller();
        source.oopsie_location().unwrap_or(here)
    }
}

#[cfg(feature = "chrono")]
impl Capturable for chrono::DateTime<chrono::Local> {
    #[inline]
    fn capture() -> Self {
        chrono::Local::now()
    }
}

#[cfg(feature = "jiff")]
impl Capturable for jiff::Timestamp {
    #[inline]
    fn capture() -> Self {
        Self::now()
    }
}

#[cfg(feature = "jiff")]
impl Capturable for jiff::Zoned {
    #[inline]
    fn capture() -> Self {
        Self::now()
    }
}

/// The stand-in "source" a leaf selector (one with no `source` field) builds
/// from. `Option::context` uses it as the source when there is no error value to
/// attach. You normally reach a leaf error through `.build()` / `.fail()` /
/// `Option::context`, so you rarely name this directly.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct NoSource;

/// Normalize any source-error value to a `&(dyn Error + 'static)`.
pub trait AsErrorSource {
    /// Borrow this value as a `&(dyn Error + 'static)`.
    fn as_error_source(&self) -> &(dyn error::Error + 'static);
}

impl<T: error::Error + 'static> AsErrorSource for T {
    #[inline]
    fn as_error_source(&self) -> &(dyn error::Error + 'static) {
        self
    }
}

impl AsErrorSource for dyn error::Error + 'static {
    #[inline]
    fn as_error_source(&self) -> &(dyn error::Error + 'static) {
        self
    }
}

impl AsErrorSource for dyn error::Error + Send + 'static {
    #[inline]
    fn as_error_source(&self) -> &(dyn error::Error + 'static) {
        self
    }
}

impl AsErrorSource for dyn error::Error + Send + Sync + 'static {
    #[inline]
    fn as_error_source(&self) -> &(dyn error::Error + 'static) {
        self
    }
}

/// Extension trait on [`Result`] for ergonomic error context wrapping.
///
/// Import via `use oopsie::prelude::*` or `use oopsie::ResultExt`.
///
/// # Example
///
/// ```
/// use oopsie::{Oopsie, ResultExt as _};
///
/// #[derive(Debug, Oopsie)]
/// #[oopsie(module(false))]
/// enum IoError {
///     #[oopsie("IO failed: {source}")]
///     Failed { source: std::io::Error },
/// }
///
/// # fn main() {
/// let result: Result<(), std::io::Error> = Err(std::io::Error::other("disk full"));
/// let err = result.context(Failed).unwrap_err();
/// assert_eq!(err.to_string(), "IO failed: disk full");
/// # }
/// ```
pub trait ResultExt<T, E> {
    /// Wrap the error with an eagerly-evaluated context selector.
    fn context<C>(self, context: C) -> Result<T, C::Destination>
    where
        C: Contextual<E>;

    /// Wrap the error with a lazily-evaluated context selector.
    fn with_context<F, C>(self, context: F) -> Result<T, C::Destination>
    where
        F: FnOnce(&E) -> C,
        C: Contextual<E>;

    /// Convert the error into a boxed trait object (`Box<dyn Error + Send + Sync>`).
    ///
    /// If `E` is itself a `Box<C>`, the result nests boxes: downcasting must
    /// target `Box<C>`, not `C`.
    fn boxed(self) -> Result<T, Box<dyn error::Error + Send + Sync + 'static>>
    where
        E: error::Error + Send + Sync + 'static;

    /// Convert the error into a boxed trait object (no `Send`/`Sync` bounds).
    ///
    /// If `E` is itself a `Box<C>`, the result nests boxes: downcasting must
    /// target `Box<C>`, not `C`.
    fn boxed_local(self) -> Result<T, Box<dyn error::Error + 'static>>
    where
        E: error::Error + 'static;
}

impl<T, E> ResultExt<T, E> for Result<T, E> {
    #[inline]
    #[track_caller]
    fn context<C>(self, context: C) -> Result<T, C::Destination>
    where
        C: Contextual<E>,
    {
        // Call `build_error` directly (not inside a `map_err` closure) so
        // `#[track_caller]` forwards the caller's `Location` through to any
        // captured location field.
        match self {
            Ok(value) => Ok(value),
            Err(error) => Err(context.build_error(error)),
        }
    }

    #[inline]
    #[track_caller]
    fn with_context<F, C>(self, context: F) -> Result<T, C::Destination>
    where
        F: FnOnce(&E) -> C,
        C: Contextual<E>,
    {
        match self {
            Ok(value) => Ok(value),
            Err(error) => {
                let selector = context(&error);
                Err(selector.build_error(error))
            }
        }
    }

    #[inline]
    fn boxed(self) -> Result<T, Box<dyn error::Error + Send + Sync + 'static>>
    where
        E: error::Error + Send + Sync + 'static,
    {
        self.map_err(|error| Box::new(error) as Box<dyn error::Error + Send + Sync + 'static>)
    }

    #[inline]
    fn boxed_local(self) -> Result<T, Box<dyn error::Error + 'static>>
    where
        E: error::Error + 'static,
    {
        self.map_err(|error| Box::new(error) as Box<dyn error::Error + 'static>)
    }
}

/// Extension trait on [`Option`] for converting `None` into a typed error.
///
/// Import via `use oopsie::prelude::*` or `use oopsie::OptionExt`.
///
/// # Example
///
/// ```
/// use oopsie::{Oopsie, OptionExt as _};
///
/// #[derive(Debug, Oopsie)]
/// #[oopsie(module(false))]
/// enum LookupError {
///     #[oopsie("Key not found: {key}")]
///     Missing { key: String },
/// }
///
/// # fn main() {
/// let opt: Option<i32> = None;
/// let err = opt.context(Missing { key: "host" }).unwrap_err();
/// assert_eq!(err.to_string(), "Key not found: host");
/// # }
/// ```
pub trait OptionExt<T> {
    /// Convert `None` into an error with an eagerly-evaluated context selector.
    fn context<C>(self, context: C) -> Result<T, C::Destination>
    where
        C: Contextual<NoSource>;

    /// Convert `None` into an error with a lazily-evaluated context selector.
    fn with_context<F, C>(self, context: F) -> Result<T, C::Destination>
    where
        F: FnOnce() -> C,
        C: Contextual<NoSource>;
}

impl<T> OptionExt<T> for Option<T> {
    #[inline]
    #[track_caller]
    fn context<C>(self, context: C) -> Result<T, C::Destination>
    where
        C: Contextual<NoSource>,
    {
        match self {
            Some(value) => Ok(value),
            None => Err(context.build_error(NoSource)),
        }
    }

    #[inline]
    #[track_caller]
    fn with_context<F, C>(self, context: F) -> Result<T, C::Destination>
    where
        F: FnOnce() -> C,
        C: Contextual<NoSource>,
    {
        match self {
            Some(value) => Ok(value),
            None => Err(context().build_error(NoSource)),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::error::Error as StdError;
    use std::fmt;
    use std::time::SystemTime;

    #[cfg(feature = "jiff")]
    #[test]
    fn captures_current_jiff_timestamp() {
        // Lower bound only: wall-clock `now()` is non-monotonic, so an upper
        // bound could flake on a backward clock step.
        let before = jiff::Timestamp::now();
        let captured = <jiff::Timestamp as Capturable>::capture();
        assert!(captured >= before, "captured {captured} predates {before}");
    }

    #[cfg(feature = "jiff")]
    #[test]
    fn captures_current_jiff_zoned() {
        let before = jiff::Timestamp::now();
        let captured = <jiff::Zoned as Capturable>::capture().timestamp();
        assert!(captured >= before, "captured {captured} predates {before}");
    }

    #[derive(Debug)]
    struct DiagOnly;

    impl fmt::Display for DiagOnly {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            f.write_str("diag only")
        }
    }

    impl std::error::Error for DiagOnly {}
    impl crate::Diagnostic for DiagOnly {}

    // Test error types for trait implementations
    #[derive(Debug)]
    struct SimpleError {
        message: String,
    }

    impl fmt::Display for SimpleError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "{}", self.message)
        }
    }

    impl std::error::Error for SimpleError {}

    #[derive(Debug)]
    struct SourceError {
        message: String,
    }

    impl fmt::Display for SourceError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "{}", self.message)
        }
    }

    impl std::error::Error for SourceError {}

    #[derive(Debug)]
    struct ChainError {
        message: String,
        source: Box<SourceError>,
    }

    impl fmt::Display for ChainError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "{}", self.message)
        }
    }

    impl std::error::Error for ChainError {
        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
            Some(&*self.source)
        }
    }

    // Test Contextual selector implementations
    struct SimpleSelector;

    impl Contextual<NoSource> for SimpleSelector {
        type Destination = SimpleError;

        fn build_error(self, _source: NoSource) -> SimpleError {
            SimpleError {
                message: "simple error".to_owned(),
            }
        }
    }

    struct ChainSelector;

    impl Contextual<SourceError> for ChainSelector {
        type Destination = ChainError;

        fn build_error(self, source: SourceError) -> ChainError {
            ChainError {
                message: "chain error".to_owned(),
                source: Box::new(source),
            }
        }
    }

    #[test]
    fn test_result_ext_context_ok() {
        let result: Result<i32, SourceError> = Ok(42);
        let chained: Result<i32, ChainError> = result.context(ChainSelector);
        assert!(chained.is_ok());
        assert_eq!(chained.unwrap(), 42);
    }

    #[test]
    fn test_result_ext_context_err() {
        let result: Result<i32, SourceError> = Err(SourceError {
            message: "source failed".to_owned(),
        });
        let chained: Result<i32, ChainError> = result.context(ChainSelector);
        assert!(chained.is_err());
        let err = chained.unwrap_err();
        assert_eq!(err.message, "chain error");
        assert_eq!(StdError::source(&err).unwrap().to_string(), "source failed");
    }

    #[test]
    fn test_result_ext_with_context_ok() {
        let result: Result<i32, SourceError> = Ok(42);
        let chained: Result<i32, ChainError> = result.with_context(|_| ChainSelector);
        assert!(chained.is_ok());
        assert_eq!(chained.unwrap(), 42);
    }

    #[test]
    fn test_result_ext_with_context_err_closure_called() {
        let mut closure_called = false;
        let result: Result<i32, SourceError> = Err(SourceError {
            message: "source failed".to_owned(),
        });
        let chained: Result<i32, ChainError> = result.with_context(|source| {
            closure_called = true;
            // Verify we can access the source in the closure
            assert_eq!(source.message, "source failed");
            ChainSelector
        });
        assert!(closure_called);
        chained.unwrap_err();
    }

    #[test]
    fn test_option_ext_context_some() {
        let option: Option<i32> = Some(42);
        let result: Result<i32, SimpleError> = option.context(SimpleSelector);
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), 42);
    }

    #[test]
    fn test_option_ext_context_none() {
        let option: Option<i32> = None;
        let result: Result<i32, SimpleError> = option.context(SimpleSelector);
        assert!(result.is_err());
        assert_eq!(result.unwrap_err().message, "simple error");
    }

    #[test]
    fn test_option_ext_with_context_some() {
        let option: Option<i32> = Some(42);
        let result: Result<i32, SimpleError> = option.with_context(|| SimpleSelector);
        assert!(result.is_ok());
        assert_eq!(result.unwrap(), 42);
    }

    #[test]
    fn test_option_ext_with_context_none_closure_called() {
        let mut closure_called = false;
        let option: Option<i32> = None;
        let result: Result<i32, SimpleError> = option.with_context(|| {
            closure_called = true;
            SimpleSelector
        });
        assert!(closure_called);
        result.unwrap_err();
    }

    const _: () = {
        // Verify that Box<T> implements Capturable when T: Capturable
        const fn is_capturable<T: Capturable>() {}
        is_capturable::<Box<crate::Backtrace>>();
    };

    #[cfg(feature = "tracing")]
    #[test]
    fn tuple_capture_produces_both_elements() {
        use crate::{Backtrace, SpanTrace};
        // Tuple capture yields both traces; neither call panics.
        let (bt, st): (Backtrace, SpanTrace) = <(Backtrace, SpanTrace) as Capturable>::capture();
        let _ = bt.frames();
        let _ = st.status();
    }

    #[cfg(feature = "tracing")]
    const _: () = {
        const fn is_capturable<T: Capturable>() {}
        is_capturable::<Box<(crate::Backtrace, crate::SpanTrace)>>();
        is_capturable::<(crate::Backtrace, crate::SpanTrace)>();
    };

    #[cfg(feature = "tracing")]
    #[test]
    fn tuple_capture_ext_extracts_both_from_source() {
        use crate::{
            Backtrace, Capturable, Diagnostic, RustBacktrace, SpanTrace,
            with_rust_backtrace_override,
        };
        use std::fmt;

        #[derive(Debug)]
        struct Src {
            backtrace: Backtrace,
            spantrace: SpanTrace,
        }
        impl fmt::Display for Src {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                f.write_str("src")
            }
        }
        impl std::error::Error for Src {}
        impl Diagnostic for Src {
            fn oopsie_backtrace(&self) -> Option<&Backtrace> {
                Some(&self.backtrace)
            }
            fn oopsie_spantrace(&self) -> Option<&SpanTrace> {
                Some(&self.spantrace)
            }
        }

        with_rust_backtrace_override(RustBacktrace::Enabled, || {
            let src = Src {
                backtrace: Backtrace::capture(),
                spantrace: SpanTrace::capture(),
            };
            let source_frames = src.backtrace.frames().len();
            assert!(
                source_frames > 0,
                "backtrace must be enabled for this test to be probative"
            );
            let extracted = <(Backtrace, SpanTrace) as Capturable>::capture_or_extract(&src);
            // The extracted backtrace must reuse the source's frame count, proving
            // extraction (not a fresh capture).
            assert_eq!(extracted.0.frames().len(), source_frames);
        });
    }

    #[cfg(feature = "tracing")]
    const _: () = {
        const fn is_capturable<T: Capturable>() {}
        is_capturable::<Box<(crate::Backtrace, crate::SpanTrace)>>();
        is_capturable::<(crate::Backtrace, crate::SpanTrace)>();
    };

    // Destination error whose source is a type-erased boxed error, exercising
    // the `result.boxed().context(...)` chaining ergonomic. The `Send + Sync`
    // bound on the field is what `boxed` (vs `boxed_local`) makes possible.
    #[derive(Debug)]
    struct BoxedChainError {
        message: String,
        source: Box<dyn StdError + Send + Sync + 'static>,
    }

    impl fmt::Display for BoxedChainError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "{}", self.message)
        }
    }

    impl StdError for BoxedChainError {
        fn source(&self) -> Option<&(dyn StdError + 'static)> {
            Some(&*self.source)
        }
    }

    struct BoxedSelector;

    impl Contextual<Box<dyn StdError + Send + Sync + 'static>> for BoxedSelector {
        type Destination = BoxedChainError;

        fn build_error(self, source: Box<dyn StdError + Send + Sync + 'static>) -> BoxedChainError {
            BoxedChainError {
                message: "boxed chain error".to_owned(),
                source,
            }
        }
    }

    #[derive(Debug)]
    struct BoxedLocalChainError {
        source: Box<dyn StdError + 'static>,
    }

    impl fmt::Display for BoxedLocalChainError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "boxed local chain error")
        }
    }

    impl StdError for BoxedLocalChainError {
        fn source(&self) -> Option<&(dyn StdError + 'static)> {
            Some(&*self.source)
        }
    }

    struct BoxedLocalSelector;

    impl Contextual<Box<dyn StdError + 'static>> for BoxedLocalSelector {
        type Destination = BoxedLocalChainError;

        fn build_error(self, source: Box<dyn StdError + 'static>) -> BoxedLocalChainError {
            BoxedLocalChainError { source }
        }
    }

    #[test]
    fn boxed_passes_through_ok() {
        let result: Result<i32, SourceError> = Ok(7);
        assert_eq!(result.boxed().unwrap(), 7);
    }

    #[test]
    fn boxed_erases_err_but_preserves_display_and_concrete_type() {
        let result: Result<i32, SourceError> = Err(SourceError {
            message: "boom".to_owned(),
        });
        let err = result.boxed().unwrap_err();
        assert_eq!(err.to_string(), "boom");
        // Erasure is reversible: the concrete type is still recoverable.
        assert!(err.downcast_ref::<SourceError>().is_some());
    }

    #[test]
    fn boxed_local_erases_err_but_preserves_display_and_concrete_type() {
        let result: Result<i32, SourceError> = Err(SourceError {
            message: "boom".to_owned(),
        });
        let err = result.boxed_local().unwrap_err();
        assert_eq!(err.to_string(), "boom");
        assert!(err.downcast_ref::<SourceError>().is_some());
    }

    #[test]
    fn boxed_then_context_chains_erased_source() {
        let result: Result<i32, SourceError> = Err(SourceError {
            message: "io broke".to_owned(),
        });
        let chained: Result<i32, BoxedChainError> = result.boxed().context(BoxedSelector);
        let err = chained.unwrap_err();
        assert_eq!(err.message, "boxed chain error");
        assert_eq!(StdError::source(&err).unwrap().to_string(), "io broke");
    }

    #[test]
    fn boxed_then_context_passes_through_ok() {
        let result: Result<i32, SourceError> = Ok(42);
        let chained: Result<i32, BoxedChainError> = result.boxed().context(BoxedSelector);
        assert_eq!(chained.unwrap(), 42);
    }

    #[test]
    fn boxed_then_with_context_chains_erased_source() {
        let result: Result<i32, SourceError> = Err(SourceError {
            message: "io broke".to_owned(),
        });
        let chained: Result<i32, BoxedChainError> = result.boxed().with_context(|source| {
            // The closure sees the already-erased boxed error.
            assert_eq!(source.to_string(), "io broke");
            BoxedSelector
        });
        assert_eq!(
            StdError::source(&chained.unwrap_err()).unwrap().to_string(),
            "io broke"
        );
    }

    #[test]
    fn boxed_local_then_context_chains_erased_source() {
        let result: Result<i32, SourceError> = Err(SourceError {
            message: "io broke".to_owned(),
        });
        let chained: Result<i32, BoxedLocalChainError> =
            result.boxed_local().context(BoxedLocalSelector);
        assert_eq!(
            StdError::source(&chained.unwrap_err()).unwrap().to_string(),
            "io broke"
        );
    }

    #[test]
    fn boxed_unifies_heterogeneous_error_types() {
        // `boxed()?` collapses distinct concrete error types to one boxed type,
        // letting a single function signature absorb either.
        fn run(fail_with_source: bool) -> Result<(), Box<dyn StdError + Send + Sync + 'static>> {
            if fail_with_source {
                Err(SourceError {
                    message: "source".to_owned(),
                })
                .boxed()?;
            } else {
                Err(SimpleError {
                    message: "simple".to_owned(),
                })
                .boxed()?;
            }
            Ok(())
        }
        assert_eq!(run(true).unwrap_err().to_string(), "source");
        assert_eq!(run(false).unwrap_err().to_string(), "simple");
    }

    #[test]
    fn boxed_on_boxed_error_downcasts_to_the_box() {
        let result: Result<(), Box<SourceError>> = Err(Box::new(SourceError {
            message: "boom".to_owned(),
        }));
        let err = result.boxed().unwrap_err();
        assert!(err.downcast_ref::<SourceError>().is_none());
        assert!(err.downcast_ref::<Box<SourceError>>().is_some());
    }

    const _: () = {
        // `boxed` yields a thread-portable error; `boxed_local` does not.
        const fn is_send_sync<T: Send + Sync>() {}
        is_send_sync::<Box<dyn StdError + Send + Sync + 'static>>();
    };

    #[derive(Debug)]
    struct WithLoc(&'static std::panic::Location<'static>);

    impl fmt::Display for WithLoc {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            f.write_str("with loc")
        }
    }

    impl StdError for WithLoc {}

    impl crate::Diagnostic for WithLoc {
        fn oopsie_location(&self) -> Option<&'static std::panic::Location<'static>> {
            Some(self.0)
        }
    }

    #[test]
    fn location_captures_caller_and_extracts_from_source() {
        // `capture` records the call site.
        let line = line!() + 1;
        let loc = <&'static std::panic::Location<'static> as Capturable>::capture();
        assert!(loc.file().ends_with("traits.rs"));
        assert_eq!(loc.line(), line);

        // `capture_or_extract` prefers a source that exposes a location.
        let src = WithLoc(std::panic::Location::caller());
        let extracted =
            <&'static std::panic::Location<'static> as Capturable>::capture_or_extract(&src);
        assert!(
            std::ptr::eq(extracted, src.0),
            "must reuse the source's location"
        );

        // A source with no location falls back to a fresh capture.
        let fresh =
            <&'static std::panic::Location<'static> as Capturable>::capture_or_extract(&DiagOnly);
        assert!(fresh.file().ends_with("traits.rs"));
    }

    #[test]
    fn system_time_captures_now_and_never_extracts() {
        let before = SystemTime::now();
        let captured = <SystemTime as Capturable>::capture();
        assert!(captured >= before);
        let extracted = <SystemTime as Capturable>::capture_or_extract(&DiagOnly);
        assert!(extracted >= before);
    }

    #[test]
    fn location_captures() {
        let location = <&'static Location<'static> as Capturable>::capture();
        let expected_line = line!() - 1; // capture() is 1 line above
        assert_eq!(location.line(), expected_line);
        assert_eq!(location.file(), file!());
    }

    #[test]
    fn location_captures_nested() {
        #[track_caller]
        fn capture_location() -> &'static Location<'static> {
            <&'static Location<'static> as Capturable>::capture()
        }
        let location = capture_location();
        let expected_line = line!() - 1; // capture() is 1 line above
        assert_eq!(location.line(), expected_line);
        assert_eq!(location.file(), file!());
    }

    #[test]
    fn complex_location_captures() {
        type Loc = &'static Location<'static>;
        type LocTimeLoc = (Loc, SystemTime, Loc);
        let location =
            <(Box<LocTimeLoc>, Rc<LocTimeLoc>, Arc<LocTimeLoc>) as Capturable>::capture();
        let expected_line = line!() - 1; // capture() is 1 line above
        let expected_col = 3 * 4 + 1; // column of the first `location` in the tuple
        let (box_loc, rc_loc, arc_loc) = location;
        for (loc1, _, loc2) in [&*box_loc, &*rc_loc, &*arc_loc] {
            for loc in [loc1, loc2] {
                assert_eq!(loc.file(), file!());
                assert_eq!(loc.line(), expected_line);
                assert_eq!(loc.column(), expected_col);
            }
        }
    }
}
