//! Exception and warning types defined by Python.
//!
//! The structs in this module represent Python's built-in exceptions and
//! warnings, while the modules comprise structs representing errors defined in
//! Python code.
//!
//! The latter are created with the
//! [`import_exception`](crate::import_exception) macro, which you can use
//! yourself to import Python classes that are ultimately derived from
//! `BaseException`.

use crate::{ffi, Bound, PyResult, Python};
use std::ffi::CStr;
use std::ops;

/// The boilerplate to convert between a Rust type and a Python exception.
#[doc(hidden)]
#[macro_export]
macro_rules! impl_exception_boilerplate {
    ($name: ident) => {
        impl $name {
            /// Creates a new [`PyErr`] of this type.
            ///
            /// [`PyErr`]: https://docs.rs/pyo3/latest/pyo3/struct.PyErr.html "PyErr in pyo3"
            #[inline]
            #[allow(dead_code, reason = "user may not call this function")]
            pub fn new_err<A>(args: A) -> $crate::PyErr
            where
                A: $crate::PyErrArguments + ::std::marker::Send + ::std::marker::Sync + 'static,
            {
                $crate::PyErr::new::<$name, A>(args)
            }
        }

        impl $crate::ToPyErr for $name {}
    };
}

/// Defines a Rust type for an exception defined in Python code.
///
/// # Syntax
///
/// ```import_exception!(module, MyError)```
///
/// * `module` is the name of the containing module.
/// * `MyError` is the name of the new exception type.
///
/// # Examples
/// ```
/// use pyforge::import_exception;
/// use pyforge::types::IntoPyDict;
/// use pyforge::Python;
///
/// import_exception!(socket, gaierror);
///
/// # fn main() -> pyforge::PyResult<()> {
/// Python::attach(|py| {
///     let ctx = [("gaierror", py.get_type::<gaierror>())].into_py_dict(py)?;
///     pyforge::py_run!(py, *ctx, "import socket; assert gaierror is socket.gaierror");
/// #   Ok(())
/// })
/// # }
///
/// ```
#[macro_export]
macro_rules! import_exception {
    ($module: expr, $name: ident) => {
        /// A Rust type representing an exception defined in Python code.
        ///
        /// This type was created by the [`pyforge::import_exception!`] macro - see its documentation
        /// for more information.
        ///
        /// [`pyforge::import_exception!`]: https://docs.rs/pyo3/latest/pyo3/macro.import_exception.html "import_exception in pyo3"
        #[repr(transparent)]
        #[allow(non_camel_case_types, reason = "matches imported exception name, e.g. `socket.herror`")]
        pub struct $name($crate::PyAny);

        $crate::impl_exception_boilerplate!($name);

        $crate::pyobject_native_type_core!(
            $name,
            $name::type_object_raw,
            stringify!($name),
            stringify!($module),
            #module=::std::option::Option::Some(stringify!($module))
        );

        impl $name {
            fn type_object_raw(py: $crate::Python<'_>) -> *mut $crate::ffi::PyTypeObject {
                use $crate::types::PyTypeMethods;
                static TYPE_OBJECT: $crate::impl_::exceptions::ImportedExceptionTypeObject =
                    $crate::impl_::exceptions::ImportedExceptionTypeObject::new(stringify!($module), stringify!($name));
                TYPE_OBJECT.get(py).as_type_ptr()
            }
        }
    };
}

/// Deprecated name for `import_exception!`.
#[macro_export]
#[deprecated(since = "0.27.0", note = "renamed to `import_exception!` instead")]
macro_rules! import_exception_bound {
    ($module: expr, $name: ident) => {
        $crate::import_exception!($module, $name);
    };
}

/// Defines a new exception type.
///
/// # Syntax
///
/// * `module` is the name of the containing module.
/// * `name` is the name of the new exception type.
/// * `base` is the base class of `MyError`, usually [`PyException`].
/// * `doc` (optional) is the docstring visible to users (with `.__doc__` and `help()`) and
///
/// accompanies your error type in your crate's documentation.
///
/// # Examples
///
/// ```
/// use pyforge::prelude::*;
/// use pyforge::create_exception;
/// use pyforge::exceptions::PyException;
///
/// create_exception!(my_module, MyError, PyException, "Some description.");
///
/// #[pyfunction]
/// fn raise_myerror() -> PyResult<()> {
///     let err = MyError::new_err("Some error happened.");
///     Err(err)
/// }
///
/// #[pymodule]
/// fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
///     m.add("MyError", m.py().get_type::<MyError>())?;
///     m.add_function(wrap_pyfunction!(raise_myerror, m)?)?;
///     Ok(())
/// }
/// # fn main() -> PyResult<()> {
/// #     Python::attach(|py| -> PyResult<()> {
/// #         let fun = wrap_pyfunction!(raise_myerror, py)?;
/// #         let locals = pyforge::types::PyDict::new(py);
/// #         locals.set_item("MyError", py.get_type::<MyError>())?;
/// #         locals.set_item("raise_myerror", fun)?;
/// #
/// #         py.run(
/// # c"try:
/// #     raise_myerror()
/// # except MyError as e:
/// #     assert e.__doc__ == 'Some description.'
/// #     assert str(e) == 'Some error happened.'",
/// #             None,
/// #             Some(&locals),
/// #         )?;
/// #
/// #         Ok(())
/// #     })
/// # }
/// ```
///
/// Python code can handle this exception like any other exception:
///
/// ```python
/// from my_module import MyError, raise_myerror
///
/// try:
///     raise_myerror()
/// except MyError as e:
///     assert e.__doc__ == 'Some description.'
///     assert str(e) == 'Some error happened.'
/// ```
///
#[macro_export]
macro_rules! create_exception {
    ($module: expr, $name: ident, $base: ty) => {
        #[repr(transparent)]
        pub struct $name($crate::PyAny);

        $crate::impl_exception_boilerplate!($name);

        $crate::create_exception_type_object!($module, $name, $base, None);
    };
    ($module: expr, $name: ident, $base: ty, $doc: expr) => {
        #[repr(transparent)]
        #[doc = $doc]
        pub struct $name($crate::PyAny);

        $crate::impl_exception_boilerplate!($name);

        $crate::create_exception_type_object!($module, $name, $base, Some($doc));
    };
}

