//! OAuth client identifier newtype.
//!
//! Provides a validated client ID type that ensures non-empty values.

use std::fmt;
use std::str::FromStr;

use serde::{Deserialize, Serialize};

use crate::error::LastIDError;

/// A validated OAuth client identifier.
///
/// Client IDs must be non-empty strings. This newtype ensures validation
/// on construction to catch empty client IDs early in the configuration
/// process rather than at request time.
///
/// # Examples
///
/// ```rust
/// use lastid_sdk::types::ClientId;
///
/// // Valid client ID
/// let client_id = ClientId::new("my-app").unwrap();
/// assert_eq!(client_id.as_str(), "my-app");
///
/// // Empty client ID fails validation
/// assert!(ClientId::new("").is_err());
/// assert!(ClientId::new("   ").is_err());
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct ClientId(String);

impl ClientId {
    /// Creates a new `ClientId` after validating it's non-empty.
    ///
    /// # Errors
    ///
    /// Returns `LastIDError::EmptyClientId` if the string is empty or
    /// contains only whitespace.
    pub fn new(value: impl Into<String>) -> Result<Self, LastIDError> {
        let s = value.into();
        Self::validate(&s)?;
        Ok(Self(s))
    }

    /// Returns the client ID as a string slice.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }

    /// Validates a client ID string.
    fn validate(s: &str) -> Result<(), LastIDError> {
        if s.is_empty() || s.trim().is_empty() {
            return Err(LastIDError::EmptyClientId);
        }
        Ok(())
    }
}

impl fmt::Display for ClientId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl AsRef<str> for ClientId {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

impl From<ClientId> for String {
    fn from(client_id: ClientId) -> Self {
        client_id.0
    }
}

impl TryFrom<String> for ClientId {
    type Error = LastIDError;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        Self::new(value)
    }
}

impl TryFrom<&str> for ClientId {
    type Error = LastIDError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        Self::new(value)
    }
}

impl FromStr for ClientId {
    type Err = LastIDError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::new(s)
    }
}

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

    #[test]
    fn test_valid_client_id() {
        let id = ClientId::new("my-app").unwrap();
        assert_eq!(id.as_str(), "my-app");
    }

    #[test]
    fn test_valid_client_id_with_special_chars() {
        let id = ClientId::new("app_123-test.com").unwrap();
        assert_eq!(id.as_str(), "app_123-test.com");
    }

    #[test]
    fn test_invalid_empty() {
        assert!(ClientId::new("").is_err());
    }

    #[test]
    fn test_invalid_whitespace_only() {
        assert!(ClientId::new("   ").is_err());
        assert!(ClientId::new("\t\n").is_err());
    }

    #[test]
    fn test_display() {
        let id = ClientId::new("test-app").unwrap();
        assert_eq!(format!("{id}"), "test-app");
    }

    #[test]
    fn test_from_str() {
        let id: ClientId = "my-client".parse().unwrap();
        assert_eq!(id.as_str(), "my-client");
    }

    #[test]
    fn test_try_from_string() {
        let id = ClientId::try_from("test".to_string()).unwrap();
        assert_eq!(id.as_str(), "test");
    }

    #[test]
    fn test_into_string() {
        let id = ClientId::new("test").unwrap();
        let s: String = id.into();
        assert_eq!(s, "test");
    }

    #[test]
    fn test_serde_roundtrip() {
        let id = ClientId::new("my-app").unwrap();
        let json = serde_json::to_string(&id).unwrap();
        assert_eq!(json, "\"my-app\"");

        let deserialized: ClientId = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized, id);
    }

    #[test]
    fn test_serde_invalid_empty() {
        let result: Result<ClientId, _> = serde_json::from_str("\"\"");
        assert!(result.is_err());
    }
}
