/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0.
 */

//! New-type for a configurable app name.

use std::borrow::Cow;
use std::error::Error;
use std::fmt;
use std::sync::atomic::{AtomicBool, Ordering};

static APP_NAME_LEN_RECOMMENDATION_WARN_EMITTED: AtomicBool = AtomicBool::new(false);

/// App name that can be configured with an AWS SDK client to become part of the user agent string.
///
/// This name is used to identify the application in the user agent that gets sent along with requests.
///
/// The name may only have alphanumeric characters and any of these characters:
/// ```text
/// !#$%&'*+-.^_`|~
/// ```
/// Spaces are not allowed.
///
/// App names are recommended to be no more than 50 characters.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AppName(Cow<'static, str>);

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

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

impl AppName {
    /// Creates a new app name.
    ///
    /// This will return an `InvalidAppName` error if the given name doesn't meet the
    /// character requirements. See [`AppName`] for details on these requirements.
    pub fn new(app_name: impl Into<Cow<'static, str>>) -> Result<Self, InvalidAppName> {
        let app_name = app_name.into();

        fn valid_character(c: char) -> bool {
            match c {
                _ if c.is_ascii_alphanumeric() => true,
                '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '.' | '^' | '_' | '`'
                | '|' | '~' => true,
                _ => false,
            }
        }
        if !app_name.chars().all(valid_character) {
            return Err(InvalidAppName);
        }
        if app_name.len() > 50 {
            if let Ok(false) = APP_NAME_LEN_RECOMMENDATION_WARN_EMITTED.compare_exchange(
                false,
                true,
                Ordering::Acquire,
                Ordering::Relaxed,
            ) {
                tracing::warn!(
                    "The `app_name` set when configuring the SDK client is recommended \
                     to have no more than 50 characters."
                )
            }
        }
        Ok(Self(app_name))
    }
}

/// Error for when an app name doesn't meet character requirements.
///
/// See [`AppName`] for details on these requirements.
#[derive(Debug)]
#[non_exhaustive]
pub struct InvalidAppName;

impl Error for InvalidAppName {}

impl fmt::Display for InvalidAppName {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "The app name can only have alphanumeric characters, or any of \
             '!' |  '#' |  '$' |  '%' |  '&' |  '\\'' |  '*' |  '+' |  '-' | \
             '.' |  '^' |  '_' |  '`' |  '|' |  '~'"
        )
    }
}

#[cfg(test)]
mod tests {
    use super::AppName;
    use crate::app_name::APP_NAME_LEN_RECOMMENDATION_WARN_EMITTED;
    use std::sync::atomic::Ordering;

    #[test]
    fn validation() {
        assert!(AppName::new("asdf1234ASDF!#$%&'*+-.^_`|~").is_ok());
        assert!(AppName::new("foo bar").is_err());
        assert!(AppName::new("🚀").is_err());
    }

    #[tracing_test::traced_test]
    #[test]
    fn log_warn_once() {
        // Pre-condition: make sure we start in the expected state of having never logged this
        assert!(!APP_NAME_LEN_RECOMMENDATION_WARN_EMITTED.load(Ordering::Relaxed));

        // Verify a short app name doesn't log
        AppName::new("not-long").unwrap();
        assert!(!logs_contain(
            "is recommended to have no more than 50 characters"
        ));
        assert!(!APP_NAME_LEN_RECOMMENDATION_WARN_EMITTED.load(Ordering::Relaxed));

        // Verify a long app name logs
        AppName::new("greaterthanfiftycharactersgreaterthanfiftycharacters").unwrap();
        assert!(logs_contain(
            "is recommended to have no more than 50 characters"
        ));
        assert!(APP_NAME_LEN_RECOMMENDATION_WARN_EMITTED.load(Ordering::Relaxed));

        // Now verify it only logs once ever

        // HACK: there's no way to reset tracing-test, so just
        // reach into its internals and clear it manually
        tracing_test::internal::GLOBAL_BUF.lock().unwrap().clear();

        AppName::new("greaterthanfiftycharactersgreaterthanfiftycharacters").unwrap();
        assert!(!logs_contain(
            "is recommended to have no more than 50 characters"
        ));
    }
}