/// `impl PyTypeInfo for $name` where `$name` is an
/// exception newly defined in Rust code.
#[doc(hidden)]
#[macro_export]
macro_rules! create_exception_type_object {
    ($module: expr, $name: ident, $base: ty, None) => {
        $crate::create_exception_type_object!($module, $name, $base, ::std::option::Option::None);
    };
    ($module: expr, $name: ident, $base: ty, Some($doc: expr)) => {
        $crate::create_exception_type_object!(
            $module,
            $name,
            $base,
            ::std::option::Option::Some($crate::ffi::c_str!($doc))
        );
    };
    ($module: expr, $name: ident, $base: ty, $doc: expr) => {
        $crate::pyobject_native_type_named!($name);

        // SAFETY: macro caller has upheld the safety contracts
        unsafe impl $crate::type_object::PyTypeInfo for $name {
            const NAME: &'static str = stringify!($name);
            const MODULE: ::std::option::Option<&'static str> =
                ::std::option::Option::Some(stringify!($module));
            $crate::create_exception_type_hint!($module, $name);

            #[inline]
            #[allow(clippy::redundant_closure_call)]
            fn type_object_raw(py: $crate::Python<'_>) -> *mut $crate::ffi::PyTypeObject {
                use $crate::sync::PyOnceLock;
                static TYPE_OBJECT: PyOnceLock<$crate::Py<$crate::types::PyType>> =
                    PyOnceLock::new();

                TYPE_OBJECT
                    .get_or_init(py, || {
                        $crate::PyErr::new_type(
                            py,
                            $crate::ffi::c_str!(concat!(
                                stringify!($module),
                                ".",
                                stringify!($name)
                            )),
                            $doc,
                            ::std::option::Option::Some(&py.get_type::<$base>()),
                            ::std::option::Option::None,
                        )
                        .expect("Failed to initialize new exception type.")
                    })
                    .as_ptr()
                    .cast()
            }
        }

        impl $name {
            #[doc(hidden)]
            pub const _PYO3_DEF: $crate::impl_::pymodule::AddTypeToModule<Self> =
                $crate::impl_::pymodule::AddTypeToModule::new();

            #[allow(dead_code)]
            #[doc(hidden)]
            pub const _PYO3_INTROSPECTION_ID: &'static str =
                concat!(stringify!($module), stringify!($name));
        }
    };
}

/// Adds a TYPE_HINT constant if the `experimental-inspect`  feature is enabled.
#[cfg(not(feature = "experimental-inspect"))]
#[doc(hidden)]
#[macro_export]
macro_rules! create_exception_type_hint(
    ($module: expr, $name: ident) => {};
);

#[cfg(feature = "experimental-inspect")]
#[doc(hidden)]
#[macro_export]
macro_rules! create_exception_type_hint(
    ($module: expr, $name: ident) => {
        const TYPE_HINT: $crate::inspect::PyStaticExpr = $crate::inspect::PyStaticExpr::PyClass($crate::inspect::PyClassNameStaticExpr::new(
            &$crate::type_hint_identifier!(stringify!($module), stringify!($name)),
            Self::_PYO3_INTROSPECTION_ID
        ));
    };
);

