//! Plugin error type for guest-side error handling.
//!
//! [`PluginError`] is the error type returned by plugin execute functions.
//! It serializes to JSON so the host can read error details from WASM
//! linear memory when execution fails.

use std::fmt;

use serde::{Deserialize, Serialize};

/// Error type for plugin functions.
///
/// This type is serialized to JSON when a plugin execution fails, allowing
/// the host to extract a structured error message and optional error code.
///
/// # Examples
///
/// ```
/// use kailash_plugin_guest::error::PluginError;
///
/// let err = PluginError::new("input validation failed");
/// assert_eq!(err.message, "input validation failed");
/// assert!(err.code.is_none());
///
/// let err = PluginError::with_code("timeout", 408);
/// assert_eq!(err.code, Some(408));
/// ```
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PluginError {
    /// Human-readable error message.
    pub message: String,
    /// Optional numeric error code for programmatic handling.
    pub code: Option<i32>,
}

impl PluginError {
    /// Creates a new error with a message and no error code.
    pub fn new(message: impl Into<String>) -> Self {
        Self {
            message: message.into(),
            code: None,
        }
    }

    /// Creates a new error with a message and an error code.
    pub fn with_code(message: impl Into<String>, code: i32) -> Self {
        Self {
            message: message.into(),
            code: Some(code),
        }
    }
}

impl fmt::Display for PluginError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if let Some(code) = self.code {
            write!(f, "[{code}] {}", self.message)
        } else {
            write!(f, "{}", self.message)
        }
    }
}

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

impl From<serde_json::Error> for PluginError {
    fn from(err: serde_json::Error) -> Self {
        PluginError::new(format!("JSON error: {err}"))
    }
}

impl From<std::string::FromUtf8Error> for PluginError {
    fn from(err: std::string::FromUtf8Error) -> Self {
        PluginError::new(format!("UTF-8 error: {err}"))
    }
}

#[cfg(test)]
#[allow(
    clippy::panic,
    clippy::expect_used,
    clippy::unwrap_used,
    clippy::approx_constant,
    clippy::doc_markdown,
    clippy::redundant_closure,
    clippy::unnecessary_get_then_check,
    clippy::double_comparisons,
    clippy::manual_let_else,
    clippy::result_large_err,
    clippy::bool_assert_comparison,
    clippy::needless_pass_by_value,
    clippy::unnecessary_cast,
    dead_code
)]
mod tests {
    use super::*;

    #[test]
    fn new_error_has_message_no_code() {
        let err = PluginError::new("something went wrong");
        assert_eq!(err.message, "something went wrong");
        assert_eq!(err.code, None);
    }

    #[test]
    fn error_with_code() {
        let err = PluginError::with_code("not found", 404);
        assert_eq!(err.message, "not found");
        assert_eq!(err.code, Some(404));
    }

    #[test]
    fn display_without_code() {
        let err = PluginError::new("oops");
        assert_eq!(err.to_string(), "oops");
    }

    #[test]
    fn display_with_code() {
        let err = PluginError::with_code("bad request", 400);
        assert_eq!(err.to_string(), "[400] bad request");
    }

    #[test]
    fn error_trait_is_implemented() {
        let err = PluginError::new("test");
        // Verify it implements std::error::Error
        let _: &dyn std::error::Error = &err;
    }

    #[test]
    fn json_roundtrip_without_code() {
        let original = PluginError::new("test error");
        let json = serde_json::to_string(&original).expect("serialize");
        let deserialized: PluginError = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(deserialized, original);
    }

    #[test]
    fn json_roundtrip_with_code() {
        let original = PluginError::with_code("internal error", 500);
        let json = serde_json::to_string(&original).expect("serialize");
        let deserialized: PluginError = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(deserialized, original);
    }

    #[test]
    fn json_format_without_code() {
        let err = PluginError::new("test");
        let json = serde_json::to_string(&err).expect("serialize");
        let parsed: serde_json::Value = serde_json::from_str(&json).expect("parse");
        assert_eq!(parsed["message"], "test");
        assert_eq!(parsed["code"], serde_json::Value::Null);
    }

    #[test]
    fn json_format_with_code() {
        let err = PluginError::with_code("fail", 42);
        let json = serde_json::to_string(&err).expect("serialize");
        let parsed: serde_json::Value = serde_json::from_str(&json).expect("parse");
        assert_eq!(parsed["message"], "fail");
        assert_eq!(parsed["code"], 42);
    }

    #[test]
    fn from_serde_json_error() {
        let serde_err =
            serde_json::from_str::<serde_json::Value>("not valid json").expect_err("should fail");
        let plugin_err = PluginError::from(serde_err);
        assert!(plugin_err.message.contains("JSON error"));
        assert_eq!(plugin_err.code, None);
    }

    #[test]
    fn from_utf8_error() {
        let invalid_bytes = vec![0xFF, 0xFE];
        let utf8_err = String::from_utf8(invalid_bytes).expect_err("should fail");
        let plugin_err = PluginError::from(utf8_err);
        assert!(plugin_err.message.contains("UTF-8 error"));
        assert_eq!(plugin_err.code, None);
    }

    #[test]
    fn clone_equality() {
        let err = PluginError::with_code("cloned", 123);
        let cloned = err.clone();
        assert_eq!(err, cloned);
    }

    #[test]
    fn debug_format() {
        let err = PluginError::with_code("debug test", 1);
        let debug = format!("{err:?}");
        assert!(debug.contains("debug test"));
        assert!(debug.contains("1"));
    }

    #[test]
    fn from_string_into() {
        let err = PluginError::new(String::from("owned string"));
        assert_eq!(err.message, "owned string");
    }

    #[test]
    fn from_str_into() {
        let err = PluginError::new("borrowed str");
        assert_eq!(err.message, "borrowed str");
    }
}
