//! Test Safety Utilities
//!
//! Provides safe testing patterns to eliminate unwrap/expect usage in tests
//! and improve debugging experience with better error messages.

use std::fmt::Debug;
use std::future::Future;
use std::time::Duration;
use tokio::time::timeout;

/// Result type for test operations with descriptive context
pub type TestResult<T = ()> = Result<T, TestError>;

/// Comprehensive test error type with context
#[derive(Debug, thiserror::Error)]
pub enum TestError {
    #[error("Test setup failed: {context}")]
    SetupFailed { context: String },

    #[error("Test assertion failed: {context}")]
    AssertionFailed { context: String },

    #[error("Test timeout after {duration:?}: {context}")]
    Timeout { duration: Duration, context: String },

    #[error("Resource cleanup failed: {context}")]
    CleanupFailed { context: String },

    #[error("External service unavailable: {service} - {reason}")]
    ServiceUnavailable { service: String, reason: String },

    #[error("Test data corruption: {context}")]
    DataCorruption { context: String },

    #[error("Network error in test: {context}")]
    NetworkError { context: String },

    #[error("Database error in test: {context}")]
    DatabaseError { context: String },

    #[error("Generic test failure: {context}")]
    Generic { context: String },
}

impl TestError {
    pub fn setup(context: impl Into<String>) -> Self {
        Self::SetupFailed {
            context: context.into(),
        }
    }

    pub fn assertion(context: impl Into<String>) -> Self {
        Self::AssertionFailed {
            context: context.into(),
        }
    }

    pub fn timeout(duration: Duration, context: impl Into<String>) -> Self {
        Self::Timeout {
            duration,
            context: context.into(),
        }
    }

    pub fn cleanup(context: impl Into<String>) -> Self {
        Self::CleanupFailed {
            context: context.into(),
        }
    }

    pub fn service_unavailable(service: impl Into<String>, reason: impl Into<String>) -> Self {
        Self::ServiceUnavailable {
            service: service.into(),
            reason: reason.into(),
        }
    }

    pub fn data_corruption(context: impl Into<String>) -> Self {
        Self::DataCorruption {
            context: context.into(),
        }
    }

    pub fn network(context: impl Into<String>) -> Self {
        Self::NetworkError {
            context: context.into(),
        }
    }

    pub fn database(context: impl Into<String>) -> Self {
        Self::DatabaseError {
            context: context.into(),
        }
    }

    pub fn generic(context: impl Into<String>) -> Self {
        Self::Generic {
            context: context.into(),
        }
    }
}

/// Safe assertion macro with descriptive error messages
#[macro_export]
macro_rules! test_assert {
    ($condition:expr, $context:expr) => {
        if !$condition {
            return Err(TestError::assertion(format!("{}: condition failed: {}", $context, stringify!($condition))));
        }
    };
    ($condition:expr, $context:expr, $($arg:tt)*) => {
        if !$condition {
            return Err(TestError::assertion(format!("{}: {}", $context, format!($($arg)*))));
        }
    };
}

/// Safe equality assertion with descriptive error messages
#[macro_export]
macro_rules! test_assert_eq {
    ($left:expr, $right:expr, $context:expr) => {
        let left_val = $left;
        let right_val = $right;
        if left_val != right_val {
            return Err(TestError::assertion(format!(
                "{}: assertion failed: {} == {}\nleft: {:?}\nright: {:?}",
                $context,
                stringify!($left),
                stringify!($right),
                left_val,
                right_val
            )));
        }
    };
}

/// Safe unwrap with descriptive error context
pub trait SafeTestUnwrap<T> {
    fn safe_unwrap(self, context: &str) -> TestResult<T>;
}

impl<T, E: Debug> SafeTestUnwrap<T> for Result<T, E> {
    fn safe_unwrap(self, context: &str) -> TestResult<T> {
        self.map_err(|e| TestError::generic(format!("{}: {:?}", context, e)))
    }
}

impl<T> SafeTestUnwrap<T> for Option<T> {
    fn safe_unwrap(self, context: &str) -> TestResult<T> {
        self.ok_or_else(|| TestError::generic(format!("{}: Option was None", context)))
    }
}