macro_rules! impl_native_exception (
    ($name:ident, $exc_name:ident, $python_name:expr, $doc:expr, $layout:path $(, #checkfunction=$checkfunction:path)?) => (
        #[doc = $doc]
        #[repr(transparent)]
        #[allow(clippy::upper_case_acronyms, reason = "Python exception names")]
        pub struct $name($crate::PyAny);

        $crate::impl_exception_boilerplate!($name);
        $crate::pyobject_native_type!($name, $layout, |_py| unsafe { $crate::ffi::$exc_name as *mut $crate::ffi::PyTypeObject }, "builtins", $python_name $(, #checkfunction=$checkfunction)?);
        $crate::pyobject_subclassable_native_type!($name, $layout);
    );
    ($name:ident, $exc_name:ident, $python_name:expr, $doc:expr) => (
        impl_native_exception!($name, $exc_name, $python_name, $doc, $crate::ffi::PyBaseExceptionObject);
    )
);

macro_rules! native_doc(
    ($name: literal, $alt: literal) => (
        concat!(
"Represents Python's [`", $name, "`](https://docs.python.org/3/library/exceptions.html#", $name, ") exception.

", $alt
        )
    );
    ($name: literal) => (
        concat!(
"
Represents Python's [`", $name, "`](https://docs.python.org/3/library/exceptions.html#", $name, ") exception.

# Example: Raising ", $name, " from Rust

This exception can be sent to Python code by converting it into a
[`PyErr`](crate::PyErr), where Python code can then catch it.
```
use pyforge::prelude::*;
use pyforge::exceptions::Py", $name, ";

#[pyfunction]
fn always_throws() -> PyResult<()> {
    let message = \"I'm ", $name ,", and I was raised from Rust.\";
    Err(Py", $name, "::new_err(message))
}
#
# Python::attach(|py| {
#     let fun = pyforge::wrap_pyfunction!(always_throws, py).unwrap();
#     let err = fun.call0().expect_err(\"called a function that should always return an error but the return value was Ok\");
#     assert!(err.is_instance_of::<Py", $name, ">(py))
# });
```

Python code:
 ```python
 from my_module import always_throws

try:
    always_throws()
except ", $name, " as e:
    print(f\"Caught an exception: {e}\")
```

# Example: Catching ", $name, " in Rust

```
use pyforge::prelude::*;
use pyforge::exceptions::Py", $name, ";
use pyforge::ffi::c_str;

Python::attach(|py| {
    let result: PyResult<()> = py.run(c_str!(\"raise ", $name, "\"), None, None);

    let error_type = match result {
        Ok(_) => \"Not an error\",
        Err(error) if error.is_instance_of::<Py", $name, ">(py) => \"" , $name, "\",
        Err(_) => \"Some other error\",
    };

    assert_eq!(error_type, \"", $name, "\");
});
```
"
        )
    );
);

impl_native_exception!(
    PyBaseException,
    PyExc_BaseException,
    "BaseException",
    native_doc!("BaseException"),
    ffi::PyBaseExceptionObject,
    #checkfunction=ffi::PyExceptionInstance_Check
);
impl_native_exception!(
    PyException,
    PyExc_Exception,
    "Exception",
    native_doc!("Exception")
);
impl_native_exception!(
    PyStopAsyncIteration,
    PyExc_StopAsyncIteration,
    "StopAsyncIteration",
    native_doc!("StopAsyncIteration")
);
impl_native_exception!(
    PyStopIteration,
    PyExc_StopIteration,
    "StopIteration",
    native_doc!("StopIteration"),
    ffi::PyStopIterationObject
);
impl_native_exception!(
    PyGeneratorExit,
    PyExc_GeneratorExit,
    "GeneratorExit",
    native_doc!("GeneratorExit")
);
impl_native_exception!(
    PyArithmeticError,
    PyExc_ArithmeticError,
    "ArithmeticError",
    native_doc!("ArithmeticError")
);
impl_native_exception!(
    PyLookupError,
    PyExc_LookupError,
    "LookupError",
    native_doc!("LookupError")
);

impl_native_exception!(
    PyAssertionError,
    PyExc_AssertionError,
    "AssertionError",
    native_doc!("AssertionError")
);
impl_native_exception!(
    PyAttributeError,
    PyExc_AttributeError,
    "AttributeError",
    native_doc!("AttributeError")
);
impl_native_exception!(
    PyBufferError,
    PyExc_BufferError,
    "BufferError",
    native_doc!("BufferError")
);
impl_native_exception!(
    PyEOFError,
    PyExc_EOFError,
    "EOFError",
    native_doc!("EOFError")
);
impl_native_exception!(
    PyFloatingPointError,
    PyExc_FloatingPointError,
    "FloatingPointError",
    native_doc!("FloatingPointError")
);
impl_native_exception!(
    PyOSError,
    PyExc_OSError,
    "OSError",
    native_doc!("OSError"),
    ffi::PyOSErrorObject
);
impl_native_exception!(
    PyImportError,
    PyExc_ImportError,
    "ImportError",
    native_doc!("ImportError")
);

impl_native_exception!(
    PyModuleNotFoundError,
    PyExc_ModuleNotFoundError,
    "ModuleNotFoundError",
    native_doc!("ModuleNotFoundError")
);

impl_native_exception!(
    PyIndexError,
    PyExc_IndexError,
    "IndexError",
    native_doc!("IndexError")
);
impl_native_exception!(
    PyKeyError,
    PyExc_KeyError,
    "KeyError",
    native_doc!("KeyError")
);
impl_native_exception!(
    PyKeyboardInterrupt,
    PyExc_KeyboardInterrupt,
    "KeyboardInterrupt",
    native_doc!("KeyboardInterrupt")
);
impl_native_exception!(
    PyMemoryError,
    PyExc_MemoryError,
    "MemoryError",
    native_doc!("MemoryError")
);
impl_native_exception!(
    PyNameError,
    PyExc_NameError,
    "NameError",
    native_doc!("NameError")
);
impl_native_exception!(
    PyOverflowError,
    PyExc_OverflowError,
    "OverflowError",
    native_doc!("OverflowError")
);
impl_native_exception!(
    PyRuntimeError,
    PyExc_RuntimeError,
    "RuntimeError",
    native_doc!("RuntimeError")
);
impl_native_exception!(
    PyRecursionError,
    PyExc_RecursionError,
    "RecursionError",
    native_doc!("RecursionError")
);
impl_native_exception!(
    PyNotImplementedError,
    PyExc_NotImplementedError,
    "NotImplementedError",
    native_doc!("NotImplementedError")
);
impl_native_exception!(
    PySyntaxError,
    PyExc_SyntaxError,
    "SyntaxError",
    native_doc!("SyntaxError"),
    ffi::PySyntaxErrorObject
);
impl_native_exception!(
    PyReferenceError,
    PyExc_ReferenceError,
    "ReferenceError",
    native_doc!("ReferenceError")
);
impl_native_exception!(
    PySystemError,
    PyExc_SystemError,
    "SystemError",
    native_doc!("SystemError")
);
impl_native_exception!(
    PySystemExit,
    PyExc_SystemExit,
    "SystemExit",
    native_doc!("SystemExit"),
    ffi::PySystemExitObject
);
impl_native_exception!(
    PyTypeError,
    PyExc_TypeError,
    "TypeError",
    native_doc!("TypeError")
);
impl_native_exception!(
    PyUnboundLocalError,
    PyExc_UnboundLocalError,
    "UnboundLocalError",
    native_doc!("UnboundLocalError")
);
impl_native_exception!(
    PyUnicodeError,
    PyExc_UnicodeError,
    "UnicodeError",
    native_doc!("UnicodeError"),
    ffi::PyUnicodeErrorObject
);
// these four errors need arguments, so they're too annoying to write tests for using macros...
impl_native_exception!(
    PyUnicodeDecodeError,
    PyExc_UnicodeDecodeError,
    "UnicodeDecodeError",
    native_doc!("UnicodeDecodeError", "")
);
impl_native_exception!(
    PyUnicodeEncodeError,
    PyExc_UnicodeEncodeError,
    "UnicodeEncodeError",
    native_doc!("UnicodeEncodeError", "")
);
impl_native_exception!(
    PyUnicodeTranslateError,
    PyExc_UnicodeTranslateError,
    "UnicodeTranslateError",
    native_doc!("UnicodeTranslateError", "")
);
#[cfg(Py_3_11)]
impl_native_exception!(
    PyBaseExceptionGroup,
    PyExc_BaseExceptionGroup,
    "BaseExceptionGroup",
    native_doc!("BaseExceptionGroup", "")
);
impl_native_exception!(
    PyValueError,
    PyExc_ValueError,
    "ValueError",
    native_doc!("ValueError")
);
impl_native_exception!(
    PyZeroDivisionError,
    PyExc_ZeroDivisionError,
    "ZeroDivisionError",
    native_doc!("ZeroDivisionError")
);

impl_native_exception!(
    PyBlockingIOError,
    PyExc_BlockingIOError,
    "BlockingIOError",
    native_doc!("BlockingIOError")
);
impl_native_exception!(
    PyBrokenPipeError,
    PyExc_BrokenPipeError,
    "BrokenPipeError",
    native_doc!("BrokenPipeError")
);
impl_native_exception!(
    PyChildProcessError,
    PyExc_ChildProcessError,
    "ChildProcessError",
    native_doc!("ChildProcessError")
);
impl_native_exception!(
    PyConnectionError,
    PyExc_ConnectionError,
    "ConnectionError",
    native_doc!("ConnectionError")
);
impl_native_exception!(
    PyConnectionAbortedError,
    PyExc_ConnectionAbortedError,
    "ConnectionAbortedError",
    native_doc!("ConnectionAbortedError")
);
impl_native_exception!(
    PyConnectionRefusedError,
    PyExc_ConnectionRefusedError,
    "ConnectionRefusedError",
    native_doc!("ConnectionRefusedError")
);
impl_native_exception!(
    PyConnectionResetError,
    PyExc_ConnectionResetError,
    "ConnectionResetError",
    native_doc!("ConnectionResetError")
);
impl_native_exception!(
    PyFileExistsError,
    PyExc_FileExistsError,
    "FileExistsError",
    native_doc!("FileExistsError")
);
impl_native_exception!(
    PyFileNotFoundError,
    PyExc_FileNotFoundError,
    "FileNotFoundError",
    native_doc!("FileNotFoundError")
);
impl_native_exception!(
    PyInterruptedError,
    PyExc_InterruptedError,
    "InterruptedError",
    native_doc!("InterruptedError")
);
impl_native_exception!(
    PyIsADirectoryError,
    PyExc_IsADirectoryError,
    "IsADirectoryError",
    native_doc!("IsADirectoryError")
);
impl_native_exception!(
    PyNotADirectoryError,
    PyExc_NotADirectoryError,
    "NotADirectoryError",
    native_doc!("NotADirectoryError")
);
impl_native_exception!(
    PyPermissionError,
    PyExc_PermissionError,
    "PermissionError",
    native_doc!("PermissionError")
);
impl_native_exception!(
    PyProcessLookupError,
    PyExc_ProcessLookupError,
    "ProcessLookupError",
    native_doc!("ProcessLookupError")
);
impl_native_exception!(
    PyTimeoutError,
    PyExc_TimeoutError,
    "TimeoutError",
    native_doc!("TimeoutError")
);

/// Alias of `PyOSError`, corresponding to `EnvironmentError` alias in Python.
pub type PyEnvironmentError = PyOSError;

/// Alias of `PyOSError`, corresponding to `IOError` alias in Python.
pub type PyIOError = PyOSError;

#[cfg(windows)]
/// Alias of `PyOSError`, corresponding to `WindowsError` alias in Python.
pub type PyWindowsError = PyOSError;

impl PyUnicodeDecodeError {
    /// Creates a Python `UnicodeDecodeError`.
    pub fn new<'py>(
        py: Python<'py>,
        encoding: &CStr,
        input: &[u8],
        range: ops::Range<usize>,
        reason: &CStr,
    ) -> PyResult<Bound<'py, PyUnicodeDecodeError>> {
        use crate::ffi_ptr_ext::FfiPtrExt;
        use crate::py_result_ext::PyResultExt;
        unsafe {
            ffi::PyUnicodeDecodeError_Create(
                encoding.as_ptr(),
                input.as_ptr().cast(),
                input.len() as ffi::Py_ssize_t,
                range.start as ffi::Py_ssize_t,
                range.end as ffi::Py_ssize_t,
                reason.as_ptr(),
            )
            .assume_owned_or_err(py)
        }
        .cast_into()
    }

    /// Creates a Python `UnicodeDecodeError` from a Rust UTF-8 decoding error.
    ///
    /// # Examples
    ///
    /// ```
    /// use pyforge::prelude::*;
    /// use pyforge::exceptions::PyUnicodeDecodeError;
    ///
    /// # fn main() -> PyResult<()> {
    /// Python::attach(|py| {
    ///     let invalid_utf8 = b"fo\xd8o";
    /// #   #[expect(invalid_from_utf8)]
    ///     let err = std::str::from_utf8(invalid_utf8).expect_err("should be invalid utf8");
    ///     let decode_err = PyUnicodeDecodeError::new_utf8(py, invalid_utf8, err)?;
    ///     assert_eq!(
    ///         decode_err.to_string(),
    ///         "'utf-8' codec can't decode byte 0xd8 in position 2: invalid utf-8"
    ///     );
    ///     Ok(())
    /// })
    /// # }
    pub fn new_utf8<'py>(
        py: Python<'py>,
        input: &[u8],
        err: std::str::Utf8Error,
    ) -> PyResult<Bound<'py, PyUnicodeDecodeError>> {
        let start = err.valid_up_to();
        let end = err.error_len().map_or(input.len(), |l| start + l);
        PyUnicodeDecodeError::new(py, c"utf-8", input, start..end, c"invalid utf-8")
    }

    /// Create a new [`PyErr`](crate::PyErr) of this type from a Rust UTF-8 decoding error.
    ///
    /// This is equivalent to [`PyUnicodeDecodeError::new_utf8`], but returning a
    /// [`PyErr`](crate::PyErr) instead of an exception object.
    ///
    /// # Example
    ///
    /// ```
    /// use pyforge::prelude::*;
    /// use pyforge::exceptions::PyUnicodeDecodeError;
    ///
    /// Python::attach(|py| {
    ///     let invalid_utf8 = b"fo\xd8o";
    ///     # #[expect(invalid_from_utf8)]
    ///     let err = std::str::from_utf8(invalid_utf8).expect_err("should be invalid utf8");
    ///     let py_err = PyUnicodeDecodeError::new_err_from_utf8(py, invalid_utf8, err);
    /// })
    /// ```
    pub fn new_err_from_utf8(
        py: Python<'_>,
        bytes: &[u8],
        err: std::str::Utf8Error,
    ) -> crate::PyErr {
        match Self::new_utf8(py, bytes, err) {
            Ok(e) => crate::PyErr::from_value(e.into_any()),
            Err(e) => e,
        }
    }
}

