use crate::error::OcelotError;
use crate::shared_string::SharedString;
use std::error::Error as StdError;
use std::panic::Location;

pub type OcelotResult<T> = Result<T, OcelotError>;

pub trait ResultExt<T> {
    #[track_caller]
    fn context(self, context: impl Into<SharedString>) -> OcelotResult<T>;

    #[track_caller]
    fn with_context<C, S>(self, context: C) -> OcelotResult<T>
    where
        C: FnOnce() -> S,
        S: Into<SharedString>;
}

pub trait OptionExt<T> {
    #[track_caller]
    fn context(self, context: impl Into<SharedString>) -> OcelotResult<T>;

    #[track_caller]
    fn with_context<C, S>(self, context: C) -> OcelotResult<T>
    where
        C: FnOnce() -> S,
        S: Into<SharedString>;
}

impl<T, E> ResultExt<T> for Result<T, E>
where
    E: StdError + Send + Sync + 'static,
{
    #[track_caller]
    fn context(self, context: impl Into<SharedString>) -> OcelotResult<T> {
        let caller = Location::caller();
        self.map_err(|error| {
            OcelotError::message_at_location(context.into(), caller)
                .with_std_source_at_location(error, caller)
        })
    }

    #[track_caller]
    fn with_context<C, S>(self, context: C) -> OcelotResult<T>
    where
        C: FnOnce() -> S,
        S: Into<SharedString>,
    {
        let caller = Location::caller();
        self.map_err(|error| {
            OcelotError::message_at_location(context(), caller)
                .with_std_source_at_location(error, caller)
        })
    }
}

impl<T> ResultExt<T> for Result<T, OcelotError> {
    #[track_caller]
    fn context(self, context: impl Into<SharedString>) -> OcelotResult<T> {
        let caller = Location::caller();
        self.map_err(|error| {
            OcelotError::message_at_location(context.into(), caller).with_source(error)
        })
    }

    #[track_caller]
    fn with_context<C, S>(self, context: C) -> OcelotResult<T>
    where
        C: FnOnce() -> S,
        S: Into<SharedString>,
    {
        let caller = Location::caller();
        self.map_err(|error| OcelotError::message_at_location(context(), caller).with_source(error))
    }
}

impl<T> OptionExt<T> for Option<T> {
    #[track_caller]
    fn context(self, context: impl Into<SharedString>) -> OcelotResult<T> {
        let caller = Location::caller();
        self.ok_or_else(|| OcelotError::message_at_location(context.into(), caller))
    }

    #[track_caller]
    fn with_context<C, S>(self, context: C) -> OcelotResult<T>
    where
        C: FnOnce() -> S,
        S: Into<SharedString>,
    {
        let caller = Location::caller();
        self.ok_or_else(|| OcelotError::message_at_location(context(), caller))
    }
}

#[cfg(test)]
mod tests {
    use crate::result::{OptionExt, ResultExt};
    use std::io;

    #[test]
    fn test_with_context_is_lazy_for_ok_results() {
        use std::cell::Cell;

        let context_called = Cell::new(false);
        let result: Result<i32, io::Error> = Ok(123);
        let value = result
            .with_context(|| {
                context_called.set(true);
                "should not be used"
            })
            .unwrap();
        assert_eq!(value, 123);
        assert!(!context_called.get());
    }

    #[test]
    fn test_option_with_context_is_lazy_for_some_results() {
        use std::cell::Cell;

        let context_called = Cell::new(false);
        let value = Some(123)
            .with_context(|| {
                context_called.set(true);
                "should not be used"
            })
            .unwrap();
        assert_eq!(value, 123);
        assert!(!context_called.get());
    }

    #[test]
    fn test_with_context_wraps_existing_error_results() {
        let result: crate::result::OcelotResult<i32> = Err(crate::err!("root cause"));
        let error = result.with_context(|| "outer context").unwrap_err();
        let rendered = error.to_test_string();

        assert!(rendered.contains("outer context"));
        assert!(rendered.contains("root cause"));
    }
}
