use snafu::{prelude::*, Backtrace};

type BoxError = Box<dyn std::error::Error>;

#[derive(Debug, Snafu)]
enum Error<'a, 'x, A, Y> {
    Everything {
        source: BoxError,
        name: &'a str,
        length: A,
        backtrace: Backtrace,
    },
    Lifetime {
        key: &'x i32,
    },
    Type {
        value: Y,
    },
}

fn cause_error() -> Result<(), BoxError> {
    Ok(())
}

fn example<'s, 'k, V>(name: &'s str, key: &'k i32, value: V) -> Result<(), Error<'s, 'k, usize, V>>
where
    V: std::fmt::Debug,
{
    let length = name.len();

    cause_error().context(EverythingSnafu { name, length })?;

    if name == "alice" {
        return LifetimeSnafu { key }.fail();
    }

    if name == "bob" {
        return TypeSnafu { value }.fail();
    }

    Ok(())
}

#[test]
fn implements_error() {
    let name = String::from("hello");
    let key = Box::new(42);
    let value = vec![false];

    example(&name, &key, value).unwrap();
}

mod bounds {
    mod inline {
        use snafu::prelude::*;
        use std::fmt::{Debug, Display};

        #[derive(Debug, Snafu)]
        pub struct ApiError<T: Debug + Display>(Error<T>);

        #[derive(Debug, Snafu)]
        enum Error<T: Display> {
            #[snafu(display("Boom: {value}"))]
            _Boom { value: T },

            #[snafu(whatever, display("{message}"))]
            Whatever {
                message: String,
                #[snafu(source(from(Box<dyn std::error::Error>, Some)))]
                source: Option<Box<dyn std::error::Error>>,
            },
        }

        #[test]
        fn implements_error() {
            fn check_bounds<T: std::error::Error>() {}
            check_bounds::<Error<i32>>();
            check_bounds::<ApiError<i32>>();
        }
    }

    mod where_clause {
        use snafu::prelude::*;
        use std::fmt::{Debug, Display};

        #[derive(Debug, Snafu)]
        pub struct ApiError<T>(Error<T>)
        where
            T: Debug + Display;

        #[derive(Debug, Snafu)]
        enum Error<T>
        where
            T: Display,
        {
            #[snafu(display("Boom: {value}"))]
            _Boom { value: T },

            #[snafu(whatever, display("{message}"))]
            Whatever {
                message: String,
                #[snafu(source(from(Box<dyn std::error::Error>, Some)))]
                source: Option<Box<dyn std::error::Error>>,
            },
        }

        #[test]
        fn implements_error() {
            fn check_bounds<T: std::error::Error>() {}
            check_bounds::<Error<i32>>();
            check_bounds::<ApiError<i32>>();
        }
    }
}

mod const_generics {
    use snafu::prelude::*;

    fn check_bounds<T: std::error::Error>() {}

    #[derive(Debug, Snafu)]
    #[snafu(display("Exceeded {N}"))]
    pub struct Error<const N: i32>;

    fn make_one() -> Result<(), Error<1>> {
        Snafu.fail()
    }

    fn make_two() -> Result<(), Error<2>> {
        Snafu.fail()
    }

    #[test]
    fn implements_error() {
        check_bounds::<Error<1>>();
        check_bounds::<Error<2>>();
    }

    #[test]
    fn can_be_constructed() {
        assert!(make_one().is_err());
        assert!(make_two().is_err());
    }

    #[test]
    fn can_use_const_in_display() {
        let e: Error<42> = Snafu.build();
        assert_eq!(e.to_string(), "Exceeded 42");
    }

    #[derive(Debug, Snafu)]
    struct OpaqueError<const N: i32>(Error<N>);

    #[test]
    fn opaque_implements_error() {
        check_bounds::<OpaqueError<1>>();
        check_bounds::<OpaqueError<2>>();
    }

    #[test]
    fn opaque_construction() {
        fn wrap_one() -> Result<(), OpaqueError<1>> {
            make_one().map_err(Into::into)
        }

        fn wrap_two() -> Result<(), OpaqueError<2>> {
            make_two()?;
            Ok(())
        }

        assert!(wrap_one().is_err());
        assert!(wrap_two().is_err());
    }

    mod with_default {
        use snafu::prelude::*;

        fn check_bounds<T: std::error::Error>() {}

        #[derive(Debug, Snafu)]
        #[snafu(display("Exceeded {N}"))]
        pub struct Error<const N: i32 = 42>;

        fn make_forty_two() -> Result<(), Error> {
            Snafu.fail()
        }

        fn make_ninety_nine() -> Result<(), Error<99>> {
            Snafu.fail()
        }

        #[test]
        fn implements_error() {
            check_bounds::<Error>();
            check_bounds::<Error<99>>();
        }

        #[test]
        fn can_be_constructed() {
            assert!(make_forty_two().is_err());
            assert!(make_ninety_nine().is_err());
        }

        #[test]
        fn can_use_const_in_display() {
            let e: Error = Snafu.build();
            assert_eq!(e.to_string(), "Exceeded 42");
        }

        #[derive(Debug, Snafu)]
        struct OpaqueError<const N: i32 = 99>(Error<N>);

        #[test]
        fn opaque_implements_error() {
            check_bounds::<OpaqueError<42>>();
            check_bounds::<OpaqueError>();
        }

        #[test]
        fn opaque_construction() {
            fn wrap_forty_two() -> Result<(), OpaqueError<42>> {
                make_forty_two().map_err(Into::into)
            }

            fn wrap_ninety_nine() -> Result<(), OpaqueError> {
                make_ninety_nine()?;
                Ok(())
            }

            assert!(wrap_forty_two().is_err());
            assert!(wrap_ninety_nine().is_err());
        }
    }
}