impl_native_exception!(PyWarning, PyExc_Warning, "Warning", native_doc!("Warning"));
impl_native_exception!(
    PyUserWarning,
    PyExc_UserWarning,
    "UserWarning",
    native_doc!("UserWarning")
);
impl_native_exception!(
    PyDeprecationWarning,
    PyExc_DeprecationWarning,
    "DeprecationWarning",
    native_doc!("DeprecationWarning")
);
impl_native_exception!(
    PyPendingDeprecationWarning,
    PyExc_PendingDeprecationWarning,
    "PendingDeprecationWarning",
    native_doc!("PendingDeprecationWarning")
);
impl_native_exception!(
    PySyntaxWarning,
    PyExc_SyntaxWarning,
    "SyntaxWarning",
    native_doc!("SyntaxWarning")
);
impl_native_exception!(
    PyRuntimeWarning,
    PyExc_RuntimeWarning,
    "RuntimeWarning",
    native_doc!("RuntimeWarning")
);
impl_native_exception!(
    PyFutureWarning,
    PyExc_FutureWarning,
    "FutureWarning",
    native_doc!("FutureWarning")
);
impl_native_exception!(
    PyImportWarning,
    PyExc_ImportWarning,
    "ImportWarning",
    native_doc!("ImportWarning")
);
impl_native_exception!(
    PyUnicodeWarning,
    PyExc_UnicodeWarning,
    "UnicodeWarning",
    native_doc!("UnicodeWarning")
);
impl_native_exception!(
    PyBytesWarning,
    PyExc_BytesWarning,
    "BytesWarning",
    native_doc!("BytesWarning")
);
impl_native_exception!(
    PyResourceWarning,
    PyExc_ResourceWarning,
    "ResourceWarning",
    native_doc!("ResourceWarning")
);