/// Safe timeout wrapper for async operations
pub async fn with_test_timeout<F, T>(future: F, duration: Duration, context: &str) -> TestResult<T>
where
    F: Future<Output = T>,
{
    timeout(duration, future)
        .await
        .map_err(|_| TestError::timeout(duration, context))
}

/// Safe timeout wrapper for Result returning async operations
pub async fn with_test_timeout_result<F, T, E>(
    future: F,
    duration: Duration,
    context: &str,
) -> TestResult<T>
where
    F: Future<Output = Result<T, E>>,
    E: Debug,
{
    let result = timeout(duration, future)
        .await
        .map_err(|_| TestError::timeout(duration, context))?;

    result.map_err(|e| TestError::generic(format!("{}: {:?}", context, e)))
}

/// Test fixture with automatic cleanup
pub struct TestFixture<T> {
    resource: Option<T>,
    cleanup_fn: Option<Box<dyn FnOnce(T) -> TestResult<()> + Send>>,
}

impl<T: std::fmt::Debug> std::fmt::Debug for TestFixture<T> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("TestFixture")
            .field("resource", &self.resource)
            .field(
                "cleanup_fn",
                &if self.cleanup_fn.is_some() {
                    "<cleanup fn>"
                } else {
                    "<none>"
                },
            )
            .finish()
    }
}

impl<T> TestFixture<T> {
    pub fn new(resource: T) -> Self {
        Self {
            resource: Some(resource),
            cleanup_fn: None,
        }
    }

    pub fn with_cleanup<F>(mut self, cleanup_fn: F) -> Self
    where
        F: FnOnce(T) -> TestResult<()> + Send + 'static,
    {
        self.cleanup_fn = Some(Box::new(cleanup_fn));
        self
    }

    pub fn get(&self) -> &T {
        self.resource.as_ref().expect("Resource already taken")
    }

    pub fn get_mut(&mut self) -> &mut T {
        self.resource.as_mut().expect("Resource already taken")
    }
}

impl<T> Drop for TestFixture<T> {
    fn drop(&mut self) {
        if let (Some(resource), Some(cleanup_fn)) = (self.resource.take(), self.cleanup_fn.take()) {
            if let Err(e) = cleanup_fn(resource) {
                eprintln!("Test cleanup failed: {}", e);
            }
        }
    }
}

/// Property based testing utilities
pub mod property {
    use super::*;

    /// Run property test with proper error handling
    pub fn run_property_test<F>(test_name: &str, iterations: usize, test_fn: F) -> TestResult<()>
    where
        F: Fn(usize) -> TestResult<()>,
    {
        for i in 0..iterations {
            test_fn(i).map_err(|e| {
                TestError::assertion(format!("{} failed at iteration {}: {}", test_name, i, e))
            })?;
        }
        Ok(())
    }
}

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

    #[tokio::test]
    async fn test_safe_unwrap_success() -> TestResult<()> {
        let result: Result<i32, &str> = Ok(42);
        let value = result.safe_unwrap("test operation")?;
        test_assert_eq!(value, 42, "safe_unwrap should return correct value");
        Ok(())
    }

    #[tokio::test]
    async fn test_safe_unwrap_failure() {
        let result: Result<i32, &str> = Err("test error");
        match result.safe_unwrap("test operation") {
            Err(TestError::Generic { context }) => {
                assert!(context.contains("test operation"));
                assert!(context.contains("test error"));
            },
            _ => panic!("Expected TestError::Generic"),
        }
    }

    #[tokio::test]
    async fn test_timeout_wrapper() {
        let result = with_test_timeout(
            async { tokio::time::sleep(Duration::from_millis(100)).await },
            Duration::from_millis(50),
            "timeout test",
        )
        .await;

        match result {
            Err(TestError::Timeout { duration, context }) => {
                assert_eq!(duration, Duration::from_millis(50));
                assert_eq!(context, "timeout test");
            },
            _ => panic!("Expected timeout error"),
        }
    }

    #[tokio::test]
    async fn test_property_testing() -> TestResult<()> {
        property::run_property_test("simple addition", 10, |i| {
            let x = i as i32;
            let y = x + 1;
            test_assert!(y > x, "addition should increase value");
            Ok(())
        })
    }
}