#[cfg(Py_3_10)]
impl_native_exception!(
    PyEncodingWarning,
    PyExc_EncodingWarning,
    "EncodingWarning",
    native_doc!("EncodingWarning")
);

#[cfg(test)]
macro_rules! test_exception {
    ($exc_ty:ident $(, |$py:tt| $constructor:expr )?) => {
        #[allow(non_snake_case, reason = "test matches exception name")]
        #[test]
        fn $exc_ty () {
            use super::$exc_ty;

            $crate::Python::attach(|py| {
                let err: $crate::PyErr = {
                    None
                    $(
                        .or(Some({ let $py = py; $constructor }))
                    )?
                        .unwrap_or($exc_ty::new_err("a test exception"))
                };

                assert!(err.is_instance_of::<$exc_ty>(py));

                let value = err.value(py).as_any().cast::<$exc_ty>().unwrap();

                assert!($crate::PyErr::from(value.clone()).is_instance_of::<$exc_ty>(py));
            })
        }
    };
}

/// Exceptions defined in Python's [`asyncio`](https://docs.python.org/3/library/asyncio.html)
/// module.
pub mod asyncio {
    import_exception!(asyncio, CancelledError);
    import_exception!(asyncio, InvalidStateError);
    import_exception!(asyncio, TimeoutError);
    import_exception!(asyncio, IncompleteReadError);
    import_exception!(asyncio, LimitOverrunError);
    import_exception!(asyncio, QueueEmpty);
    import_exception!(asyncio, QueueFull);

    #[cfg(test)]
    mod tests {
        test_exception!(CancelledError);
        test_exception!(InvalidStateError);
        test_exception!(TimeoutError);
        test_exception!(IncompleteReadError, |_| IncompleteReadError::new_err((
            "partial", "expected"
        )));
        test_exception!(LimitOverrunError, |_| LimitOverrunError::new_err((
            "message", "consumed"
        )));
        test_exception!(QueueEmpty);
        test_exception!(QueueFull);
    }
}

/// Exceptions defined in Python's [`socket`](https://docs.python.org/3/library/socket.html)
/// module.
pub mod socket {
    import_exception!(socket, herror);
    import_exception!(socket, gaierror);
    import_exception!(socket, timeout);

    #[cfg(test)]
    mod tests {
        test_exception!(herror);
        test_exception!(gaierror);
        test_exception!(timeout);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::any::PyAnyMethods;
    use crate::types::{IntoPyDict, PyDict};
    use crate::{IntoPyObjectExt as _, PyErr};

    import_exception!(socket, gaierror);
    import_exception!(email.errors, MessageError);

    #[test]
    fn test_check_exception() {
        Python::attach(|py| {
            let err: PyErr = gaierror::new_err(());
            let socket = py
                .import("socket")
                .map_err(|e| e.display(py))
                .expect("could not import socket");

            let d = PyDict::new(py);
            d.set_item("socket", socket)
                .map_err(|e| e.display(py))
                .expect("could not setitem");

            d.set_item("exc", err)
                .map_err(|e| e.display(py))
                .expect("could not setitem");

            py.run(c"assert isinstance(exc, socket.gaierror)", None, Some(&d))
                .map_err(|e| e.display(py))
                .expect("assertion failed");
        });
    }

    #[test]
    fn test_check_exception_nested() {
        Python::attach(|py| {
            let err: PyErr = MessageError::new_err(());
            let email = py
                .import("email")
                .map_err(|e| e.display(py))
                .expect("could not import email");

            let d = PyDict::new(py);
            d.set_item("email", email)
                .map_err(|e| e.display(py))
                .expect("could not setitem");
            d.set_item("exc", err)
                .map_err(|e| e.display(py))
                .expect("could not setitem");

            py.run(
                c"assert isinstance(exc, email.errors.MessageError)",
                None,
                Some(&d),
            )
            .map_err(|e| e.display(py))
            .expect("assertion failed");
        });
    }

    #[test]
    fn custom_exception() {
        create_exception!(mymodule, CustomError, PyException);

        Python::attach(|py| {
            let error_type = py.get_type::<CustomError>();
            let ctx = [("CustomError", error_type)].into_py_dict(py).unwrap();
            let type_description: String = py
                .eval(c"str(CustomError)", None, Some(&ctx))
                .unwrap()
                .extract()
                .unwrap();
            assert_eq!(type_description, "<class 'mymodule.CustomError'>");
            py.run(
                c"assert CustomError('oops').args == ('oops',)",
                None,
                Some(&ctx),
            )
            .unwrap();
            py.run(c"assert CustomError.__doc__ is None", None, Some(&ctx))
                .unwrap();
        });
    }

    #[test]
    fn custom_exception_dotted_module() {
        create_exception!(mymodule.exceptions, CustomError, PyException);
        Python::attach(|py| {
            let error_type = py.get_type::<CustomError>();
            let ctx = [("CustomError", error_type)].into_py_dict(py).unwrap();
            let type_description: String = py
                .eval(c"str(CustomError)", None, Some(&ctx))
                .unwrap()
                .extract()
                .unwrap();
            assert_eq!(
                type_description,
                "<class 'mymodule.exceptions.CustomError'>"
            );
        });
    }

    #[test]
    fn custom_exception_doc() {
        create_exception!(mymodule, CustomError, PyException, "Some docs");

        Python::attach(|py| {
            let error_type = py.get_type::<CustomError>();
            let ctx = [("CustomError", error_type)].into_py_dict(py).unwrap();
            let type_description: String = py
                .eval(c"str(CustomError)", None, Some(&ctx))
                .unwrap()
                .extract()
                .unwrap();
            assert_eq!(type_description, "<class 'mymodule.CustomError'>");
            py.run(
                c"assert CustomError('oops').args == ('oops',)",
                None,
                Some(&ctx),
            )
            .unwrap();
            py.run(
                c"assert CustomError.__doc__ == 'Some docs'",
                None,
                Some(&ctx),
            )
            .unwrap();
        });
    }

    #[test]
    fn custom_exception_doc_expr() {
        create_exception!(
            mymodule,
            CustomError,
            PyException,
            concat!("Some", " more ", stringify!(docs))
        );

        Python::attach(|py| {
            let error_type = py.get_type::<CustomError>();
            let ctx = [("CustomError", error_type)].into_py_dict(py).unwrap();
            let type_description: String = py
                .eval(c"str(CustomError)", None, Some(&ctx))
                .unwrap()
                .extract()
                .unwrap();
            assert_eq!(type_description, "<class 'mymodule.CustomError'>");
            py.run(
                c"assert CustomError('oops').args == ('oops',)",
                None,
                Some(&ctx),
            )
            .unwrap();
            py.run(
                c"assert CustomError.__doc__ == 'Some more docs'",
                None,
                Some(&ctx),
            )
            .unwrap();
        });
    }

    #[test]
    fn native_exception_debug() {
        Python::attach(|py| {
            let exc = py
                .run(c"raise Exception('banana')", None, None)
                .expect_err("raising should have given us an error")
                .into_value(py)
                .into_bound(py);
            assert_eq!(
                format!("{exc:?}"),
                exc.repr().unwrap().extract::<String>().unwrap()
            );
        });
    }

    #[test]
    fn native_exception_display() {
        Python::attach(|py| {
            let exc = py
                .run(c"raise Exception('banana')", None, None)
                .expect_err("raising should have given us an error")
                .into_value(py)
                .into_bound(py);
            assert_eq!(
                exc.to_string(),
                exc.str().unwrap().extract::<String>().unwrap()
            );
        });
    }

    #[test]
    fn unicode_decode_error() {
        let invalid_utf8 = b"fo\xd8o";
        #[expect(invalid_from_utf8)]
        let err = std::str::from_utf8(invalid_utf8).expect_err("should be invalid utf8");
        Python::attach(|py| {
            let decode_err = PyUnicodeDecodeError::new_utf8(py, invalid_utf8, err).unwrap();
            assert_eq!(
                format!("{decode_err:?}"),
                "UnicodeDecodeError('utf-8', b'fo\\xd8o', 2, 3, 'invalid utf-8')"
            );

            // Restoring should preserve the same error
            let e: PyErr = decode_err.into();
            e.restore(py);

            assert_eq!(
                PyErr::fetch(py).to_string(),
                "UnicodeDecodeError: \'utf-8\' codec can\'t decode byte 0xd8 in position 2: invalid utf-8"
            );
        });
    }
    #[cfg(Py_3_11)]
    test_exception!(PyBaseExceptionGroup, |_| PyBaseExceptionGroup::new_err((
        "msg",
        vec![PyValueError::new_err("err")]
    )));
    test_exception!(PyBaseException);
    test_exception!(PyException);
    test_exception!(PyStopAsyncIteration);
    test_exception!(PyStopIteration);
    test_exception!(PyGeneratorExit);
    test_exception!(PyArithmeticError);
    test_exception!(PyLookupError);
    test_exception!(PyAssertionError);
    test_exception!(PyAttributeError);
    test_exception!(PyBufferError);
    test_exception!(PyEOFError);
    test_exception!(PyFloatingPointError);
    test_exception!(PyOSError);
    test_exception!(PyImportError);
    test_exception!(PyModuleNotFoundError);
    test_exception!(PyIndexError);
    test_exception!(PyKeyError);
    test_exception!(PyKeyboardInterrupt);
    test_exception!(PyMemoryError);
    test_exception!(PyNameError);
    test_exception!(PyOverflowError);
    test_exception!(PyRuntimeError);
    test_exception!(PyRecursionError);
    test_exception!(PyNotImplementedError);
    test_exception!(PySyntaxError);
    test_exception!(PyReferenceError);
    test_exception!(PySystemError);
    test_exception!(PySystemExit);
    test_exception!(PyTypeError);
    test_exception!(PyUnboundLocalError);
    test_exception!(PyUnicodeError);
    test_exception!(PyUnicodeDecodeError, |py| {
        let invalid_utf8 = b"fo\xd8o";
        #[expect(invalid_from_utf8)]
        let err = std::str::from_utf8(invalid_utf8).expect_err("should be invalid utf8");
        PyErr::from_value(
            PyUnicodeDecodeError::new_utf8(py, invalid_utf8, err)
                .unwrap()
                .into_any(),
        )
    });
    test_exception!(PyUnicodeEncodeError, |py| py
        .eval(c"chr(40960).encode('ascii')", None, None)
        .unwrap_err());
    test_exception!(PyUnicodeTranslateError, |_| {
        PyUnicodeTranslateError::new_err(("\u{3042}", 0, 1, "ouch"))
    });
    test_exception!(PyValueError);
    test_exception!(PyZeroDivisionError);
    test_exception!(PyBlockingIOError);
    test_exception!(PyBrokenPipeError);
    test_exception!(PyChildProcessError);
    test_exception!(PyConnectionError);
    test_exception!(PyConnectionAbortedError);
    test_exception!(PyConnectionRefusedError);
    test_exception!(PyConnectionResetError);
    test_exception!(PyFileExistsError);
    test_exception!(PyFileNotFoundError);
    test_exception!(PyInterruptedError);
    test_exception!(PyIsADirectoryError);
    test_exception!(PyNotADirectoryError);
    test_exception!(PyPermissionError);
    test_exception!(PyProcessLookupError);
    test_exception!(PyTimeoutError);
    test_exception!(PyEnvironmentError);
    test_exception!(PyIOError);
    #[cfg(windows)]
    test_exception!(PyWindowsError);

    test_exception!(PyWarning);
    test_exception!(PyUserWarning);
    test_exception!(PyDeprecationWarning);
    test_exception!(PyPendingDeprecationWarning);
    test_exception!(PySyntaxWarning);
    test_exception!(PyRuntimeWarning);
    test_exception!(PyFutureWarning);
    test_exception!(PyImportWarning);
    test_exception!(PyUnicodeWarning);
    test_exception!(PyBytesWarning);
    #[cfg(Py_3_10)]
    test_exception!(PyEncodingWarning);

    #[test]
    #[allow(invalid_from_utf8)]
    fn unicode_decode_error_from_utf8() {
        Python::attach(|py| {
            let bytes = b"abc\xffdef".to_vec();

            let check_err = |py_err: PyErr| {
                let py_err = py_err.into_bound_py_any(py).unwrap();

                assert!(py_err.is_instance_of::<PyUnicodeDecodeError>());
                assert_eq!(
                    py_err
                        .getattr("encoding")
                        .unwrap()
                        .extract::<String>()
                        .unwrap(),
                    "utf-8"
                );
                assert_eq!(
                    py_err
                        .getattr("object")
                        .unwrap()
                        .extract::<Vec<u8>>()
                        .unwrap(),
                    &*bytes
                );
                assert_eq!(
                    py_err.getattr("start").unwrap().extract::<usize>().unwrap(),
                    3
                );
                assert_eq!(
                    py_err.getattr("end").unwrap().extract::<usize>().unwrap(),
                    4
                );
                assert_eq!(
                    py_err
                        .getattr("reason")
                        .unwrap()
                        .extract::<String>()
                        .unwrap(),
                    "invalid utf-8"
                );
            };

            let utf8_err_with_bytes = PyUnicodeDecodeError::new_err_from_utf8(
                py,
                &bytes,
                std::str::from_utf8(&bytes).expect_err("\\xff is invalid utf-8"),
            );
            check_err(utf8_err_with_bytes);
        })
    }
}
