amp_rs/
client.rs

1use std::env;
2use std::sync::Arc;
3use std::time::Duration as StdDuration;
4
5use async_trait::async_trait;
6use chrono::{Duration, Utc};
7
8use reqwest::header::AUTHORIZATION;
9use reqwest::{Client, Method, Url};
10use serde::de::DeserializeOwned;
11use thiserror::Error;
12use tokio::sync::{Mutex, OnceCell, Semaphore};
13use tokio::time::sleep;
14
15use secrecy::ExposeSecret;
16use secrecy::Secret;
17
18use crate::model::{
19    Activity, Asset, AssetActivityParams, AssetSummary, Assignment, Balance, BroadcastResponse,
20    CategoriesRequest, CategoryAdd, CategoryEdit, CategoryResponse, ChangePasswordRequest,
21    ChangePasswordResponse, CreateAssetAssignmentRequest, EditAssetRequest, GaidBalanceEntry,
22    GaidRequest, IssuanceRequest, IssuanceResponse, Outpoint, Ownership, Password, TokenData,
23    TokenInfo, TokenRequest, TokenResponse, Utxo,
24};
25
26/// Environment variables used for token environment detection
27#[derive(Debug)]
28struct EnvironmentVariables {
29    username: String,
30    password: String,
31    amp_tests: String,
32    base_url: String,
33}
34
35/// Token environment detection for automatic strategy selection
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum TokenEnvironment {
38    /// Mock environment - use isolated token management without persistence
39    Mock,
40    /// Live environment - use full token management with persistence
41    Live,
42    /// Auto-detect environment based on credentials and settings
43    Auto,
44}
45
46impl TokenEnvironment {
47    /// Detects the current token environment based on environment variables and credential patterns
48    ///
49    /// Detection logic:
50    /// 1. If `AMP_TESTS=live` is set, returns `Live`
51    /// 2. If credentials contain "mock" string, returns `Mock`
52    /// 3. If real credentials are present without live test flag, returns `Live`
53    /// 4. Fallback to `Mock` for safety
54    #[must_use]
55    pub fn detect() -> Self {
56        let env_vars = Self::read_environment_variables();
57        Self::log_detection_start(&env_vars);
58
59        if Self::is_explicit_live_environment(&env_vars.amp_tests) {
60            return Self::Live;
61        }
62
63        if Self::has_mock_credentials(&env_vars.username, &env_vars.password, &env_vars.base_url) {
64            Self::log_detection_result("mock environment via mock credentials");
65            return Self::Mock;
66        }
67
68        if Self::has_real_credentials(&env_vars.username, &env_vars.password) {
69            Self::log_detection_result("live environment via real credentials");
70            return Self::Live;
71        }
72
73        Self::log_detection_result("mock environment via fallback (no credentials)");
74        Self::Mock
75    }
76
77    /// Reads environment variables needed for token environment detection
78    fn read_environment_variables() -> EnvironmentVariables {
79        EnvironmentVariables {
80            username: env::var("AMP_USERNAME").unwrap_or_default(),
81            password: env::var("AMP_PASSWORD").unwrap_or_default(),
82            amp_tests: env::var("AMP_TESTS").unwrap_or_default(),
83            base_url: env::var("AMP_API_BASE_URL").unwrap_or_default(),
84        }
85    }
86
87    /// Logs the start of environment detection with current variable values
88    fn log_detection_start(env_vars: &EnvironmentVariables) {
89        tracing::debug!(
90            "Detecting token environment - AMP_TESTS: '{}', username: '{}', base_url: '{}'",
91            env_vars.amp_tests,
92            env_vars.username,
93            env_vars.base_url
94        );
95    }
96
97    /// Checks if the environment is explicitly set to live testing
98    fn is_explicit_live_environment(amp_tests: &str) -> bool {
99        if amp_tests == "live" {
100            Self::log_detection_result("live environment via AMP_TESTS=live");
101            true
102        } else {
103            false
104        }
105    }
106
107    /// Checks if real (non-empty) credentials are present
108    const fn has_real_credentials(username: &str, password: &str) -> bool {
109        !username.is_empty() && !password.is_empty()
110    }
111
112    /// Logs the final detection result
113    fn log_detection_result(reason: &str) {
114        tracing::info!("Detected {}", reason);
115    }
116
117    /// Checks if the provided credentials indicate a mock environment
118    ///
119    /// Mock credentials are detected by:
120    /// - Username containing "mock" (case-insensitive)
121    /// - Password containing "mock" (case-insensitive)
122    /// - Base URL containing localhost, 127.0.0.1, or "mock"
123    #[must_use]
124    pub fn has_mock_credentials(username: &str, password: &str, base_url: &str) -> bool {
125        let username_lower = username.to_lowercase();
126        let password_lower = password.to_lowercase();
127        let base_url_lower = base_url.to_lowercase();
128
129        let has_mock_username = username_lower.contains("mock");
130        let has_mock_password = password_lower.contains("mock");
131        let has_mock_url = base_url_lower.contains("localhost")
132            || base_url_lower.contains("127.0.0.1")
133            || base_url_lower.contains("mock");
134
135        let is_mock = has_mock_username || has_mock_password || has_mock_url;
136
137        tracing::debug!(
138            "Mock credential check - username: {}, password: {}, url: {}, result: {}",
139            has_mock_username,
140            has_mock_password,
141            has_mock_url,
142            is_mock
143        );
144
145        is_mock
146    }
147
148    /// Creates a token strategy based on the environment type
149    ///
150    /// # Arguments
151    /// * `mock_token` - Optional mock token to use for mock environments
152    ///
153    /// # Errors
154    /// Returns an error if strategy creation fails
155    pub async fn create_strategy(
156        &self,
157        mock_token: Option<String>,
158    ) -> Result<Box<dyn TokenStrategy>, Error> {
159        match self {
160            Self::Mock => Ok(Self::create_mock_strategy(mock_token)),
161            Self::Live => Self::create_live_strategy().await,
162            Self::Auto => Self::create_auto_detected_strategy(mock_token).await,
163        }
164    }
165
166    /// Creates a mock token strategy with the provided or default token
167    fn create_mock_strategy(mock_token: Option<String>) -> Box<dyn TokenStrategy> {
168        let token = mock_token.unwrap_or_else(|| "default_mock_token".to_string());
169        tracing::debug!("Creating mock token strategy with token");
170        Box::new(MockTokenStrategy::new(token))
171    }
172
173    /// Creates a live token strategy
174    async fn create_live_strategy() -> Result<Box<dyn TokenStrategy>, Error> {
175        tracing::debug!("Creating live token strategy");
176        let strategy = LiveTokenStrategy::new().await?;
177        Ok(Box::new(strategy))
178    }
179
180    /// Creates a strategy based on auto-detected environment
181    async fn create_auto_detected_strategy(
182        mock_token: Option<String>,
183    ) -> Result<Box<dyn TokenStrategy>, Error> {
184        tracing::debug!("Auto-detecting environment for strategy creation");
185        let detected = Self::detect();
186
187        match detected {
188            Self::Mock => Ok(Self::create_auto_detected_mock_strategy(mock_token)),
189            Self::Live => Self::create_auto_detected_live_strategy().await,
190            Self::Auto => Self::handle_unexpected_auto_detection(),
191        }
192    }
193
194    /// Creates a mock strategy for auto-detected mock environment
195    fn create_auto_detected_mock_strategy(mock_token: Option<String>) -> Box<dyn TokenStrategy> {
196        let token = mock_token.unwrap_or_else(|| "default_mock_token".to_string());
197        tracing::debug!("Auto-detected mock environment, creating mock strategy");
198        Box::new(MockTokenStrategy::new(token))
199    }
200
201    /// Creates a live strategy for auto-detected live environment
202    async fn create_auto_detected_live_strategy() -> Result<Box<dyn TokenStrategy>, Error> {
203        tracing::debug!("Auto-detected live environment, creating live strategy");
204        let strategy = LiveTokenStrategy::new().await?;
205        Ok(Box::new(strategy))
206    }
207
208    /// Handles the unexpected case where `detect()` returns Auto
209    fn handle_unexpected_auto_detection() -> Result<Box<dyn TokenStrategy>, Error> {
210        tracing::error!("Unexpected Auto environment from detect()");
211        Err(Error::Token(TokenError::validation(
212            "Environment detection returned Auto, which should not happen".to_string(),
213        )))
214    }
215
216    /// Creates a token strategy with automatic environment detection
217    ///
218    /// This is a convenience method that combines environment detection with strategy creation.
219    ///
220    /// # Arguments
221    /// * `mock_token` - Optional mock token to use if mock environment is detected
222    ///
223    /// # Errors
224    /// Returns an error if strategy creation fails
225    pub async fn create_auto_strategy(
226        mock_token: Option<String>,
227    ) -> Result<Box<dyn TokenStrategy>, Error> {
228        let environment = Self::detect();
229        environment.create_strategy(mock_token).await
230    }
231
232    /// Determines if token persistence should be enabled for this environment
233    #[must_use]
234    pub fn should_persist_tokens(&self) -> bool {
235        match self {
236            Self::Mock => false,
237            Self::Live => true,
238            Self::Auto => Self::detect().should_persist_tokens(),
239        }
240    }
241
242    /// Returns true if this is a mock environment
243    #[must_use]
244    pub fn is_mock(&self) -> bool {
245        matches!(self, Self::Mock) || (matches!(self, Self::Auto) && Self::detect().is_mock())
246    }
247
248    /// Returns true if this is a live environment
249    #[must_use]
250    pub fn is_live(&self) -> bool {
251        matches!(self, Self::Live) || (matches!(self, Self::Auto) && Self::detect().is_live())
252    }
253}
254
255/// Token management strategy trait for different token handling approaches
256#[async_trait]
257pub trait TokenStrategy: Send + Sync + std::fmt::Debug {
258    /// Gets a valid authentication token
259    async fn get_token(&self) -> Result<String, Error>;
260
261    /// Clears stored token (for testing)
262    async fn clear_token(&self) -> Result<(), Error>;
263
264    /// Returns whether this strategy should persist tokens
265    fn should_persist(&self) -> bool;
266
267    /// Returns the strategy type for debugging
268    fn strategy_type(&self) -> &'static str;
269
270    /// Returns self as Any for downcasting (used internally)
271    fn as_any(&self) -> &dyn std::any::Any;
272}
273
274/// Mock token strategy for isolated testing without persistence
275#[derive(Debug, Clone)]
276pub struct MockTokenStrategy {
277    token: String,
278}
279
280impl MockTokenStrategy {
281    /// Creates a new mock token strategy with the provided token
282    #[must_use]
283    pub const fn new(token: String) -> Self {
284        Self { token }
285    }
286
287    /// Creates a mock token strategy with a default test token
288    #[must_use]
289    pub fn with_default_token() -> Self {
290        Self::new("mock_token_default".to_string())
291    }
292
293    /// Creates a mock token strategy for a specific test case
294    #[must_use]
295    pub fn for_test(test_name: &str) -> Self {
296        Self::new(format!("mock_token_{test_name}"))
297    }
298}
299
300#[async_trait]
301impl TokenStrategy for MockTokenStrategy {
302    async fn get_token(&self) -> Result<String, Error> {
303        tracing::debug!("Using mock token strategy - returning pre-set token");
304        Ok(self.token.clone())
305    }
306
307    async fn clear_token(&self) -> Result<(), Error> {
308        tracing::debug!("Mock token strategy - clear_token is a no-op");
309        Ok(())
310    }
311
312    fn should_persist(&self) -> bool {
313        false
314    }
315
316    fn strategy_type(&self) -> &'static str {
317        "mock"
318    }
319
320    fn as_any(&self) -> &dyn std::any::Any {
321        self
322    }
323}
324
325/// Live token strategy that wraps the existing `TokenManager` for full token management
326#[derive(Debug)]
327pub struct LiveTokenStrategy {
328    token_manager: Arc<TokenManager>,
329}
330
331impl LiveTokenStrategy {
332    /// Creates a new live token strategy using the global `TokenManager` instance
333    ///
334    /// # Errors
335    /// Returns an error if the `TokenManager` cannot be initialized
336    pub async fn new() -> Result<Self, Error> {
337        let token_manager = TokenManager::get_global_instance().await?;
338        Ok(Self { token_manager })
339    }
340
341    /// Creates a new live token strategy with a custom `TokenManager`
342    #[must_use]
343    pub const fn with_token_manager(token_manager: Arc<TokenManager>) -> Self {
344        Self { token_manager }
345    }
346
347    /// Creates a live token strategy with custom retry configuration
348    ///
349    /// # Errors
350    /// Returns an error if the `TokenManager` cannot be initialized
351    pub async fn with_config(config: RetryConfig) -> Result<Self, Error> {
352        let base_url = get_amp_api_base_url()?;
353        let token_manager =
354            Arc::new(TokenManager::with_config_and_base_url(config, base_url).await?);
355        Ok(Self { token_manager })
356    }
357
358    /// Creates a live token strategy optimized for testing
359    ///
360    /// # Errors
361    /// Returns an error if the `TokenManager` cannot be initialized
362    pub async fn for_testing() -> Result<Self, Error> {
363        let config = RetryConfig::for_tests();
364        Self::with_config(config).await
365    }
366
367    /// Gets current token information for debugging and monitoring
368    ///
369    /// # Errors
370    /// Returns an error if token information retrieval fails
371    pub async fn get_token_info(&self) -> Result<Option<TokenInfo>, Error> {
372        self.token_manager.get_token_info().await
373    }
374}
375
376#[async_trait]
377impl TokenStrategy for LiveTokenStrategy {
378    async fn get_token(&self) -> Result<String, Error> {
379        tracing::debug!("Using live token strategy - full token management");
380        self.token_manager.get_token().await
381    }
382
383    async fn clear_token(&self) -> Result<(), Error> {
384        self.token_manager.clear_token().await
385    }
386
387    fn should_persist(&self) -> bool {
388        true
389    }
390
391    fn strategy_type(&self) -> &'static str {
392        "live"
393    }
394
395    fn as_any(&self) -> &dyn std::any::Any {
396        self
397    }
398}
399
400#[derive(Error, Debug)]
401pub enum Error {
402    #[error("Missing {0} environment variable")]
403    MissingEnvVar(String),
404    #[error("AMP request failed: {0}")]
405    RequestFailed(String),
406    #[error("Failed to parse AMP response: {0}")]
407    ResponseParsingFailed(String),
408    #[error("AMP token request failed with status {status}: {error_text}")]
409    TokenRequestFailed {
410        status: reqwest::StatusCode,
411        error_text: String,
412    },
413    #[error("Failed to parse url: {0}")]
414    UrlParse(#[from] url::ParseError),
415    #[error("Reqwest error: {0}")]
416    Reqwest(#[from] reqwest::Error),
417    #[error("Invalid retry configuration: {0}")]
418    InvalidRetryConfig(String),
419    #[error("Token management error: {0}")]
420    Token(#[from] TokenError),
421}
422
423/// Detailed error types for token management operations
424#[derive(Error, Debug, Clone, PartialEq, Eq)]
425pub enum TokenError {
426    #[error("Token refresh failed: {0}")]
427    RefreshFailed(String),
428    #[error("Token obtain failed after {attempts} attempts: {last_error}")]
429    ObtainFailed { attempts: u32, last_error: String },
430    #[error("Rate limited: retry after {retry_after_seconds} seconds")]
431    RateLimited { retry_after_seconds: u64 },
432    #[error("Request timeout after {timeout_seconds} seconds")]
433    Timeout { timeout_seconds: u64 },
434    #[error("Serialization error: {0}")]
435    Serialization(String),
436    #[error("Token storage error: {0}")]
437    Storage(String),
438    #[error("Token validation error: {0}")]
439    Validation(String),
440}
441
442impl TokenError {
443    /// Creates a new `RefreshFailed` error
444    #[must_use]
445    pub fn refresh_failed<S: Into<String>>(message: S) -> Self {
446        Self::RefreshFailed(message.into())
447    }
448
449    /// Creates a new `ObtainFailed` error
450    #[must_use]
451    pub const fn obtain_failed(attempts: u32, last_error: String) -> Self {
452        Self::ObtainFailed {
453            attempts,
454            last_error,
455        }
456    }
457
458    /// Creates a new `RateLimited` error
459    #[must_use]
460    pub const fn rate_limited(retry_after_seconds: u64) -> Self {
461        Self::RateLimited {
462            retry_after_seconds,
463        }
464    }
465
466    /// Creates a new Timeout error
467    #[must_use]
468    pub const fn timeout(timeout_seconds: u64) -> Self {
469        Self::Timeout { timeout_seconds }
470    }
471
472    /// Creates a new Serialization error
473    #[must_use]
474    pub fn serialization<S: Into<String>>(message: S) -> Self {
475        Self::Serialization(message.into())
476    }
477
478    /// Creates a new Storage error
479    #[must_use]
480    pub fn storage<S: Into<String>>(message: S) -> Self {
481        Self::Storage(message.into())
482    }
483
484    /// Creates a new Validation error
485    #[must_use]
486    pub fn validation<S: Into<String>>(message: S) -> Self {
487        Self::Validation(message.into())
488    }
489
490    /// Returns true if this error indicates a retryable condition
491    #[must_use]
492    pub const fn is_retryable(&self) -> bool {
493        matches!(
494            self,
495            Self::RefreshFailed(_) | Self::RateLimited { .. } | Self::Timeout { .. }
496        )
497    }
498
499    /// Returns true if this error indicates a rate limiting condition
500    #[must_use]
501    pub const fn is_rate_limited(&self) -> bool {
502        matches!(self, Self::RateLimited { .. })
503    }
504
505    /// Returns the retry delay in seconds if this is a rate limited error
506    #[must_use]
507    pub const fn retry_after_seconds(&self) -> Option<u64> {
508        match self {
509            Self::RateLimited {
510                retry_after_seconds,
511            } => Some(*retry_after_seconds),
512            _ => None,
513        }
514    }
515}
516
517// Conversion from serde_json::Error for serialization errors
518impl From<serde_json::Error> for TokenError {
519    fn from(err: serde_json::Error) -> Self {
520        Self::Serialization(err.to_string())
521    }
522}
523
524/// Configuration for retry behavior in API requests
525#[derive(Debug, Clone)]
526pub struct RetryConfig {
527    /// Maximum number of retry attempts
528    pub max_attempts: u32,
529    /// Base delay in milliseconds for exponential backoff
530    pub base_delay_ms: u64,
531    /// Maximum delay in milliseconds to cap exponential backoff
532    pub max_delay_ms: u64,
533    /// Request timeout in seconds
534    pub timeout_seconds: u64,
535}
536
537impl Default for RetryConfig {
538    fn default() -> Self {
539        Self {
540            max_attempts: 3,
541            base_delay_ms: 1000,
542            max_delay_ms: 30_000,
543            timeout_seconds: 10,
544        }
545    }
546}
547
548impl RetryConfig {
549    /// Creates a `RetryConfig` from environment variables with default fallbacks
550    ///
551    /// Environment variables:
552    /// - `API_RETRY_MAX_ATTEMPTS`: Maximum retry attempts (default: 3)
553    /// - `API_RETRY_BASE_DELAY_MS`: Base delay in milliseconds (default: 1000)
554    /// - `API_RETRY_MAX_DELAY_MS`: Maximum delay in milliseconds (default: 30000)
555    /// - `API_REQUEST_TIMEOUT_SECONDS`: Request timeout in seconds (default: 10)
556    ///
557    /// # Errors
558    ///
559    /// Returns an error if any environment variable contains an invalid value
560    pub fn from_env() -> Result<Self, Error> {
561        let max_attempts = match env::var("API_RETRY_MAX_ATTEMPTS") {
562            Ok(val) => val.parse::<u32>().map_err(|e| {
563                Error::InvalidRetryConfig(format!("Invalid API_RETRY_MAX_ATTEMPTS: {e}"))
564            })?,
565            Err(_) => 3,
566        };
567
568        let base_delay_ms = match env::var("API_RETRY_BASE_DELAY_MS") {
569            Ok(val) => val.parse::<u64>().map_err(|e| {
570                Error::InvalidRetryConfig(format!("Invalid API_RETRY_BASE_DELAY_MS: {e}"))
571            })?,
572            Err(_) => 1000,
573        };
574
575        let max_delay_ms = match env::var("API_RETRY_MAX_DELAY_MS") {
576            Ok(val) => val.parse::<u64>().map_err(|e| {
577                Error::InvalidRetryConfig(format!("Invalid API_RETRY_MAX_DELAY_MS: {e}"))
578            })?,
579            Err(_) => 30_000,
580        };
581
582        let timeout_seconds = match env::var("API_REQUEST_TIMEOUT_SECONDS") {
583            Ok(val) => val.parse::<u64>().map_err(|e| {
584                Error::InvalidRetryConfig(format!("Invalid API_REQUEST_TIMEOUT_SECONDS: {e}"))
585            })?,
586            Err(_) => 10,
587        };
588
589        // Validate configuration
590        if max_attempts == 0 {
591            return Err(Error::InvalidRetryConfig(
592                "max_attempts must be greater than 0".to_string(),
593            ));
594        }
595        if base_delay_ms == 0 {
596            return Err(Error::InvalidRetryConfig(
597                "base_delay_ms must be greater than 0".to_string(),
598            ));
599        }
600        if max_delay_ms < base_delay_ms {
601            return Err(Error::InvalidRetryConfig(
602                "max_delay_ms must be greater than or equal to base_delay_ms".to_string(),
603            ));
604        }
605        if timeout_seconds == 0 {
606            return Err(Error::InvalidRetryConfig(
607                "timeout_seconds must be greater than 0".to_string(),
608            ));
609        }
610
611        Ok(Self {
612            max_attempts,
613            base_delay_ms,
614            max_delay_ms,
615            timeout_seconds,
616        })
617    }
618
619    /// Creates a `RetryConfig` optimized for test environments
620    ///
621    /// Uses reduced values for faster test execution:
622    /// - 2 retry attempts
623    /// - 500ms base delay
624    /// - 5000ms max delay
625    /// - 5 second timeout
626    #[must_use]
627    pub const fn for_tests() -> Self {
628        Self {
629            max_attempts: 2,
630            base_delay_ms: 500,
631            max_delay_ms: 5000,
632            timeout_seconds: 5,
633        }
634    }
635
636    /// Sets a custom timeout value
637    #[must_use]
638    pub const fn with_timeout(mut self, timeout_seconds: u64) -> Self {
639        self.timeout_seconds = timeout_seconds;
640        self
641    }
642
643    /// Sets custom max attempts
644    #[must_use]
645    pub const fn with_max_attempts(mut self, max_attempts: u32) -> Self {
646        self.max_attempts = max_attempts;
647        self
648    }
649
650    /// Sets custom base delay
651    #[must_use]
652    pub const fn with_base_delay_ms(mut self, base_delay_ms: u64) -> Self {
653        self.base_delay_ms = base_delay_ms;
654        self
655    }
656
657    /// Sets custom max delay
658    #[must_use]
659    pub const fn with_max_delay_ms(mut self, max_delay_ms: u64) -> Self {
660        self.max_delay_ms = max_delay_ms;
661        self
662    }
663}
664
665/// HTTP client with sophisticated retry logic and exponential backoff
666#[derive(Debug, Clone)]
667pub struct RetryClient {
668    client: Client,
669    config: RetryConfig,
670}
671
672impl RetryClient {
673    /// Creates a new `RetryClient` with the given configuration
674    #[must_use]
675    pub fn new(config: RetryConfig) -> Self {
676        Self {
677            client: Client::new(),
678            config,
679        }
680    }
681
682    /// Creates a new `RetryClient` with default configuration
683    #[must_use]
684    pub fn with_default_config() -> Self {
685        Self::new(RetryConfig::default())
686    }
687
688    /// Creates a new `RetryClient` with test-optimized configuration
689    #[must_use]
690    pub fn for_tests() -> Self {
691        Self::new(RetryConfig::for_tests())
692    }
693
694    /// Executes an HTTP request with retry logic and exponential backoff
695    ///
696    /// # Arguments
697    /// * `request_builder` - A function that creates the request builder
698    ///
699    /// # Returns
700    /// The response if successful, or an error after all retries are exhausted
701    ///
702    /// # Errors
703    /// Returns `TokenError::Timeout` if the request times out
704    /// Returns `TokenError::RateLimited` if rate limited and retries are exhausted
705    /// Returns `TokenError::ObtainFailed` if all retry attempts fail
706    #[allow(clippy::cognitive_complexity)]
707    pub async fn execute_with_retry<F>(
708        &self,
709        request_builder: F,
710    ) -> Result<reqwest::Response, TokenError>
711    where
712        F: Fn() -> reqwest::RequestBuilder + Send + Sync,
713    {
714        let mut last_error = String::new();
715        let mut attempt = 0;
716
717        while attempt < self.config.max_attempts {
718            attempt += 1;
719
720            // Create the request with timeout
721            let request =
722                request_builder().timeout(StdDuration::from_secs(self.config.timeout_seconds));
723
724            // Execute the request
725            match request.send().await {
726                Ok(response) => {
727                    let status = response.status();
728
729                    // Handle rate limiting (429 Too Many Requests)
730                    if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
731                        let retry_after = Self::extract_retry_after(&response).unwrap_or(60);
732
733                        tracing::warn!(
734                            "Rate limited (429) on attempt {}/{}. Retry after {} seconds",
735                            attempt,
736                            self.config.max_attempts,
737                            retry_after
738                        );
739
740                        // If this is our last attempt, return the rate limit error
741                        if attempt >= self.config.max_attempts {
742                            return Err(TokenError::rate_limited(retry_after));
743                        }
744
745                        // Wait for the rate limit period (or our max delay, whichever is smaller)
746                        let delay_ms = std::cmp::min(retry_after * 1000, self.config.max_delay_ms);
747                        sleep(StdDuration::from_millis(delay_ms)).await;
748                        continue;
749                    }
750
751                    // Handle other client errors (4xx) - these are generally not retryable
752                    if status.is_client_error() && status != reqwest::StatusCode::TOO_MANY_REQUESTS
753                    {
754                        last_error = format!("Client error: {status}");
755                        tracing::error!("Non-retryable client error: {}", status);
756                        break;
757                    }
758
759                    // Handle server errors (5xx) - these are retryable
760                    if status.is_server_error() {
761                        last_error = format!("Server error: {status}");
762                        tracing::warn!(
763                            "Server error {} on attempt {}/{}",
764                            status,
765                            attempt,
766                            self.config.max_attempts
767                        );
768
769                        if attempt < self.config.max_attempts {
770                            let delay = self.calculate_backoff_delay(attempt);
771                            sleep(delay).await;
772                            continue;
773                        }
774                        break;
775                    }
776
777                    // Success case
778                    return Ok(response);
779                }
780                Err(e) => {
781                    last_error = e.to_string();
782
783                    // Check if this is a timeout error
784                    if e.is_timeout() {
785                        tracing::warn!(
786                            "Request timeout on attempt {}/{}",
787                            attempt,
788                            self.config.max_attempts
789                        );
790
791                        if attempt >= self.config.max_attempts {
792                            return Err(TokenError::timeout(self.config.timeout_seconds));
793                        }
794                    } else {
795                        tracing::warn!(
796                            "Request failed on attempt {}/{}: {}",
797                            attempt,
798                            self.config.max_attempts,
799                            e
800                        );
801                    }
802
803                    // If we have more attempts, wait and retry
804                    if attempt < self.config.max_attempts {
805                        let delay = self.calculate_backoff_delay(attempt);
806                        sleep(delay).await;
807                    }
808                }
809            }
810        }
811
812        // All retries exhausted
813        Err(TokenError::obtain_failed(attempt, last_error))
814    }
815
816    /// Calculates the delay for exponential backoff with jitter
817    ///
818    /// Uses the formula: `min(base_delay * 2^(attempt-1) + jitter, max_delay)`
819    /// where jitter is a random value between 0 and `base_delay/2`
820    pub fn calculate_backoff_delay(&self, attempt: u32) -> StdDuration {
821        use rand::Rng;
822
823        let base_delay = self.config.base_delay_ms;
824        let max_delay = self.config.max_delay_ms;
825
826        // Calculate exponential backoff: base_delay * 2^(attempt-1)
827        let exponential_delay = base_delay * 2_u64.pow(attempt.saturating_sub(1));
828
829        // Add jitter (random value between 0 and base_delay/2)
830        let jitter = rand::thread_rng().gen_range(0..=base_delay / 2);
831        let total_delay = exponential_delay + jitter;
832
833        // Cap at max_delay
834        let final_delay = std::cmp::min(total_delay, max_delay);
835
836        tracing::debug!(
837            "Calculated backoff delay for attempt {}: {}ms (exponential: {}ms, jitter: {}ms, capped at: {}ms)",
838            attempt,
839            final_delay,
840            exponential_delay,
841            jitter,
842            max_delay
843        );
844
845        StdDuration::from_millis(final_delay)
846    }
847
848    /// Extracts the Retry-After header value from a 429 response
849    ///
850    /// Returns the number of seconds to wait, or None if the header is not present
851    /// or cannot be parsed
852    fn extract_retry_after(response: &reqwest::Response) -> Option<u64> {
853        response
854            .headers()
855            .get("retry-after")
856            .and_then(|value| value.to_str().ok())
857            .and_then(|s| s.parse::<u64>().ok())
858    }
859
860    /// Gets the underlying reqwest client
861    #[must_use]
862    pub const fn client(&self) -> &Client {
863        &self.client
864    }
865
866    /// Gets the retry configuration
867    #[must_use]
868    pub const fn config(&self) -> &RetryConfig {
869        &self.config
870    }
871}
872
873/// Singleton instance of the `TokenManager` for shared token storage across all `ApiClient` instances
874static GLOBAL_TOKEN_MANAGER: OnceCell<Arc<TokenManager>> = OnceCell::const_new();
875
876/// Core token manager with proactive refresh and secure storage
877#[derive(Debug)]
878pub struct TokenManager {
879    pub token_data: Arc<Mutex<Option<TokenData>>>,
880    pub retry_client: RetryClient,
881    base_url: Url,
882    /// Semaphore to ensure only one token operation (obtain/refresh) happens at a time
883    /// This prevents race conditions where multiple threads try to refresh/obtain simultaneously
884    token_operation_semaphore: Arc<Semaphore>,
885}
886
887impl TokenManager {
888    /// Gets the global singleton instance of `TokenManager`
889    ///
890    /// This ensures all `ApiClient` instances share the same token storage,
891    /// preventing multiple token acquisition attempts in concurrent tests.
892    ///
893    /// # Errors
894    /// Returns an error if the `TokenManager` cannot be initialized
895    pub async fn get_global_instance() -> Result<Arc<Self>, Error> {
896        let manager = GLOBAL_TOKEN_MANAGER
897            .get_or_try_init(|| async {
898                let config = RetryConfig::from_env()?;
899                let base_url = get_amp_api_base_url()?;
900                let manager = Self::with_config_and_base_url(config, base_url).await?;
901                Ok::<Arc<Self>, Error>(Arc::new(manager))
902            })
903            .await?;
904
905        Ok(manager.clone())
906    }
907
908    /// Creates a new `TokenManager` with default configuration
909    ///
910    /// # Errors
911    /// Returns an error if the base URL cannot be obtained from environment variables
912    pub async fn new() -> Result<Self, Error> {
913        let config = RetryConfig::from_env()?;
914        Self::with_config(config).await
915    }
916
917    /// Creates a new `TokenManager` with the specified retry configuration
918    ///
919    /// # Errors
920    /// Returns an error if the base URL cannot be obtained from environment variables
921    pub async fn with_config(config: RetryConfig) -> Result<Self, Error> {
922        let base_url = get_amp_api_base_url()?;
923        Self::with_config_and_base_url(config, base_url).await
924    }
925
926    /// Creates a new `TokenManager` with the specified configuration and base URL (for testing)
927    ///
928    /// # Errors
929    /// This method is infallible but returns Result for API consistency
930    pub async fn with_config_and_base_url(
931        config: RetryConfig,
932        base_url: Url,
933    ) -> Result<Self, Error> {
934        let manager = Self {
935            token_data: Arc::new(Mutex::new(None)),
936            retry_client: RetryClient::new(config),
937            base_url,
938            token_operation_semaphore: Arc::new(Semaphore::new(1)),
939        };
940
941        // Load token from disk if persistence is enabled
942        if Self::should_persist_tokens() {
943            if let Ok(Some(token_data)) = manager.load_token_from_disk().await {
944                *manager.token_data.lock().await = Some(token_data);
945                tracing::info!("Token loaded from disk during initialization");
946            }
947        }
948
949        Ok(manager)
950    }
951
952    /// Creates a new `TokenManager` with a pre-set mock token (for testing)
953    ///
954    /// # Errors
955    /// This method is infallible but returns Result for API consistency
956    pub fn with_mock_token(
957        config: RetryConfig,
958        base_url: Url,
959        mock_token: String,
960    ) -> Result<Self, Error> {
961        let expires_at = Utc::now() + Duration::hours(24); // Mock token valid for 24 hours
962        let token_data = TokenData::new(mock_token, expires_at);
963
964        let manager = Self {
965            token_data: Arc::new(Mutex::new(Some(token_data))),
966            retry_client: RetryClient::new(config),
967            base_url,
968            token_operation_semaphore: Arc::new(Semaphore::new(1)),
969        };
970
971        Ok(manager)
972    }
973
974    /// Gets a valid authentication token with proactive refresh logic
975    ///
976    /// This method implements thread-safe token management logic:
977    /// 1. Check if a valid token exists and is not expiring soon (within 5 minutes)
978    /// 2. If token needs refresh/obtain, acquire semaphore to prevent concurrent operations
979    /// 3. Double-check token state after acquiring semaphore (another thread may have updated it)
980    /// 4. Perform atomic token update operations
981    /// 5. Return the valid token
982    ///
983    /// # Thread Safety
984    /// This method is fully thread-safe and prevents race conditions by:
985    /// - Using a semaphore to ensure only one token operation at a time
986    /// - Double-checking token state after acquiring the semaphore
987    /// - Performing atomic token updates within the critical section
988    ///
989    /// # Errors
990    /// Returns a `TokenError` if token acquisition or refresh fails after all retries
991    pub async fn get_token(&self) -> Result<String, Error> {
992        // Fast path: check if we have a valid token without acquiring semaphore
993        if let Some(token) = self.check_existing_token().await? {
994            return Ok(token);
995        }
996
997        // Slow path: token needs refresh/obtain, acquire semaphore for thread safety
998        let _permit = self.acquire_token_semaphore().await?;
999
1000        // Double-check token state after acquiring semaphore - another thread may have updated it
1001        if let Some(token) = self.check_existing_token().await? {
1002            tracing::debug!("Token was updated by another thread, using existing valid token");
1003            return Ok(token);
1004        }
1005
1006        // At this point, we need to refresh or obtain a new token
1007        self.handle_token_refresh_or_obtain().await
1008    }
1009
1010    /// Checks if we have a valid existing token that doesn't expire soon
1011    async fn check_existing_token(&self) -> Result<Option<String>, Error> {
1012        let token_guard = self.token_data.lock().await;
1013        if let Some(ref token_data) = *token_guard {
1014            if !token_data.expires_soon(Duration::minutes(5)) {
1015                tracing::debug!("Using existing valid token");
1016                let token = token_data.token.expose_secret().clone();
1017                drop(token_guard);
1018                return Ok(Some(token));
1019            }
1020        }
1021        drop(token_guard);
1022        Ok(None)
1023    }
1024
1025    /// Acquires the token operation semaphore for thread-safe operations
1026    async fn acquire_token_semaphore(&self) -> Result<tokio::sync::SemaphorePermit<'_>, Error> {
1027        let permit = self
1028            .token_operation_semaphore
1029            .acquire()
1030            .await
1031            .map_err(|e| {
1032                Error::Token(TokenError::storage(format!(
1033                    "Failed to acquire token operation semaphore: {e}"
1034                )))
1035            })?;
1036
1037        tracing::debug!("Acquired token operation semaphore for thread-safe token management");
1038        Ok(permit)
1039    }
1040
1041    /// Handles the token refresh or obtain logic
1042    async fn handle_token_refresh_or_obtain(&self) -> Result<String, Error> {
1043        let needs_refresh = self.determine_token_operation().await;
1044
1045        if needs_refresh {
1046            match self.refresh_token_internal().await {
1047                Ok(token) => {
1048                    tracing::info!("Token refreshed successfully");
1049                    return Ok(token);
1050                }
1051                Err(e) => {
1052                    tracing::warn!("Token refresh failed, falling back to obtain: {e}");
1053                    // Fall through to obtain new token
1054                }
1055            }
1056        }
1057
1058        // Either we needed to obtain from the start, or refresh failed
1059        self.obtain_token_internal().await
1060    }
1061
1062    /// Determines whether we need to refresh or obtain a new token
1063    async fn determine_token_operation(&self) -> bool {
1064        let token_guard = self.token_data.lock().await;
1065        token_guard.as_ref().map_or_else(
1066            || {
1067                tracing::info!("No token exists, will obtain new token");
1068                false
1069            },
1070            |token_data| {
1071                if token_data.is_expired() {
1072                    tracing::info!("Token is expired, will obtain new token");
1073                    false
1074                } else {
1075                    tracing::info!("Token expires soon, will attempt refresh");
1076                    true
1077                }
1078            },
1079        )
1080    }
1081
1082    /// Obtains a new authentication token using environment credentials with retry logic
1083    ///
1084    /// This method:
1085    /// 1. Reads credentials from environment variables
1086    /// 2. Makes a token request with retry logic
1087    /// 3. Stores the new token with 24-hour expiry
1088    /// 4. Returns the token string
1089    ///
1090    /// # Thread Safety
1091    /// This method acquires the token operation semaphore to ensure thread-safe operation.
1092    /// For internal use within already-synchronized contexts, use `obtain_token_internal()`.
1093    ///
1094    /// # Errors
1095    /// Returns an error if:
1096    /// - Environment variables are missing
1097    /// - All retry attempts fail
1098    /// - Response parsing fails
1099    pub async fn obtain_token(&self) -> Result<String, Error> {
1100        let _permit = self
1101            .token_operation_semaphore
1102            .acquire()
1103            .await
1104            .map_err(|e| {
1105                Error::Token(TokenError::storage(format!(
1106                    "Failed to acquire token operation semaphore: {e}"
1107                )))
1108            })?;
1109
1110        self.obtain_token_internal().await
1111    }
1112
1113    /// Internal method to obtain a new authentication token without acquiring semaphore
1114    ///
1115    /// This method should only be called from contexts where the token operation semaphore
1116    /// has already been acquired (e.g., from within `get_token()`).
1117    ///
1118    /// # Errors
1119    /// Returns an error if:
1120    /// - Environment variables are missing
1121    /// - All retry attempts fail
1122    /// - Response parsing fails
1123    async fn obtain_token_internal(&self) -> Result<String, Error> {
1124        tracing::debug!("Obtaining new authentication token");
1125
1126        let request_payload = Self::get_credentials_from_env()?;
1127        let url = self.build_obtain_token_url();
1128        let response = self.execute_token_request(&url, &request_payload).await?;
1129        let token_response = self.parse_token_response(response).await?;
1130
1131        self.store_token_data(&token_response.token).await;
1132
1133        tracing::info!("New authentication token obtained successfully");
1134        Ok(token_response.token)
1135    }
1136
1137    /// Gets credentials from environment variables
1138    fn get_credentials_from_env() -> Result<TokenRequest, Error> {
1139        let username = env::var("AMP_USERNAME")
1140            .map_err(|_| Error::MissingEnvVar("AMP_USERNAME".to_string()))?;
1141        let password = env::var("AMP_PASSWORD")
1142            .map_err(|_| Error::MissingEnvVar("AMP_PASSWORD".to_string()))?;
1143
1144        Ok(TokenRequest { username, password })
1145    }
1146
1147    /// Builds the URL for token obtain endpoint
1148    fn build_obtain_token_url(&self) -> Url {
1149        let mut url = self.base_url.clone();
1150        url.path_segments_mut()
1151            .unwrap()
1152            .push("user")
1153            .push("obtain_token");
1154        url
1155    }
1156
1157    /// Executes the token request with retry logic
1158    async fn execute_token_request(
1159        &self,
1160        url: &Url,
1161        request_payload: &TokenRequest,
1162    ) -> Result<reqwest::Response, Error> {
1163        let response = self
1164            .retry_client
1165            .execute_with_retry(|| {
1166                self.retry_client
1167                    .client()
1168                    .post(url.clone())
1169                    .json(request_payload)
1170            })
1171            .await
1172            .map_err(Error::Token)?;
1173
1174        if !response.status().is_success() {
1175            let status = response.status();
1176            let error_text = response
1177                .text()
1178                .await
1179                .unwrap_or_else(|_| "Unknown error".to_string());
1180            return Err(Error::TokenRequestFailed { status, error_text });
1181        }
1182
1183        Ok(response)
1184    }
1185
1186    /// Parses the token response from the API
1187    async fn parse_token_response(
1188        &self,
1189        response: reqwest::Response,
1190    ) -> Result<TokenResponse, Error> {
1191        response
1192            .json()
1193            .await
1194            .map_err(|e| Error::ResponseParsingFailed(e.to_string()))
1195    }
1196
1197    /// Stores the token data with 24-hour expiry and optional disk persistence
1198    async fn store_token_data(&self, token: &str) {
1199        let expires_at = Utc::now() + Duration::days(1);
1200        let token_data = TokenData::new(token.to_string(), expires_at);
1201
1202        // Atomic token update - hold the lock for the minimal time needed
1203        *self.token_data.lock().await = Some(token_data.clone());
1204        tracing::debug!("Token data updated atomically in storage");
1205
1206        // Save to disk if persistence is enabled
1207        if Self::should_persist_tokens() {
1208            if let Err(e) = self.save_token_to_disk(&token_data).await {
1209                tracing::warn!("Failed to save token to disk: {e}");
1210            }
1211        }
1212    }
1213
1214    /// Refreshes the current authentication token with fallback to obtain on failure
1215    ///
1216    /// This method:
1217    /// 1. Uses the existing token to request a refresh
1218    /// 2. Updates the stored token data on success
1219    /// 3. Falls back to obtaining a new token if refresh fails
1220    ///
1221    /// # Thread Safety
1222    /// This method acquires the token operation semaphore to ensure thread-safe operation.
1223    /// For internal use within already-synchronized contexts, use `refresh_token_internal()`.
1224    ///
1225    /// # Errors
1226    /// Returns an error if both refresh and obtain operations fail
1227    pub async fn refresh_token(&self) -> Result<String, Error> {
1228        let _permit = self
1229            .token_operation_semaphore
1230            .acquire()
1231            .await
1232            .map_err(|e| {
1233                Error::Token(TokenError::storage(format!(
1234                    "Failed to acquire token operation semaphore: {e}"
1235                )))
1236            })?;
1237
1238        self.refresh_token_internal().await
1239    }
1240
1241    /// Internal method to refresh the current authentication token without acquiring semaphore
1242    ///
1243    /// This method should only be called from contexts where the token operation semaphore
1244    /// has already been acquired (e.g., from within `get_token()`).
1245    ///
1246    /// # Errors
1247    /// Returns an error if both refresh and obtain operations fail
1248    #[allow(clippy::cognitive_complexity)]
1249    async fn refresh_token_internal(&self) -> Result<String, Error> {
1250        tracing::debug!("Refreshing authentication token");
1251
1252        let Some(current_token) = self.get_current_token_for_refresh().await else {
1253            tracing::warn!("No token available for refresh, obtaining new token");
1254            return self.obtain_token_internal().await;
1255        };
1256
1257        let url = self.build_refresh_token_url();
1258        let response = self.execute_refresh_request(&url, &current_token).await;
1259
1260        match response {
1261            Ok(resp) => self.handle_refresh_response(resp).await,
1262            Err(e) => {
1263                tracing::warn!("Token refresh request failed: {e}, falling back to obtain");
1264                self.obtain_token_internal().await
1265            }
1266        }
1267    }
1268
1269    /// Gets the current token for refresh operations
1270    async fn get_current_token_for_refresh(&self) -> Option<String> {
1271        let token_guard = self.token_data.lock().await;
1272        token_guard
1273            .as_ref()
1274            .map(|token_data| token_data.token.expose_secret().clone())
1275    }
1276
1277    /// Builds the URL for token refresh endpoint
1278    fn build_refresh_token_url(&self) -> Url {
1279        let mut url = self.base_url.clone();
1280        url.path_segments_mut()
1281            .unwrap()
1282            .push("user")
1283            .push("refresh_token");
1284        url
1285    }
1286
1287    /// Executes the refresh request with retry logic
1288    async fn execute_refresh_request(
1289        &self,
1290        url: &Url,
1291        current_token: &str,
1292    ) -> Result<reqwest::Response, TokenError> {
1293        self.retry_client
1294            .execute_with_retry(|| {
1295                self.retry_client
1296                    .client()
1297                    .post(url.clone())
1298                    .header(AUTHORIZATION, format!("token {current_token}"))
1299            })
1300            .await
1301    }
1302
1303    /// Handles the refresh response, either storing the new token or falling back to obtain
1304    async fn handle_refresh_response(&self, resp: reqwest::Response) -> Result<String, Error> {
1305        if !resp.status().is_success() {
1306            let status = resp.status();
1307            let error_text = resp
1308                .text()
1309                .await
1310                .unwrap_or_else(|_| "Unknown error".to_string());
1311
1312            tracing::warn!("Token refresh failed with status {status}: {error_text}");
1313            return self.obtain_token_internal().await;
1314        }
1315
1316        let token_response: TokenResponse = resp
1317            .json()
1318            .await
1319            .map_err(|e| Error::ResponseParsingFailed(e.to_string()))?;
1320
1321        self.store_token_data(&token_response.token).await;
1322        tracing::info!("Authentication token refreshed successfully");
1323        Ok(token_response.token)
1324    }
1325
1326    /// Gets current token information for debugging and monitoring
1327    ///
1328    /// Returns detailed information about the current token including:
1329    /// - Expiry time and remaining duration
1330    /// - Token age since acquisition
1331    /// - Expiry status flags
1332    ///
1333    /// # Returns
1334    /// `Some(TokenInfo)` if a token exists, `None` if no token is stored
1335    ///
1336    /// # Errors
1337    /// Returns an error if token information retrieval fails
1338    pub async fn get_token_info(&self) -> Result<Option<TokenInfo>, Error> {
1339        tracing::debug!("Retrieving token information for debugging");
1340
1341        let token_info = self.token_data.lock().await.as_ref().map(TokenInfo::from);
1342
1343        match &token_info {
1344            Some(info) => {
1345                tracing::debug!(
1346                    "Token info retrieved - expires_at: {}, age: {:?}, expires_in: {:?}, is_expired: {}, expires_soon: {}",
1347                    info.expires_at,
1348                    info.age,
1349                    info.expires_in,
1350                    info.is_expired,
1351                    info.expires_soon
1352                );
1353            }
1354            None => {
1355                tracing::debug!("No token information available - no token stored");
1356            }
1357        }
1358
1359        Ok(token_info)
1360    }
1361
1362    /// Clears the stored token (useful for testing scenarios)
1363    ///
1364    /// This method removes the current token from storage, forcing the next
1365    /// `get_token()` call to obtain a fresh token.
1366    ///
1367    /// # Errors
1368    /// Returns an error if token clearing fails
1369    pub async fn clear_token(&self) -> Result<(), Error> {
1370        tracing::debug!("Clearing stored token from memory and disk");
1371
1372        let had_token = self.clear_token_from_memory().await;
1373        self.clear_token_from_disk_if_enabled().await;
1374        Self::log_token_clear_result(had_token);
1375
1376        Ok(())
1377    }
1378
1379    /// Clears the token from memory and returns whether a token was present
1380    async fn clear_token_from_memory(&self) -> bool {
1381        let mut token_guard = self.token_data.lock().await;
1382        let had_token = token_guard.is_some();
1383        *token_guard = None;
1384        drop(token_guard);
1385        had_token
1386    }
1387
1388    /// Clears the token from disk if persistence is enabled
1389    async fn clear_token_from_disk_if_enabled(&self) {
1390        if Self::should_persist_tokens() {
1391            if let Err(e) = self.remove_token_from_disk().await {
1392                tracing::warn!("Failed to remove token from disk: {e}");
1393            }
1394        }
1395    }
1396
1397    /// Logs the result of the token clearing operation
1398    fn log_token_clear_result(had_token: bool) {
1399        if had_token {
1400            tracing::info!("Token successfully cleared from memory and disk - next get_token() will obtain fresh token");
1401        } else {
1402            tracing::debug!("No token was stored to clear");
1403        }
1404    }
1405
1406    /// Forces a token refresh regardless of current token status
1407    ///
1408    /// This method bypasses the normal proactive refresh logic and immediately
1409    /// attempts to refresh the current token. If no token exists or refresh fails,
1410    /// it falls back to obtaining a new token.
1411    ///
1412    /// # Thread Safety
1413    /// This method is fully thread-safe and uses the same semaphore-based synchronization
1414    /// as other token operations to prevent race conditions.
1415    ///
1416    /// # Errors
1417    /// Returns an error if both refresh and obtain operations fail
1418    pub async fn force_refresh(&self) -> Result<String, Error> {
1419        tracing::info!("Forcing token refresh - bypassing normal proactive refresh logic");
1420
1421        let _permit = self.acquire_token_semaphore().await?;
1422        self.log_token_status_for_refresh().await;
1423        self.execute_forced_refresh().await
1424    }
1425
1426    /// Logs the current token status for forced refresh operation
1427    async fn log_token_status_for_refresh(&self) {
1428        let has_token = {
1429            let token_guard = self.token_data.lock().await;
1430            token_guard.is_some()
1431        };
1432
1433        if has_token {
1434            tracing::debug!("Existing token found, attempting forced refresh");
1435        } else {
1436            tracing::debug!("No existing token found, will obtain new token");
1437        }
1438    }
1439
1440    /// Executes the forced refresh operation
1441    async fn execute_forced_refresh(&self) -> Result<String, Error> {
1442        match self.refresh_token_internal().await {
1443            Ok(token) => {
1444                tracing::info!("Forced token refresh completed successfully");
1445                Ok(token)
1446            }
1447            Err(e) => {
1448                tracing::error!("Forced token refresh failed: {e}");
1449                Err(e)
1450            }
1451        }
1452    }
1453
1454    /// Determines if token persistence is enabled based on environment variables
1455    ///
1456    /// Token persistence is enabled when:
1457    /// - `AMP_TESTS=live` (for live API testing)
1458    /// - `AMP_TOKEN_PERSISTENCE=true` is set
1459    /// - NOT in mock test environments (to prevent test pollution)
1460    fn should_persist_tokens() -> bool {
1461        // Use the new environment detection logic
1462        let environment = TokenEnvironment::detect();
1463
1464        // Never persist tokens in mock environments to prevent test pollution
1465        if environment.is_mock() {
1466            tracing::debug!("Token persistence disabled - mock environment detected");
1467            return false;
1468        }
1469
1470        // Check if explicitly enabled
1471        if env::var("AMP_TOKEN_PERSISTENCE").unwrap_or_default() == "true" {
1472            tracing::debug!("Token persistence enabled - AMP_TOKEN_PERSISTENCE=true");
1473            return true;
1474        }
1475
1476        // Use environment-based persistence setting
1477        let should_persist = environment.should_persist_tokens();
1478        tracing::debug!(
1479            "Token persistence setting from environment: {}",
1480            should_persist
1481        );
1482        should_persist
1483    }
1484
1485    /// Loads token data from disk if it exists and is valid
1486    async fn load_token_from_disk(&self) -> Result<Option<TokenData>, Error> {
1487        let token_file = "token.json";
1488
1489        if !self.token_file_exists(token_file).await {
1490            return Ok(None);
1491        }
1492
1493        let content = self.read_token_file(token_file).await?;
1494        self.parse_and_validate_token(token_file, &content).await
1495    }
1496
1497    /// Checks if the token file exists on disk
1498    async fn token_file_exists(&self, token_file: &str) -> bool {
1499        tokio::fs::try_exists(token_file).await.map_or_else(
1500            |_| {
1501                tracing::debug!("Error checking token file existence: {}", token_file);
1502                false
1503            },
1504            |exists| {
1505                if !exists {
1506                    tracing::debug!("Token file does not exist: {}", token_file);
1507                }
1508                exists
1509            },
1510        )
1511    }
1512
1513    /// Reads the token file content from disk
1514    async fn read_token_file(&self, token_file: &str) -> Result<String, Error> {
1515        use tokio::fs;
1516
1517        match fs::read_to_string(token_file).await {
1518            Ok(content) => Ok(content),
1519            Err(e) => {
1520                tracing::warn!("Failed to read token file: {e}");
1521                Err(Error::Token(TokenError::storage(format!(
1522                    "Failed to read token file: {e}"
1523                ))))
1524            }
1525        }
1526    }
1527
1528    /// Parses token content and validates expiration
1529    async fn parse_and_validate_token(
1530        &self,
1531        token_file: &str,
1532        content: &str,
1533    ) -> Result<Option<TokenData>, Error> {
1534        match serde_json::from_str::<TokenData>(content) {
1535            Ok(token_data) => self.handle_parsed_token(token_file, token_data).await,
1536            Err(e) => self.handle_parse_error(token_file, e).await,
1537        }
1538    }
1539
1540    /// Handles successfully parsed token data, checking expiration
1541    async fn handle_parsed_token(
1542        &self,
1543        token_file: &str,
1544        token_data: TokenData,
1545    ) -> Result<Option<TokenData>, Error> {
1546        if token_data.is_expired() {
1547            tracing::info!("Token loaded from disk is expired, removing file");
1548            let _ = tokio::fs::remove_file(token_file).await;
1549            Ok(None)
1550        } else {
1551            tracing::info!("Valid token loaded from disk");
1552            Ok(Some(token_data))
1553        }
1554    }
1555
1556    /// Handles token parsing errors by cleaning up the invalid file
1557    async fn handle_parse_error(
1558        &self,
1559        token_file: &str,
1560        e: serde_json::Error,
1561    ) -> Result<Option<TokenData>, Error> {
1562        tracing::warn!("Failed to parse token file, removing: {e}");
1563        let _ = tokio::fs::remove_file(token_file).await;
1564        Err(Error::Token(TokenError::serialization(format!(
1565            "Failed to parse token file: {e}"
1566        ))))
1567    }
1568
1569    /// Saves token data to disk
1570    async fn save_token_to_disk(&self, token_data: &TokenData) -> Result<(), Error> {
1571        use tokio::fs;
1572
1573        let token_file = "token.json";
1574
1575        match serde_json::to_string_pretty(token_data) {
1576            Ok(json) => match fs::write(token_file, json).await {
1577                Ok(()) => {
1578                    tracing::debug!("Token saved to disk: {}", token_file);
1579                    Ok(())
1580                }
1581                Err(e) => {
1582                    tracing::error!("Failed to write token file: {e}");
1583                    Err(Error::Token(TokenError::storage(format!(
1584                        "Failed to write token file: {e}"
1585                    ))))
1586                }
1587            },
1588            Err(e) => {
1589                tracing::error!("Failed to serialize token data: {e}");
1590                Err(Error::Token(TokenError::serialization(format!(
1591                    "Failed to serialize token data: {e}"
1592                ))))
1593            }
1594        }
1595    }
1596
1597    /// Removes the token file from disk
1598    async fn remove_token_from_disk(&self) -> Result<(), Error> {
1599        use tokio::fs;
1600
1601        let token_file = "token.json";
1602
1603        match fs::remove_file(token_file).await {
1604            Ok(()) => {
1605                tracing::debug!("Token file removed from disk: {}", token_file);
1606                Ok(())
1607            }
1608            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
1609                tracing::debug!("Token file does not exist, nothing to remove");
1610                Ok(())
1611            }
1612            Err(e) => {
1613                tracing::warn!("Failed to remove token file: {e}");
1614                Err(Error::Token(TokenError::storage(format!(
1615                    "Failed to remove token file: {e}"
1616                ))))
1617            }
1618        }
1619    }
1620
1621    /// Forces cleanup of token persistence files (useful for testing)
1622    /// This method removes token files regardless of persistence settings
1623    ///
1624    /// # Errors
1625    /// Returns an error if:
1626    /// - File system permissions prevent deletion of the token file
1627    /// - I/O errors occur during file deletion operations
1628    /// - The token file is locked by another process
1629    pub async fn force_cleanup_token_files() -> Result<(), Error> {
1630        use tokio::fs;
1631
1632        let token_file = "token.json";
1633
1634        match fs::remove_file(token_file).await {
1635            Ok(()) => {
1636                tracing::debug!("Token file forcefully removed: {}", token_file);
1637                Ok(())
1638            }
1639            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
1640                tracing::debug!("No token file to clean up");
1641                Ok(())
1642            }
1643            Err(e) => {
1644                tracing::warn!("Failed to force cleanup token file: {e}");
1645                Err(Error::Token(TokenError::storage(format!(
1646                    "Failed to force cleanup token file: {e}"
1647                ))))
1648            }
1649        }
1650    }
1651
1652    /// Resets the global `TokenManager` singleton (useful for testing)
1653    ///
1654    /// This method clears the global singleton instance, forcing the next
1655    /// call to `get_global_instance()` to create a fresh `TokenManager`.
1656    /// Primarily intended for test scenarios where a clean state is needed.
1657    ///
1658    /// # Errors
1659    /// Returns an error if:
1660    /// - Token clearing operations fail during the reset process
1661    /// - File system errors occur when clearing persistent token data
1662    /// - The global instance is in an invalid state that prevents cleanup
1663    pub async fn reset_global_instance() -> Result<(), Error> {
1664        // Clear any existing token from the current global instance
1665        if let Some(manager) = GLOBAL_TOKEN_MANAGER.get() {
1666            let _ = manager.clear_token().await;
1667        }
1668
1669        // Reset the OnceCell to allow a new instance to be created
1670        // Note: OnceCell doesn't have a reset method, so we can't actually reset it
1671        // The best we can do is clear the token from the existing instance
1672        tracing::debug!("Global TokenManager instance token cleared for testing");
1673        Ok(())
1674    }
1675}
1676
1677#[derive(Debug)]
1678pub struct ApiClient {
1679    client: Client,
1680    base_url: Url,
1681    token_strategy: Box<dyn TokenStrategy>,
1682}
1683
1684#[allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
1685impl ApiClient {
1686    /// Creates a new API client with the base URL from environment variables.
1687    ///
1688    /// Automatically selects the appropriate token strategy based on environment detection:
1689    /// - Mock strategy for mock environments (no persistence, isolated tokens)
1690    /// - Live strategy for live environments (full token management with persistence)
1691    ///
1692    /// # Errors
1693    ///
1694    /// Returns an error if:
1695    /// - The `AMP_API_BASE_URL` environment variable contains an invalid URL
1696    /// - Token strategy initialization fails
1697    ///
1698    /// # Examples
1699    /// ```no_run
1700    /// # use amp_rs::ApiClient;
1701    /// # #[tokio::main]
1702    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1703    /// // Create a new client - automatically detects environment
1704    /// let client = ApiClient::new().await?;
1705    /// 
1706    /// // Client is ready to use
1707    /// let assets = client.get_assets().await?;
1708    /// println!("Found {} assets", assets.len());
1709    /// # Ok(())
1710    /// # }
1711    /// ```
1712    pub async fn new() -> Result<Self, Error> {
1713        let base_url = get_amp_api_base_url()?;
1714        let client = Client::new();
1715
1716        // Automatic strategy selection based on environment
1717        let token_strategy = TokenEnvironment::create_auto_strategy(None).await?;
1718
1719        tracing::info!(
1720            "Created ApiClient with {} strategy for base URL: {}",
1721            token_strategy.strategy_type(),
1722            base_url
1723        );
1724
1725        Ok(Self {
1726            client,
1727            base_url,
1728            token_strategy,
1729        })
1730    }
1731
1732    /// Creates a new API client with the specified base URL.
1733    ///
1734    /// Automatically selects the appropriate token strategy based on environment detection.
1735    ///
1736    /// # Errors
1737    ///
1738    /// Returns an error if token strategy initialization fails.
1739    ///
1740    /// # Examples
1741    /// ```no_run
1742    /// # use amp_rs::ApiClient;
1743    /// # use reqwest::Url;
1744    /// # #[tokio::main]
1745    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1746    /// let base_url = Url::parse("https://amp-test.blockstream.com/api")?;
1747    /// let client = ApiClient::with_base_url(base_url).await?;
1748    /// 
1749    /// // Client is ready to use with the specified URL
1750    /// let assets = client.get_assets().await?;
1751    /// # Ok(())
1752    /// # }
1753    /// ```
1754    pub async fn with_base_url(base_url: Url) -> Result<Self, Error> {
1755        let client = Client::new();
1756
1757        // Automatic strategy selection based on environment
1758        let token_strategy = TokenEnvironment::create_auto_strategy(None).await?;
1759
1760        tracing::info!(
1761            "Created ApiClient with {} strategy for base URL: {}",
1762            token_strategy.strategy_type(),
1763            base_url
1764        );
1765
1766        Ok(Self {
1767            client,
1768            base_url,
1769            token_strategy,
1770        })
1771    }
1772
1773    /// Creates a new API client with a custom token strategy (useful for testing).
1774    ///
1775    /// # Errors
1776    ///
1777    /// Returns an error if the base URL cannot be obtained from environment variables.
1778    pub fn with_token_strategy(token_strategy: Box<dyn TokenStrategy>) -> Result<Self, Error> {
1779        let base_url = get_amp_api_base_url()?;
1780
1781        tracing::info!(
1782            "Created ApiClient with explicit {} strategy for base URL: {}",
1783            token_strategy.strategy_type(),
1784            base_url
1785        );
1786
1787        Ok(Self {
1788            client: Client::new(),
1789            base_url,
1790            token_strategy,
1791        })
1792    }
1793
1794    /// Creates a new API client with a custom token manager (useful for testing).
1795    ///
1796    /// # Errors
1797    ///
1798    /// Returns an error if the base URL cannot be obtained from environment variables.
1799    pub fn with_token_manager(token_manager: Arc<TokenManager>) -> Result<Self, Error> {
1800        let base_url = get_amp_api_base_url()?;
1801        let token_strategy: Box<dyn TokenStrategy> =
1802            Box::new(LiveTokenStrategy::with_token_manager(token_manager));
1803
1804        tracing::info!(
1805            "Created ApiClient with custom token manager for base URL: {}",
1806            base_url
1807        );
1808
1809        Ok(Self {
1810            client: Client::new(),
1811            base_url,
1812            token_strategy,
1813        })
1814    }
1815
1816    /// Creates a new API client for testing with a mock token strategy that always returns a fixed token.
1817    /// This bypasses all token acquisition and management logic and uses complete isolation.
1818    ///
1819    /// # Errors
1820    ///
1821    /// This method is infallible but returns Result for API consistency.
1822    ///
1823    /// # Examples
1824    /// ```
1825    /// # use amp_rs::ApiClient;
1826    /// # use reqwest::Url;
1827    /// # #[tokio::main]
1828    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1829    /// let base_url = Url::parse("http://localhost:8080/api")?;
1830    /// let client = ApiClient::with_mock_token(base_url, "test_token".to_string())?;
1831    /// 
1832    /// // Client will always use "test_token" for authentication
1833    /// let token = client.get_token().await?;
1834    /// assert_eq!(token, "test_token");
1835    /// # Ok(())
1836    /// # }
1837    /// ```
1838    pub fn with_mock_token(base_url: Url, mock_token: String) -> Result<Self, Error> {
1839        let client = Client::new();
1840        let token_strategy: Box<dyn TokenStrategy> = Box::new(MockTokenStrategy::new(mock_token));
1841
1842        tracing::info!(
1843            "Created ApiClient with explicit mock token strategy for base URL: {}",
1844            base_url
1845        );
1846
1847        Ok(Self {
1848            client,
1849            base_url,
1850            token_strategy,
1851        })
1852    }
1853
1854    /// Obtains a new authentication token from the AMP API.
1855    ///
1856    /// **Note**: This method is deprecated in favor of the automatic token management
1857    /// provided by `get_token()`. The `TokenManager` handles token acquisition internally
1858    /// with enhanced retry logic and error handling.
1859    ///
1860    /// # Errors
1861    ///
1862    /// Returns an error if:
1863    /// - The `AMP_USERNAME` or `AMP_PASSWORD` environment variables are not set
1864    /// - The HTTP request fails
1865    /// - The token request is rejected by the server
1866    /// - The response cannot be parsed
1867    #[deprecated(note = "Use get_token() instead - it provides automatic token management")]
1868    pub async fn obtain_amp_token(&self) -> Result<String, Error> {
1869        // Delegate to get_token for backward compatibility
1870        self.get_token().await
1871    }
1872
1873    /// Gets current token information for debugging and monitoring.
1874    ///
1875    /// Returns detailed information about the current token including:
1876    /// - Expiry time and remaining duration
1877    /// - Token age since acquisition
1878    /// - Expiry status flags
1879    ///
1880    /// Note: Mock strategies may return limited or no token information.
1881    ///
1882    /// # Returns
1883    /// `Some(TokenInfo)` if a token exists, `None` if no token is stored or strategy doesn't support info
1884    ///
1885    /// # Errors
1886    /// Returns an error if token information retrieval fails
1887    ///
1888    /// # Examples
1889    /// ```no_run
1890    /// # use amp_rs::ApiClient;
1891    /// # #[tokio::main]
1892    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1893    /// let client = ApiClient::new().await?;
1894    /// 
1895    /// if let Some(token_info) = client.get_token_info().await? {
1896    ///     println!("Token expires at: {}", token_info.expires_at);
1897    ///     println!("Token is expired: {}", token_info.is_expired);
1898    /// } else {
1899    ///     println!("No token stored or mock strategy in use");
1900    /// }
1901    /// # Ok(())
1902    /// # }
1903    /// ```
1904    pub async fn get_token_info(&self) -> Result<Option<TokenInfo>, Error> {
1905        // Only live strategies support detailed token information
1906        if let Some(live_strategy) = self
1907            .token_strategy
1908            .as_any()
1909            .downcast_ref::<LiveTokenStrategy>()
1910        {
1911            live_strategy.get_token_info().await
1912        } else {
1913            // Mock strategies don't provide detailed token information
1914            tracing::debug!(
1915                "Token info not available for {} strategy",
1916                self.token_strategy.strategy_type()
1917            );
1918            Ok(None)
1919        }
1920    }
1921
1922    /// Clears the stored token (useful for testing scenarios).
1923    ///
1924    /// This method removes the current token from storage, forcing the next
1925    /// `get_token()` call to obtain a fresh token.
1926    ///
1927    /// # Errors
1928    /// Returns an error if token clearing fails
1929    ///
1930    /// # Examples
1931    /// ```no_run
1932    /// # use amp_rs::ApiClient;
1933    /// # #[tokio::main]
1934    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1935    /// let client = ApiClient::new().await?;
1936    /// 
1937    /// // Clear any existing token
1938    /// client.clear_token().await?;
1939    /// 
1940    /// // Next get_token() call will obtain a fresh token
1941    /// let token = client.get_token().await?;
1942    /// # Ok(())
1943    /// # }
1944    /// ```
1945    pub async fn clear_token(&self) -> Result<(), Error> {
1946        self.token_strategy.clear_token().await
1947    }
1948
1949    /// Forces a token refresh regardless of current token status.
1950    ///
1951    /// This method bypasses the normal proactive refresh logic and immediately
1952    /// attempts to refresh the current token. If no token exists or refresh fails,
1953    /// it falls back to obtaining a new token.
1954    ///
1955    /// # Errors
1956    /// Returns an error if both refresh and obtain operations fail
1957    pub async fn force_refresh(&self) -> Result<String, Error> {
1958        // Clear current token and get a fresh one
1959        self.token_strategy.clear_token().await?;
1960        self.token_strategy.get_token().await
1961    }
1962
1963    /// Resets the global `TokenManager` singleton (useful for testing).
1964    ///
1965    /// This method clears the token from the global `TokenManager` instance.
1966    /// Primarily intended for test scenarios where a clean token state is needed.
1967    ///
1968    /// # Errors
1969    /// Returns an error if the reset operation fails
1970    pub async fn reset_global_token_manager() -> Result<(), Error> {
1971        TokenManager::reset_global_instance().await
1972    }
1973
1974    /// Gets a valid authentication token with automatic token management.
1975    ///
1976    /// This method uses the integrated `TokenManager` to handle:
1977    /// - Proactive token refresh (5 minutes before expiry)
1978    /// - Automatic fallback from refresh to obtain on failure
1979    /// - Retry logic with exponential backoff
1980    /// - Thread-safe token storage
1981    ///
1982    /// # Errors
1983    ///
1984    /// Returns an error if token acquisition or refresh fails after all retries.
1985    ///
1986    /// # Examples
1987    /// ```no_run
1988    /// # use amp_rs::ApiClient;
1989    /// # #[tokio::main]
1990    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1991    /// let client = ApiClient::new().await?;
1992    /// 
1993    /// // Get a valid token - automatically handles refresh if needed
1994    /// let token = client.get_token().await?;
1995    /// println!("Got token: {}", &token[..10]); // Print first 10 chars
1996    /// # Ok(())
1997    /// # }
1998    /// ```
1999    pub async fn get_token(&self) -> Result<String, Error> {
2000        self.token_strategy.get_token().await
2001    }
2002
2003    /// Returns the type of token strategy currently in use
2004    ///
2005    /// This is useful for debugging and testing to verify the correct strategy is selected.
2006    ///
2007    /// # Returns
2008    /// A string indicating the strategy type: "mock" or "live"
2009    #[must_use]
2010    pub fn get_strategy_type(&self) -> &'static str {
2011        self.token_strategy.strategy_type()
2012    }
2013
2014    /// Returns whether the current strategy persists tokens
2015    ///
2016    /// This is useful for understanding the token management behavior.
2017    ///
2018    /// # Returns
2019    /// `true` if tokens are persisted to disk, `false` for in-memory only
2020    #[must_use]
2021    pub fn should_persist_tokens(&self) -> bool {
2022        self.token_strategy.should_persist()
2023    }
2024
2025    /// Force cleanup of token files (for test cleanup)
2026    ///
2027    /// This is a static method that can be used to cleanup token files
2028    /// without needing an `ApiClient` instance. Useful for test teardown.
2029    ///
2030    /// # Errors
2031    /// Returns an error if token file cleanup fails
2032    pub async fn force_cleanup_token_files() -> Result<(), Error> {
2033        // Only cleanup if we're not in a live test environment
2034        let environment = TokenEnvironment::detect();
2035        if !environment.is_live() || environment.is_mock() {
2036            TokenManager::force_cleanup_token_files().await?;
2037            tracing::debug!("Token files cleaned up for non-live environment");
2038        } else {
2039            tracing::debug!("Skipping token file cleanup in live environment");
2040        }
2041        Ok(())
2042    }
2043
2044    async fn request_raw(
2045        &self,
2046        method: Method,
2047        path: &[&str],
2048        body: Option<impl serde::Serialize>,
2049    ) -> Result<reqwest::Response, Error> {
2050        let token = self.get_token().await?;
2051        let mut url = self.base_url.clone();
2052        url.path_segments_mut().unwrap().extend(path);
2053
2054        let mut request_builder = self
2055            .client
2056            .request(method, url)
2057            .header(AUTHORIZATION, format!("token {token}"));
2058
2059        if let Some(body) = body {
2060            request_builder = request_builder.json(&body);
2061        }
2062
2063        let response = request_builder.send().await?;
2064
2065        if !response.status().is_success() {
2066            let status = response.status();
2067            let error_text = response
2068                .text()
2069                .await
2070                .unwrap_or_else(|_| "Unknown error".to_string());
2071            return Err(Error::RequestFailed(format!(
2072                "Request to {path:?} failed with status {status}: {error_text}"
2073            )));
2074        }
2075
2076        Ok(response)
2077    }
2078
2079    async fn request_json<T: DeserializeOwned>(
2080        &self,
2081        method: Method,
2082        path: &[&str],
2083        body: Option<impl serde::Serialize>,
2084    ) -> Result<T, Error> {
2085        let response = self.request_raw(method, path, body).await?;
2086        response
2087            .json()
2088            .await
2089            .map_err(|e| Error::ResponseParsingFailed(e.to_string()))
2090    }
2091
2092    async fn request_empty(
2093        &self,
2094        method: Method,
2095        path: &[&str],
2096        body: Option<impl serde::Serialize>,
2097    ) -> Result<(), Error> {
2098        self.request_raw(method, path, body).await?;
2099        Ok(())
2100    }
2101
2102    /// Gets the API changelog.
2103    ///
2104    /// # Errors
2105    ///
2106    /// Returns an error if:
2107    /// - Authentication fails
2108    /// - The HTTP request fails
2109    /// - The server returns an error status
2110    /// - The response cannot be parsed as JSON
2111    pub async fn get_changelog(&self) -> Result<serde_json::Value, Error> {
2112        self.request_json(Method::GET, &["changelog"], None::<&()>)
2113            .await
2114    }
2115
2116    /// Changes the user's password.
2117    ///
2118    /// # Errors
2119    ///
2120    /// Returns an error if:
2121    /// - Authentication fails
2122    /// - The HTTP request fails
2123    /// - The server rejects the password change
2124    /// - The response cannot be parsed
2125    pub async fn user_change_password(
2126        &self,
2127        password: Secret<String>,
2128    ) -> Result<ChangePasswordResponse, Error> {
2129        let request = ChangePasswordRequest {
2130            password: Secret::new(Password(password.expose_secret().clone())),
2131        };
2132        self.request_json(Method::POST, &["user", "change_password"], Some(request))
2133            .await
2134    }
2135
2136    /// Gets a list of all assets.
2137    ///
2138    /// # Errors
2139    ///
2140    /// Returns an error if:
2141    /// - Authentication fails
2142    /// - The HTTP request fails
2143    /// - The server returns an error status
2144    /// - The response cannot be parsed
2145    ///
2146    /// # Examples
2147    /// ```no_run
2148    /// # use amp_rs::ApiClient;
2149    /// # #[tokio::main]
2150    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2151    /// let client = ApiClient::new().await?;
2152    /// 
2153    /// let assets = client.get_assets().await?;
2154    /// for asset in assets {
2155    ///     println!("Asset: {} ({})", asset.name, asset.ticker.unwrap_or_default());
2156    /// }
2157    /// # Ok(())
2158    /// # }
2159    /// ```
2160    pub async fn get_assets(&self) -> Result<Vec<Asset>, Error> {
2161        self.request_json(Method::GET, &["assets"], None::<&()>)
2162            .await
2163    }
2164
2165    /// Gets a specific asset by UUID.
2166    ///
2167    /// # Errors
2168    ///
2169    /// Returns an error if:
2170    /// - Authentication fails
2171    /// - The HTTP request fails
2172    /// - The asset does not exist
2173    /// - The response cannot be parsed
2174    ///
2175    /// # Examples
2176    /// ```no_run
2177    /// # use amp_rs::ApiClient;
2178    /// # #[tokio::main]
2179    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2180    /// let client = ApiClient::new().await?;
2181    /// 
2182    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
2183    /// let asset = client.get_asset(asset_uuid).await?;
2184    /// 
2185    /// println!("Asset: {} ({})", asset.name, asset.ticker.unwrap_or_default());
2186    /// println!("Registered: {}, Locked: {}", asset.is_registered, asset.is_locked);
2187    /// # Ok(())
2188    /// # }
2189    /// ```
2190    pub async fn get_asset(&self, asset_uuid: &str) -> Result<Asset, Error> {
2191        self.request_json(Method::GET, &["assets", asset_uuid], None::<&()>)
2192            .await
2193    }
2194
2195    /// Issues a new asset.
2196    ///
2197    /// # Errors
2198    ///
2199    /// Returns an error if:
2200    /// - Authentication fails
2201    /// - The HTTP request fails
2202    /// - The issuance request is invalid
2203    /// - The response cannot be parsed
2204    ///
2205    /// # Examples
2206    /// ```no_run
2207    /// # use amp_rs::{ApiClient, model::IssuanceRequest};
2208    /// # #[tokio::main]
2209    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2210    /// let client = ApiClient::new().await?;
2211    /// 
2212    /// let issuance_request = IssuanceRequest {
2213    ///     name: "My Token".to_string(),
2214    ///     amount: 1000000,
2215    ///     destination_address: "vjU2i2EM2viGEzSywpStMPkTX9U9QSDsLSN63kJJYVpxKJZuxaph8v5r5Jf11aqnfBVdjSbrvcJ2pw26".to_string(),
2216    ///     domain: "example.com".to_string(),
2217    ///     ticker: "MYTKN".to_string(),
2218    ///     pubkey: "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798".to_string(),
2219    ///     precision: Some(8),
2220    ///     is_confidential: Some(true),
2221    ///     is_reissuable: Some(false),
2222    ///     reissuance_amount: None,
2223    ///     reissuance_address: None,
2224    ///     transfer_restricted: Some(false),
2225    /// };
2226    /// 
2227    /// let response = client.issue_asset(&issuance_request).await?;
2228    /// println!("Issued asset with UUID: {}", response.asset_uuid);
2229    /// # Ok(())
2230    /// # }
2231    /// ```
2232    pub async fn issue_asset(
2233        &self,
2234        issuance_request: &IssuanceRequest,
2235    ) -> Result<IssuanceResponse, Error> {
2236        self.request_json(Method::POST, &["assets", "issue"], Some(issuance_request))
2237            .await
2238    }
2239
2240    /// Edits an existing asset.
2241    ///
2242    /// # Errors
2243    ///
2244    /// Returns an error if:
2245    /// - Authentication fails
2246    /// - The HTTP request fails
2247    /// - The asset does not exist
2248    /// - The edit request is invalid
2249    /// - The response cannot be parsed
2250    pub async fn edit_asset(
2251        &self,
2252        asset_uuid: &str,
2253        edit_asset_request: &EditAssetRequest,
2254    ) -> Result<Asset, Error> {
2255        self.request_json(
2256            Method::PUT,
2257            &["assets", asset_uuid, "edit"],
2258            Some(edit_asset_request),
2259        )
2260        .await
2261    }
2262
2263    /// # Errors
2264    /// Returns an error if:
2265    /// - The asset does not exist or cannot be found
2266    /// - Authentication fails or token is invalid
2267    /// - Network connectivity issues occur
2268    /// - The server returns an error status
2269    pub async fn delete_asset(&self, asset_uuid: &str) -> Result<(), Error> {
2270        self.request_empty(
2271            Method::DELETE,
2272            &["assets", asset_uuid, "delete"],
2273            None::<&()>,
2274        )
2275        .await
2276    }
2277
2278    /// # Errors
2279    /// Returns an error if:
2280    /// - The transaction ID is invalid or not found
2281    /// - Authentication fails or token is invalid
2282    /// - Network connectivity issues occur
2283    /// - The server returns an error status
2284    /// - The response cannot be parsed
2285    pub async fn get_broadcast_status(&self, txid: &str) -> Result<BroadcastResponse, Error> {
2286        self.request_json(Method::GET, &["tx", "broadcast", txid], None::<&()>)
2287            .await
2288    }
2289
2290    /// # Errors
2291    /// Returns an error if:
2292    /// - The transaction hex is invalid or malformed
2293    /// - The transaction is rejected by the network
2294    /// - Authentication fails or token is invalid
2295    /// - Network connectivity issues occur
2296    /// - The server returns an error status
2297    /// - The response cannot be parsed
2298    pub async fn broadcast_transaction(&self, tx_hex: &str) -> Result<BroadcastResponse, Error> {
2299        self.request_json(Method::POST, &["tx", "broadcast"], Some(tx_hex))
2300            .await
2301    }
2302
2303    /// # Errors
2304    /// Returns an error if:
2305    /// - The asset UUID is invalid or not found
2306    /// - The asset is already registered
2307    /// - Authentication fails or token is invalid
2308    /// - Network connectivity issues occur
2309    /// - The server returns an error status
2310    /// - The response cannot be parsed
2311    pub async fn register_asset(&self, asset_uuid: &str) -> Result<Asset, Error> {
2312        self.request_json(
2313            Method::GET,
2314            &["assets", asset_uuid, "register"],
2315            None::<&()>,
2316        )
2317        .await
2318    }
2319
2320    /// # Errors
2321    /// Returns an error if:
2322    /// - The asset UUID is invalid or not found
2323    /// - The user lacks authorization to register the asset
2324    /// - The asset is already registered
2325    /// - Authentication fails or token is invalid
2326    /// - Network connectivity issues occur
2327    /// - The server returns an error status
2328    /// - The response cannot be parsed
2329    pub async fn register_asset_authorized(&self, asset_uuid: &str) -> Result<Asset, Error> {
2330        self.request_json(
2331            Method::GET,
2332            &["assets", asset_uuid, "register-authorized"],
2333            None::<&()>,
2334        )
2335        .await
2336    }
2337
2338    /// # Errors
2339    /// Returns an error if:
2340    /// - The asset UUID is invalid or not found
2341    /// - The asset is already locked
2342    /// - The user lacks permission to lock the asset
2343    /// - Authentication fails or token is invalid
2344    /// - Network connectivity issues occur
2345    /// - The server returns an error status
2346    /// - The response cannot be parsed
2347    pub async fn lock_asset(&self, asset_uuid: &str) -> Result<Asset, Error> {
2348        self.request_json(Method::PUT, &["assets", asset_uuid, "lock"], None::<&()>)
2349            .await
2350    }
2351
2352    /// # Errors
2353    /// Returns an error if:
2354    /// - The asset UUID is invalid or not found
2355    /// - The asset is not currently locked
2356    /// - The user lacks permission to unlock the asset
2357    /// - Authentication fails or token is invalid
2358    /// - Network connectivity issues occur
2359    /// - The server returns an error status
2360    /// - The response cannot be parsed
2361    pub async fn unlock_asset(&self, asset_uuid: &str) -> Result<Asset, Error> {
2362        self.request_json(Method::PUT, &["assets", asset_uuid, "unlock"], None::<&()>)
2363            .await
2364    }
2365
2366    /// # Errors
2367    /// Returns an error if:
2368    /// - The asset UUID is invalid or not found
2369    /// - The activity parameters are invalid
2370    /// - Authentication fails or token is invalid
2371    /// - Network connectivity issues occur
2372    /// - The server returns an error status
2373    /// - The response cannot be parsed
2374    pub async fn get_asset_activities(
2375        &self,
2376        asset_uuid: &str,
2377        params: &AssetActivityParams,
2378    ) -> Result<Vec<Activity>, Error> {
2379        self.request_json(
2380            Method::GET,
2381            &["assets", asset_uuid, "activities"],
2382            Some(params),
2383        )
2384        .await
2385    }
2386
2387    /// # Errors
2388    /// Returns an error if:
2389    /// - The asset UUID is invalid or not found
2390    /// - The specified height is invalid or out of range
2391    /// - Authentication fails or token is invalid
2392    /// - Network connectivity issues occur
2393    /// - The server returns an error status
2394    /// - The response cannot be parsed
2395    pub async fn get_asset_ownerships(
2396        &self,
2397        asset_uuid: &str,
2398        height: Option<i64>,
2399    ) -> Result<Vec<Ownership>, Error> {
2400        let mut path = vec!["assets", asset_uuid, "ownerships"];
2401        let height_str;
2402        if let Some(h) = height {
2403            height_str = h.to_string();
2404            path.push(&height_str);
2405        }
2406        self.request_json(Method::GET, &path, None::<&()>).await
2407    }
2408
2409    /// # Errors
2410    /// Returns an error if:
2411    /// - The asset UUID is invalid or not found
2412    /// - Authentication fails or token is invalid
2413    /// - Network connectivity issues occur
2414    /// - The server returns an error status
2415    /// - The response cannot be parsed
2416    pub async fn get_asset_balance(&self, asset_uuid: &str) -> Result<Balance, Error> {
2417        self.request_json(Method::GET, &["assets", asset_uuid, "balance"], None::<&()>)
2418            .await
2419    }
2420
2421    /// # Errors
2422    /// Returns an error if:
2423    /// - The asset UUID is invalid or not found
2424    /// - Authentication fails or token is invalid
2425    /// - Network connectivity issues occur
2426    /// - The server returns an error status
2427    /// - The response cannot be parsed
2428    pub async fn get_asset_summary(&self, asset_uuid: &str) -> Result<AssetSummary, Error> {
2429        self.request_json(Method::GET, &["assets", asset_uuid, "summary"], None::<&()>)
2430            .await
2431    }
2432
2433    /// # Errors
2434    /// Returns an error if:
2435    /// - The asset UUID is invalid or not found
2436    /// - Authentication fails or token is invalid
2437    /// - Network connectivity issues occur
2438    /// - The server returns an error status
2439    /// - The response cannot be parsed
2440    pub async fn get_asset_utxos(&self, asset_uuid: &str) -> Result<Vec<Utxo>, Error> {
2441        self.request_json(Method::GET, &["assets", asset_uuid, "utxos"], None::<&()>)
2442            .await
2443    }
2444
2445    /// Gets the memo for a specific asset.
2446    ///
2447    /// # Arguments
2448    /// * `asset_uuid` - The UUID of the asset to retrieve the memo for
2449    ///
2450    /// # Returns
2451    /// The memo string associated with the asset
2452    ///
2453    /// # Errors
2454    /// Returns an error if:
2455    /// - Authentication fails
2456    /// - The HTTP request fails
2457    /// - The server returns an error status
2458    /// - The asset does not exist
2459    /// - The response cannot be parsed
2460    pub async fn get_asset_memo(&self, asset_uuid: &str) -> Result<String, Error> {
2461        self.request_json(Method::GET, &["assets", asset_uuid, "memo"], None::<&()>)
2462            .await
2463    }
2464
2465    /// Sets a memo for the specified asset.
2466    ///
2467    /// # Arguments
2468    /// * `asset_uuid` - The UUID of the asset to set the memo for
2469    /// * `memo` - The memo string to associate with the asset
2470    ///
2471    /// # Returns
2472    /// Returns `Ok(())` on success.
2473    ///
2474    /// # Errors
2475    /// Returns an error if:
2476    /// - Authentication fails
2477    /// - The HTTP request fails
2478    /// - The server returns an error status
2479    /// - The asset does not exist
2480    /// - The memo cannot be set due to validation errors
2481    ///
2482    /// # Example
2483    /// ```rust
2484    /// # use amp_rs::ApiClient;
2485    /// # async fn example(client: &ApiClient) -> Result<(), Box<dyn std::error::Error>> {
2486    /// client.set_asset_memo("asset-uuid-123", "This is a memo for the asset").await?;
2487    /// # Ok(())
2488    /// # }
2489    /// ```
2490    pub async fn set_asset_memo(&self, asset_uuid: &str, memo: &str) -> Result<(), Error> {
2491        let token = self.get_token().await?;
2492        let mut url = self.base_url.clone();
2493        url.path_segments_mut()
2494            .unwrap()
2495            .extend(&["assets", asset_uuid, "memo", "set"]);
2496
2497        let response = self
2498            .client
2499            .request(Method::POST, url)
2500            .header(AUTHORIZATION, format!("token {token}"))
2501            .header("content-type", "application/json")
2502            .body(format!("\"{}\"", memo.replace('"', "\\\"")))
2503            .send()
2504            .await?;
2505
2506        if !response.status().is_success() {
2507            let status = response.status();
2508            let error_text = response
2509                .text()
2510                .await
2511                .unwrap_or_else(|_| "Unknown error".to_string());
2512            return Err(Error::RequestFailed(format!(
2513                "Request to [\"assets\", \"{asset_uuid}\", \"memo\", \"set\"] failed with status {status}: {error_text}"
2514            )));
2515        }
2516
2517        Ok(())
2518    }
2519
2520    /// Blacklists specific UTXOs for an asset to prevent them from being used in transactions.
2521    ///
2522    /// This method adds the specified UTXOs to the asset's blacklist, preventing them from being
2523    /// used in future transactions. This is typically used for security purposes when UTXOs are
2524    /// suspected to be compromised or need to be temporarily disabled.
2525    ///
2526    /// # Arguments
2527    /// * `asset_uuid` - The UUID of the asset to blacklist UTXOs for
2528    /// * `utxos` - A slice of `Outpoint` structs representing the UTXOs to blacklist
2529    ///
2530    /// # Returns
2531    /// Returns a vector of `Utxo` structs representing the blacklisted UTXOs with their updated status.
2532    ///
2533    /// # Errors
2534    /// Returns an error if:
2535    /// - Authentication fails or insufficient permissions
2536    /// - The asset UUID is invalid or does not exist
2537    /// - One or more UTXOs are invalid or already blacklisted
2538    /// - The HTTP request fails
2539    /// - The server returns an error status
2540    /// - The response cannot be parsed
2541    ///
2542    /// # Examples
2543    /// ```no_run
2544    /// # use amp_rs::{ApiClient, model::Outpoint};
2545    /// # #[tokio::main]
2546    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2547    /// let client = ApiClient::new().await?;
2548    /// 
2549    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
2550    /// let utxos = vec![
2551    ///     Outpoint {
2552    ///         txid: "abc123...".to_string(),
2553    ///         vout: 0,
2554    ///     },
2555    ///     Outpoint {
2556    ///         txid: "def456...".to_string(),
2557    ///         vout: 1,
2558    ///     },
2559    /// ];
2560    /// 
2561    /// let blacklisted_utxos = client.blacklist_asset_utxos(asset_uuid, &utxos).await?;
2562    /// println!("Blacklisted {} UTXOs", blacklisted_utxos.len());
2563    /// # Ok(())
2564    /// # }
2565    /// ```
2566    ///
2567    /// # Related Methods
2568    /// - [`whitelist_asset_utxos`](Self::whitelist_asset_utxos) - Remove UTXOs from blacklist
2569    /// - [`get_asset`](Self::get_asset) - Get asset information including UTXO status
2570    pub async fn blacklist_asset_utxos(
2571        &self,
2572        asset_uuid: &str,
2573        utxos: &[Outpoint],
2574    ) -> Result<Vec<Utxo>, Error> {
2575        self.request_json(
2576            Method::POST,
2577            &["assets", asset_uuid, "utxos", "blacklist"],
2578            Some(utxos),
2579        )
2580        .await
2581    }
2582
2583    /// Removes UTXOs from the asset's blacklist, allowing them to be used in transactions again.
2584    ///
2585    /// This method removes the specified UTXOs from the asset's blacklist, restoring their ability
2586    /// to be used in transactions. This is the reverse operation of blacklisting UTXOs.
2587    ///
2588    /// # Arguments
2589    /// * `asset_uuid` - The UUID of the asset to whitelist UTXOs for
2590    /// * `utxos` - A slice of `Outpoint` structs representing the UTXOs to remove from blacklist
2591    ///
2592    /// # Returns
2593    /// Returns a vector of `Utxo` structs representing the whitelisted UTXOs with their updated status.
2594    ///
2595    /// # Errors
2596    /// Returns an error if:
2597    /// - Authentication fails or insufficient permissions
2598    /// - The asset UUID is invalid or does not exist
2599    /// - One or more UTXOs are invalid or not currently blacklisted
2600    /// - The HTTP request fails
2601    /// - The server returns an error status
2602    /// - The response cannot be parsed
2603    ///
2604    /// # Examples
2605    /// ```no_run
2606    /// # use amp_rs::{ApiClient, model::Outpoint};
2607    /// # #[tokio::main]
2608    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2609    /// let client = ApiClient::new().await?;
2610    /// 
2611    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
2612    /// let utxos = vec![
2613    ///     Outpoint {
2614    ///         txid: "abc123...".to_string(),
2615    ///         vout: 0,
2616    ///     },
2617    /// ];
2618    /// 
2619    /// let whitelisted_utxos = client.whitelist_asset_utxos(asset_uuid, &utxos).await?;
2620    /// println!("Whitelisted {} UTXOs", whitelisted_utxos.len());
2621    /// # Ok(())
2622    /// # }
2623    /// ```
2624    ///
2625    /// # Related Methods
2626    /// - [`blacklist_asset_utxos`](Self::blacklist_asset_utxos) - Add UTXOs to blacklist
2627    /// - [`get_asset`](Self::get_asset) - Get asset information including UTXO status
2628    pub async fn whitelist_asset_utxos(
2629        &self,
2630        asset_uuid: &str,
2631        utxos: &[Outpoint],
2632    ) -> Result<Vec<Utxo>, Error> {
2633        self.request_json(
2634            Method::POST,
2635            &["assets", asset_uuid, "utxos", "whitelist"],
2636            Some(utxos),
2637        )
2638        .await
2639    }
2640
2641    /// Gets the treasury addresses for a specific asset
2642    ///
2643    /// # Arguments
2644    /// * `asset_uuid` - The UUID of the asset to get treasury addresses for
2645    ///
2646    /// # Returns
2647    /// A vector of treasury addresses as strings
2648    ///
2649    /// # Errors
2650    /// Returns an error if:
2651    /// - The asset does not exist
2652    /// - The request fails
2653    /// - The response cannot be parsed
2654    pub async fn get_asset_treasury_addresses(
2655        &self,
2656        asset_uuid: &str,
2657    ) -> Result<Vec<String>, Error> {
2658        self.request_json(
2659            Method::GET,
2660            &["assets", asset_uuid, "treasury-addresses"],
2661            None::<&()>,
2662        )
2663        .await
2664    }
2665
2666    /// Adds treasury addresses to a specific asset
2667    ///
2668    /// # Arguments
2669    /// * `asset_uuid` - The UUID of the asset to add treasury addresses to
2670    /// * `addresses` - A slice of address strings to add as treasury addresses
2671    ///
2672    /// # Returns
2673    /// Returns `Ok(())` on success
2674    ///
2675    /// # Errors
2676    /// Returns an error if:
2677    /// - The asset does not exist
2678    /// - The addresses are invalid
2679    /// - The request fails
2680    /// - Insufficient permissions
2681    pub async fn add_asset_treasury_addresses(
2682        &self,
2683        asset_uuid: &str,
2684        addresses: &[String],
2685    ) -> Result<(), Error> {
2686        self.request_empty(
2687            Method::POST,
2688            &["assets", asset_uuid, "treasury-addresses", "add"],
2689            Some(addresses),
2690        )
2691        .await
2692    }
2693
2694    /// Removes treasury addresses from a specific asset.
2695    ///
2696    /// This method removes the specified addresses from the asset's treasury address list.
2697    /// Treasury addresses are special addresses that can be used for asset management operations
2698    /// such as reissuance and burning.
2699    ///
2700    /// # Arguments
2701    /// * `asset_uuid` - The UUID of the asset to remove treasury addresses from
2702    /// * `addresses` - A slice of address strings to remove from the treasury addresses
2703    ///
2704    /// # Returns
2705    /// Returns `Ok(())` on successful removal.
2706    ///
2707    /// # Errors
2708    /// Returns an error if:
2709    /// - Authentication fails or insufficient permissions
2710    /// - The asset UUID is invalid or does not exist
2711    /// - One or more addresses are invalid or not currently treasury addresses
2712    /// - The HTTP request fails
2713    /// - The server returns an error status
2714    /// - Attempting to remove the last treasury address (if not allowed)
2715    ///
2716    /// # Examples
2717    /// ```no_run
2718    /// # use amp_rs::ApiClient;
2719    /// # #[tokio::main]
2720    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2721    /// let client = ApiClient::new().await?;
2722    /// 
2723    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
2724    /// let addresses = vec![
2725    ///     "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
2726    ///     "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4".to_string(),
2727    /// ];
2728    /// 
2729    /// client.delete_asset_treasury_addresses(asset_uuid, &addresses).await?;
2730    /// println!("Removed {} treasury addresses", addresses.len());
2731    /// # Ok(())
2732    /// # }
2733    /// ```
2734    ///
2735    /// # Related Methods
2736    /// - [`add_asset_treasury_addresses`](Self::add_asset_treasury_addresses) - Add treasury addresses
2737    /// - [`get_asset_treasury_addresses`](Self::get_asset_treasury_addresses) - Get current treasury addresses
2738    /// - [`reissue_asset`](Self::reissue_asset) - Reissue assets using treasury addresses
2739    pub async fn delete_asset_treasury_addresses(
2740        &self,
2741        asset_uuid: &str,
2742        addresses: &[String],
2743    ) -> Result<(), Error> {
2744        self.request_empty(
2745            Method::DELETE,
2746            &["assets", asset_uuid, "treasury-addresses", "delete"],
2747            Some(addresses),
2748        )
2749        .await
2750    }
2751
2752    /// Gets a list of all registered users.
2753    ///
2754    /// # Errors
2755    /// Returns an error if:
2756    /// - Authentication fails
2757    /// - The HTTP request fails
2758    /// - The server returns an error status
2759    /// - The response cannot be parsed
2760    ///
2761    /// # Examples
2762    /// ```no_run
2763    /// # use amp_rs::ApiClient;
2764    /// # #[tokio::main]
2765    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2766    /// let client = ApiClient::new().await?;
2767    /// 
2768    /// let users = client.get_registered_users().await?;
2769    /// for user in users {
2770    ///     println!("User: {} (ID: {})", user.name, user.id);
2771    /// }
2772    /// # Ok(())
2773    /// # }
2774    /// ```
2775    pub async fn get_registered_users(
2776        &self,
2777    ) -> Result<Vec<crate::model::RegisteredUserResponse>, Error> {
2778        self.request_json(Method::GET, &["registered_users"], None::<&()>)
2779            .await
2780    }
2781
2782    /// Gets a specific registered user by ID.
2783    ///
2784    /// # Arguments
2785    /// * `user_id` - The ID of the registered user to retrieve
2786    ///
2787    /// # Errors
2788    /// Returns an error if:
2789    /// - Authentication fails
2790    /// - The HTTP request fails
2791    /// - The user ID does not exist
2792    /// - The response cannot be parsed
2793    ///
2794    /// # Examples
2795    /// ```no_run
2796    /// # use amp_rs::ApiClient;
2797    /// # #[tokio::main]
2798    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2799    /// let client = ApiClient::new().await?;
2800    /// 
2801    /// let user = client.get_registered_user(1).await?;
2802    /// println!("User: {} (ID: {})", user.name, user.id);
2803    /// # Ok(())
2804    /// # }
2805    /// ```
2806    pub async fn get_registered_user(
2807        &self,
2808        user_id: i64,
2809    ) -> Result<crate::model::RegisteredUserResponse, Error> {
2810        self.request_json(
2811            Method::GET,
2812            &["registered_users", &user_id.to_string()],
2813            None::<&()>,
2814        )
2815        .await
2816    }
2817
2818    /// Creates a new registered user in the AMP system.
2819    ///
2820    /// This method creates a new registered user with the provided information. Registered users
2821    /// can be associated with GAIDs, assigned to categories, and receive asset assignments.
2822    ///
2823    /// # Arguments
2824    /// * `new_user` - A `RegisteredUserAdd` struct containing the user information to create
2825    ///
2826    /// # Returns
2827    /// Returns a `RegisteredUserResponse` containing the created user's information including
2828    /// the assigned user ID.
2829    ///
2830    /// # Errors
2831    /// Returns an error if:
2832    /// - Authentication fails or insufficient permissions
2833    /// - The user data is invalid (e.g., missing required fields, invalid email format)
2834    /// - A user with the same identifier already exists
2835    /// - The HTTP request fails
2836    /// - The server returns an error status
2837    /// - The response cannot be parsed
2838    ///
2839    /// # Examples
2840    /// ```no_run
2841    /// # use amp_rs::{ApiClient, model::RegisteredUserAdd};
2842    /// # #[tokio::main]
2843    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2844    /// let client = ApiClient::new().await?;
2845    /// 
2846    /// let new_user = RegisteredUserAdd {
2847    ///     name: "John Doe".to_string(),
2848    ///     gaid: Some("GAbYScu6jkWUND2jo3L4KJxyvo55d".to_string()),
2849    ///     is_company: false,
2850    /// };
2851    /// 
2852    /// let created_user = client.add_registered_user(&new_user).await?;
2853    /// println!("Created user: {} with ID {}", created_user.name, created_user.id);
2854    /// # Ok(())
2855    /// # }
2856    /// ```
2857    ///
2858    /// # Related Methods
2859    /// - [`get_registered_users`](Self::get_registered_users) - List all registered users
2860    /// - [`edit_registered_user`](Self::edit_registered_user) - Update user information
2861    /// - [`delete_registered_user`](Self::delete_registered_user) - Remove a user
2862    pub async fn add_registered_user(
2863        &self,
2864        new_user: &crate::model::RegisteredUserAdd,
2865    ) -> Result<crate::model::RegisteredUserResponse, Error> {
2866        self.request_json(Method::POST, &["registered_users", "add"], Some(new_user))
2867            .await
2868    }
2869
2870    /// Removes a registered user from the AMP system.
2871    ///
2872    /// This method permanently deletes a registered user and all associated data. This operation
2873    /// cannot be undone. Any GAIDs associated with the user will be disassociated, and any
2874    /// pending assignments may be affected.
2875    ///
2876    /// # Arguments
2877    /// * `user_id` - The ID of the registered user to delete
2878    ///
2879    /// # Returns
2880    /// Returns `Ok(())` on successful deletion.
2881    ///
2882    /// # Errors
2883    /// Returns an error if:
2884    /// - Authentication fails or insufficient permissions
2885    /// - The user ID is invalid or does not exist
2886    /// - The user has active assignments that prevent deletion
2887    /// - The HTTP request fails
2888    /// - The server returns an error status
2889    ///
2890    /// # Examples
2891    /// ```no_run
2892    /// # use amp_rs::ApiClient;
2893    /// # #[tokio::main]
2894    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2895    /// let client = ApiClient::new().await?;
2896    /// 
2897    /// let user_id = 123;
2898    /// client.delete_registered_user(user_id).await?;
2899    /// println!("Successfully deleted user with ID {}", user_id);
2900    /// # Ok(())
2901    /// # }
2902    /// ```
2903    ///
2904    /// # Related Methods
2905    /// - [`get_registered_user`](Self::get_registered_user) - Get user information before deletion
2906    /// - [`add_registered_user`](Self::add_registered_user) - Create a new user
2907    /// - [`get_registered_user_summary`](Self::get_registered_user_summary) - Check user's assignments
2908    pub async fn delete_registered_user(&self, user_id: i64) -> Result<(), Error> {
2909        self.request_empty(
2910            Method::DELETE,
2911            &["registered_users", &user_id.to_string(), "delete"],
2912            None::<&()>,
2913        )
2914        .await
2915    }
2916
2917    /// Updates registered user information.
2918    ///
2919    /// This method allows you to modify the information of an existing registered user.
2920    /// Only the fields provided in the edit data will be updated; other fields remain unchanged.
2921    ///
2922    /// # Arguments
2923    /// * `registered_user_id` - The ID of the registered user to update
2924    /// * `edit_data` - A `RegisteredUserEdit` struct containing the fields to update
2925    ///
2926    /// # Returns
2927    /// Returns a `RegisteredUserResponse` containing the updated user information.
2928    ///
2929    /// # Errors
2930    /// Returns an error if:
2931    /// - Authentication fails or insufficient permissions
2932    /// - The user ID is invalid or does not exist
2933    /// - The edit data contains invalid values (e.g., invalid email format)
2934    /// - The HTTP request fails
2935    /// - The server returns an error status
2936    /// - The response cannot be parsed
2937    ///
2938    /// # Examples
2939    /// ```no_run
2940    /// # use amp_rs::{ApiClient, model::RegisteredUserEdit};
2941    /// # #[tokio::main]
2942    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
2943    /// let client = ApiClient::new().await?;
2944    /// 
2945    /// let user_id = 123;
2946    /// let edit_data = RegisteredUserEdit {
2947    ///     name: Some("Jane Doe".to_string()),
2948    /// };
2949    /// 
2950    /// let updated_user = client.edit_registered_user(user_id, &edit_data).await?;
2951    /// println!("Updated user: {}", updated_user.name);
2952    /// # Ok(())
2953    /// # }
2954    /// ```
2955    ///
2956    /// # Related Methods
2957    /// - [`get_registered_user`](Self::get_registered_user) - Get current user information
2958    /// - [`add_registered_user`](Self::add_registered_user) - Create a new user
2959    /// - [`delete_registered_user`](Self::delete_registered_user) - Remove a user
2960    pub async fn edit_registered_user(
2961        &self,
2962        registered_user_id: i64,
2963        edit_data: &crate::model::RegisteredUserEdit,
2964    ) -> Result<crate::model::RegisteredUserResponse, Error> {
2965        self.request_json(
2966            Method::PUT,
2967            &["registered_users", &registered_user_id.to_string(), "edit"],
2968            Some(edit_data),
2969        )
2970        .await
2971    }
2972
2973    /// Gets comprehensive summary information for a registered user including assets and distributions.
2974    ///
2975    /// This method retrieves detailed summary information about a registered user, including
2976    /// their basic information, associated assets, assignment history, and distribution records.
2977    /// This provides a complete overview of the user's activity and holdings in the system.
2978    ///
2979    /// # Arguments
2980    /// * `registered_user_id` - The ID of the registered user to get summary for
2981    ///
2982    /// # Returns
2983    /// Returns a `RegisteredUserSummary` containing:
2984    /// - Basic user information (name, email, etc.)
2985    /// - List of associated GAIDs
2986    /// - Asset assignments and their status
2987    /// - Distribution history
2988    /// - Balance information
2989    /// - Activity timestamps
2990    ///
2991    /// # Errors
2992    /// Returns an error if:
2993    /// - Authentication fails or insufficient permissions
2994    /// - The user ID is invalid or does not exist
2995    /// - The HTTP request fails
2996    /// - The server returns an error status
2997    /// - The response cannot be parsed
2998    ///
2999    /// # Examples
3000    /// ```no_run
3001    /// # use amp_rs::ApiClient;
3002    /// # #[tokio::main]
3003    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3004    /// let client = ApiClient::new().await?;
3005    /// 
3006    /// let user_id = 123;
3007    /// let summary = client.get_registered_user_summary(user_id).await?;
3008    /// 
3009    /// println!("Asset UUID: {}", summary.asset_uuid);
3010    /// println!("Asset ID: {}", summary.asset_id);
3011    /// println!("Asset assignments: {}", summary.assignments.len());
3012    /// println!("Distributions received: {}", summary.distributions.len());
3013    /// # Ok(())
3014    /// # }
3015    /// ```
3016    ///
3017    /// # Related Methods
3018    /// - [`get_registered_user`](Self::get_registered_user) - Get basic user information
3019    /// - [`get_registered_user_gaids`](Self::get_registered_user_gaids) - Get only GAIDs
3020    /// - [`get_asset_assignments`](Self::get_asset_assignments) - Get assignments for specific asset
3021    pub async fn get_registered_user_summary(
3022        &self,
3023        registered_user_id: i64,
3024    ) -> Result<crate::model::RegisteredUserSummary, Error> {
3025        self.request_json(
3026            Method::GET,
3027            &[
3028                "registered_users",
3029                &registered_user_id.to_string(),
3030                "summary",
3031            ],
3032            None::<&()>,
3033        )
3034        .await
3035    }
3036
3037    /// Gets all GAIDs (Green Address IDs) associated with a registered user.
3038    ///
3039    /// This method retrieves a list of all GAIDs that are currently associated with the specified
3040    /// registered user. GAIDs are unique identifiers that can be used to receive assets and
3041    /// track ownership.
3042    ///
3043    /// # Arguments
3044    /// * `registered_user_id` - The ID of the registered user to get GAIDs for
3045    ///
3046    /// # Returns
3047    /// Returns a vector of GAID strings associated with the user.
3048    ///
3049    /// # Errors
3050    /// Returns an error if:
3051    /// - Authentication fails or insufficient permissions
3052    /// - The user ID is invalid or does not exist
3053    /// - The HTTP request fails
3054    /// - The server returns an error status
3055    /// - The response cannot be parsed
3056    ///
3057    /// # Examples
3058    /// ```no_run
3059    /// # use amp_rs::ApiClient;
3060    /// # #[tokio::main]
3061    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3062    /// let client = ApiClient::new().await?;
3063    /// 
3064    /// let user_id = 123;
3065    /// let gaids = client.get_registered_user_gaids(user_id).await?;
3066    /// 
3067    /// println!("User {} has {} associated GAIDs:", user_id, gaids.len());
3068    /// for gaid in gaids {
3069    ///     println!("  - {}", gaid);
3070    /// }
3071    /// # Ok(())
3072    /// # }
3073    /// ```
3074    ///
3075    /// # Related Methods
3076    /// - [`add_gaid_to_registered_user`](Self::add_gaid_to_registered_user) - Associate a GAID with user
3077    /// - [`set_default_gaid_for_registered_user`](Self::set_default_gaid_for_registered_user) - Set default GAID
3078    /// - [`get_gaid_registered_user`](Self::get_gaid_registered_user) - Find user by GAID
3079    /// - [`validate_gaid`](Self::validate_gaid) - Validate GAID format
3080    pub async fn get_registered_user_gaids(
3081        &self,
3082        registered_user_id: i64,
3083    ) -> Result<Vec<String>, Error> {
3084        self.request_json(
3085            Method::GET,
3086            &["registered_users", &registered_user_id.to_string(), "gaids"],
3087            None::<&()>,
3088        )
3089        .await
3090    }
3091
3092    /// Associates a GAID with a registered user.
3093    ///
3094    /// # Arguments
3095    /// * `registered_user_id` - The ID of the registered user
3096    /// * `gaid` - The GAID to associate with the user
3097    ///
3098    /// # Errors
3099    ///
3100    /// Returns an error if:
3101    /// - Authentication fails
3102    /// - The HTTP request fails
3103    /// - The server returns an error status
3104    /// - The registered user ID is invalid
3105    /// - The GAID is invalid or already associated
3106    pub async fn add_gaid_to_registered_user(
3107        &self,
3108        registered_user_id: i64,
3109        gaid: &str,
3110    ) -> Result<(), Error> {
3111        let request = GaidRequest {
3112            gaid: gaid.to_string(),
3113        };
3114
3115        self.request_empty(
3116            Method::POST,
3117            &[
3118                "registered_users",
3119                &registered_user_id.to_string(),
3120                "gaids",
3121                "add",
3122            ],
3123            Some(request),
3124        )
3125        .await
3126    }
3127
3128    /// Sets an existing GAID as the default for a registered user.
3129    ///
3130    /// This method allows you to designate a specific GAID as the primary/default
3131    /// GAID for a registered user. The GAID must already be associated with the user.
3132    ///
3133    /// # Arguments
3134    /// * `registered_user_id` - The ID of the registered user
3135    /// * `gaid` - The GAID to set as default
3136    ///
3137    /// # Returns
3138    /// Returns `Ok(())` if the operation is successful.
3139    ///
3140    /// # Errors
3141    /// Returns an error if:
3142    /// - Authentication fails
3143    /// - The HTTP request fails
3144    /// - The server returns an error status
3145    /// - The registered user ID is invalid
3146    /// - The GAID is not associated with the user
3147    pub async fn set_default_gaid_for_registered_user(
3148        &self,
3149        registered_user_id: i64,
3150        gaid: &str,
3151    ) -> Result<(), Error> {
3152        let request = GaidRequest {
3153            gaid: gaid.to_string(),
3154        };
3155
3156        self.request_empty(
3157            Method::POST,
3158            &[
3159                "registered_users",
3160                &registered_user_id.to_string(),
3161                "gaids",
3162                "set-default",
3163            ],
3164            Some(request),
3165        )
3166        .await
3167    }
3168
3169    /// Retrieves the registered user associated with a GAID
3170    ///
3171    /// # Arguments
3172    /// * `gaid` - The GAID to look up
3173    ///
3174    /// # Returns
3175    /// Returns the registered user data if the GAID is associated with a user
3176    ///
3177    /// # Errors
3178    /// This function will return an error if:
3179    /// - The GAID has no associated user
3180    /// - The GAID is invalid
3181    /// - Network or authentication errors occur
3182    pub async fn get_gaid_registered_user(
3183        &self,
3184        gaid: &str,
3185    ) -> Result<crate::model::RegisteredUserResponse, Error> {
3186        self.request_json(
3187            Method::GET,
3188            &["gaids", gaid, "registered_user"],
3189            None::<&()>,
3190        )
3191        .await
3192    }
3193
3194    /// Gets the balance information for a specific GAID.
3195    ///
3196    /// This method retrieves all asset balances associated with the given GAID,
3197    /// including confirmed balances and any lost outputs.
3198    ///
3199    /// # Arguments
3200    /// * `gaid` - The GAID to query balance for
3201    ///
3202    /// # Returns
3203    /// Returns a `Balance` struct containing confirmed balances and lost outputs
3204    ///
3205    /// # Errors
3206    /// Returns an error if:
3207    /// - The GAID is invalid
3208    /// - Network or authentication errors occur
3209    /// - The response cannot be parsed
3210    ///
3211    /// # Examples
3212    /// ```no_run
3213    /// # use amp_rs::ApiClient;
3214    /// # #[tokio::main]
3215    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3216    /// let client = ApiClient::new().await?;
3217    /// 
3218    /// let gaid = "GAbYScu6jkWUND2jo3L4KJxyvo55d";
3219    /// let balance = client.get_gaid_balance(gaid).await?;
3220    /// 
3221    /// println!("GAID {} has {} balance entries", gaid, balance.len());
3222    /// for entry in balance {
3223    ///     println!("Asset {}: {} units", entry.asset_id, entry.balance);
3224    /// }
3225    /// # Ok(())
3226    /// # }
3227    /// ```
3228    pub async fn get_gaid_balance(&self, gaid: &str) -> Result<Balance, Error> {
3229        self.request_json(Method::GET, &["gaids", gaid, "balance"], None::<&()>)
3230            .await
3231    }
3232
3233    /// Retrieves the specific asset balance for a GAID
3234    ///
3235    /// # Arguments
3236    /// * `gaid` - The GAID to query
3237    /// * `asset_uuid` - The UUID of the asset to query
3238    ///
3239    /// # Returns
3240    /// Returns the specific asset balance information
3241    ///
3242    /// # Errors
3243    /// Returns an error if:
3244    /// - The GAID is invalid
3245    /// - The asset UUID is invalid
3246    /// - Network or authentication errors occur
3247    /// - The response cannot be parsed
3248    pub async fn get_gaid_asset_balance(
3249        &self,
3250        gaid: &str,
3251        asset_uuid: &str,
3252    ) -> Result<Ownership, Error> {
3253        // Try to get the response as a GaidBalanceEntry first, then convert to Ownership
3254        let balance_entry: GaidBalanceEntry = self
3255            .request_json(
3256                Method::GET,
3257                &["gaids", gaid, "balance", asset_uuid],
3258                None::<&()>,
3259            )
3260            .await?;
3261
3262        // Convert GaidBalanceEntry to Ownership format
3263        Ok(Ownership {
3264            owner: gaid.to_string(),
3265            amount: balance_entry.balance,
3266            gaid: Some(gaid.to_string()),
3267        })
3268    }
3269
3270    /// Gets a list of all categories.
3271    ///
3272    /// # Returns
3273    /// Returns a vector of `CategoryResponse` objects
3274    ///
3275    /// # Errors
3276    /// Returns an error if:
3277    /// - Authentication fails
3278    /// - The HTTP request fails
3279    /// - The response cannot be parsed
3280    ///
3281    /// # Examples
3282    /// ```no_run
3283    /// # use amp_rs::ApiClient;
3284    /// # #[tokio::main]
3285    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3286    /// let client = ApiClient::new().await?;
3287    /// 
3288    /// let categories = client.get_categories().await?;
3289    /// for category in categories {
3290    ///     println!("Category: {} (ID: {})", category.name, category.id);
3291    ///     if let Some(desc) = category.description {
3292    ///         println!("  Description: {}", desc);
3293    ///     }
3294    /// }
3295    /// # Ok(())
3296    /// # }
3297    /// ```
3298    pub async fn get_categories(&self) -> Result<Vec<CategoryResponse>, Error> {
3299        self.request_json(Method::GET, &["categories"], None::<&()>)
3300            .await
3301    }
3302
3303    /// Creates a new category for organizing users and assets.
3304    ///
3305    /// This method creates a new category that can be used to group registered users and assets
3306    /// for organizational purposes. Categories help manage permissions and provide logical
3307    /// groupings for assets and users.
3308    ///
3309    /// # Arguments
3310    /// * `new_category` - A `CategoryAdd` struct containing the category information to create
3311    ///
3312    /// # Returns
3313    /// Returns a `CategoryResponse` containing the created category information including
3314    /// the assigned category ID.
3315    ///
3316    /// # Errors
3317    /// Returns an error if:
3318    /// - Authentication fails or insufficient permissions
3319    /// - The category data is invalid (e.g., missing name, invalid characters)
3320    /// - A category with the same name already exists
3321    /// - The HTTP request fails
3322    /// - The server returns an error status
3323    /// - The response cannot be parsed
3324    ///
3325    /// # Examples
3326    /// ```no_run
3327    /// # use amp_rs::{ApiClient, model::CategoryAdd};
3328    /// # #[tokio::main]
3329    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3330    /// let client = ApiClient::new().await?;
3331    /// 
3332    /// let new_category = CategoryAdd {
3333    ///     name: "Premium Users".to_string(),
3334    ///     description: Some("High-value users with special privileges".to_string()),
3335    /// };
3336    /// 
3337    /// let created_category = client.add_category(&new_category).await?;
3338    /// println!("Created category: {} with ID {}", created_category.name, created_category.id);
3339    /// # Ok(())
3340    /// # }
3341    /// ```
3342    ///
3343    /// # Related Methods
3344    /// - [`get_categories`](Self::get_categories) - List all categories
3345    /// - [`edit_category`](Self::edit_category) - Update category information
3346    /// - [`delete_category`](Self::delete_category) - Remove a category
3347    /// - [`add_registered_user_to_category`](Self::add_registered_user_to_category) - Add users to category
3348    pub async fn add_category(
3349        &self,
3350        new_category: &CategoryAdd,
3351    ) -> Result<CategoryResponse, Error> {
3352        self.request_json(Method::POST, &["categories", "add"], Some(new_category))
3353            .await
3354    }
3355
3356    /// Gets a specific category by ID.
3357    ///
3358    /// This method retrieves detailed information about a specific category, including
3359    /// its name, description, and associated users and assets.
3360    ///
3361    /// # Arguments
3362    /// * `category_id` - The ID of the category to retrieve
3363    ///
3364    /// # Returns
3365    /// Returns a `CategoryResponse` containing the category information including:
3366    /// - Category ID, name, and description
3367    /// - List of associated registered users
3368    /// - List of associated assets
3369    /// - Creation and modification timestamps
3370    ///
3371    /// # Errors
3372    /// Returns an error if:
3373    /// - Authentication fails or insufficient permissions
3374    /// - The category ID is invalid or does not exist
3375    /// - The HTTP request fails
3376    /// - The server returns an error status
3377    /// - The response cannot be parsed
3378    ///
3379    /// # Examples
3380    /// ```no_run
3381    /// # use amp_rs::ApiClient;
3382    /// # #[tokio::main]
3383    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3384    /// let client = ApiClient::new().await?;
3385    /// 
3386    /// let category_id = 1;
3387    /// let category = client.get_category(category_id).await?;
3388    /// 
3389    /// println!("Category: {} (ID: {})", category.name, category.id);
3390    /// if let Some(desc) = category.description {
3391    ///     println!("Description: {}", desc);
3392    /// }
3393    /// println!("Users: {}, Assets: {}", category.registered_users.len(), category.assets.len());
3394    /// # Ok(())
3395    /// # }
3396    /// ```
3397    ///
3398    /// # Related Methods
3399    /// - [`get_categories`](Self::get_categories) - List all categories
3400    /// - [`add_category`](Self::add_category) - Create a new category
3401    /// - [`edit_category`](Self::edit_category) - Update category information
3402    /// - [`delete_category`](Self::delete_category) - Remove a category
3403    pub async fn get_category(&self, category_id: i64) -> Result<CategoryResponse, Error> {
3404        self.request_json(
3405            Method::GET,
3406            &["categories", &category_id.to_string()],
3407            None::<&()>,
3408        )
3409        .await
3410    }
3411
3412    /// Updates category information.
3413    ///
3414    /// This method allows you to modify the information of an existing category.
3415    /// Only the fields provided in the edit data will be updated; other fields remain unchanged.
3416    ///
3417    /// # Arguments
3418    /// * `category_id` - The ID of the category to update
3419    /// * `edit_category` - A `CategoryEdit` struct containing the fields to update
3420    ///
3421    /// # Returns
3422    /// Returns a `CategoryResponse` containing the updated category information.
3423    ///
3424    /// # Errors
3425    /// Returns an error if:
3426    /// - Authentication fails or insufficient permissions
3427    /// - The category ID is invalid or does not exist
3428    /// - The edit data contains invalid values (e.g., empty name, invalid characters)
3429    /// - A category with the new name already exists (if name is being changed)
3430    /// - The HTTP request fails
3431    /// - The server returns an error status
3432    /// - The response cannot be parsed
3433    ///
3434    /// # Examples
3435    /// ```no_run
3436    /// # use amp_rs::{ApiClient, model::CategoryEdit};
3437    /// # #[tokio::main]
3438    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3439    /// let client = ApiClient::new().await?;
3440    /// 
3441    /// let category_id = 1;
3442    /// let edit_data = CategoryEdit {
3443    ///     name: Some("VIP Users".to_string()),
3444    ///     description: Some("Very important users with premium access".to_string()),
3445    /// };
3446    /// 
3447    /// let updated_category = client.edit_category(category_id, &edit_data).await?;
3448    /// println!("Updated category: {}", updated_category.name);
3449    /// # Ok(())
3450    /// # }
3451    /// ```
3452    ///
3453    /// # Related Methods
3454    /// - [`get_category`](Self::get_category) - Get current category information
3455    /// - [`add_category`](Self::add_category) - Create a new category
3456    /// - [`delete_category`](Self::delete_category) - Remove a category
3457    pub async fn edit_category(
3458        &self,
3459        category_id: i64,
3460        edit_category: &CategoryEdit,
3461    ) -> Result<CategoryResponse, Error> {
3462        self.request_json(
3463            Method::PUT,
3464            &["categories", &category_id.to_string(), "edit"],
3465            Some(edit_category),
3466        )
3467        .await
3468    }
3469
3470    /// Removes a category from the system.
3471    ///
3472    /// This method permanently deletes a category. All users and assets associated with the
3473    /// category will be disassociated, but the users and assets themselves are not deleted.
3474    /// This operation cannot be undone.
3475    ///
3476    /// # Arguments
3477    /// * `category_id` - The ID of the category to delete
3478    ///
3479    /// # Returns
3480    /// Returns `Ok(())` on successful deletion.
3481    ///
3482    /// # Errors
3483    /// Returns an error if:
3484    /// - Authentication fails or insufficient permissions
3485    /// - The category ID is invalid or does not exist
3486    /// - The category is still in use and cannot be deleted (depending on system configuration)
3487    /// - The HTTP request fails
3488    /// - The server returns an error status
3489    ///
3490    /// # Examples
3491    /// ```no_run
3492    /// # use amp_rs::ApiClient;
3493    /// # #[tokio::main]
3494    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3495    /// let client = ApiClient::new().await?;
3496    /// 
3497    /// let category_id = 1;
3498    /// client.delete_category(category_id).await?;
3499    /// println!("Successfully deleted category with ID {}", category_id);
3500    /// # Ok(())
3501    /// # }
3502    /// ```
3503    ///
3504    /// # Related Methods
3505    /// - [`get_category`](Self::get_category) - Get category information before deletion
3506    /// - [`add_category`](Self::add_category) - Create a new category
3507    /// - [`remove_registered_user_from_category`](Self::remove_registered_user_from_category) - Remove users first
3508    /// - [`remove_asset_from_category`](Self::remove_asset_from_category) - Remove assets first
3509    pub async fn delete_category(&self, category_id: i64) -> Result<(), Error> {
3510        self.request_empty(
3511            Method::DELETE,
3512            &["categories", &category_id.to_string(), "delete"],
3513            None::<&()>,
3514        )
3515        .await
3516    }
3517
3518    /// Associates a registered user with a category.
3519    ///
3520    /// This method adds a registered user to a category, allowing for organized grouping
3521    /// of users. Users can belong to multiple categories, and categories can contain
3522    /// multiple users.
3523    ///
3524    /// # Arguments
3525    /// * `category_id` - The ID of the category to add the user to
3526    /// * `user_id` - The ID of the registered user to add to the category
3527    ///
3528    /// # Returns
3529    /// Returns a `CategoryResponse` containing the updated category information including
3530    /// the newly added user.
3531    ///
3532    /// # Errors
3533    /// Returns an error if:
3534    /// - Authentication fails or insufficient permissions
3535    /// - The category ID is invalid or does not exist
3536    /// - The user ID is invalid or does not exist
3537    /// - The user is already associated with the category
3538    /// - The HTTP request fails
3539    /// - The server returns an error status
3540    /// - The response cannot be parsed
3541    ///
3542    /// # Examples
3543    /// ```no_run
3544    /// # use amp_rs::ApiClient;
3545    /// # #[tokio::main]
3546    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3547    /// let client = ApiClient::new().await?;
3548    /// 
3549    /// let category_id = 1;
3550    /// let user_id = 123;
3551    /// 
3552    /// let updated_category = client.add_registered_user_to_category(category_id, user_id).await?;
3553    /// println!("Added user {} to category '{}'", user_id, updated_category.name);
3554    /// println!("Category now has {} users", updated_category.registered_users.len());
3555    /// # Ok(())
3556    /// # }
3557    /// ```
3558    ///
3559    /// # Related Methods
3560    /// - [`remove_registered_user_from_category`](Self::remove_registered_user_from_category) - Remove user from category
3561    /// - [`get_category`](Self::get_category) - Get category information including users
3562    /// - [`get_registered_user`](Self::get_registered_user) - Get user information
3563    pub async fn add_registered_user_to_category(
3564        &self,
3565        category_id: i64,
3566        user_id: i64,
3567    ) -> Result<CategoryResponse, Error> {
3568        self.request_json(
3569            Method::PUT,
3570            &[
3571                "categories",
3572                &category_id.to_string(),
3573                "registered_users",
3574                &user_id.to_string(),
3575                "add",
3576            ],
3577            None::<&()>,
3578        )
3579        .await
3580    }
3581
3582    /// Removes a registered user from a category.
3583    ///
3584    /// This method disassociates a registered user from a category. The user remains in the
3585    /// system but is no longer part of the specified category. This does not affect the user's
3586    /// association with other categories.
3587    ///
3588    /// # Arguments
3589    /// * `category_id` - The ID of the category to remove the user from
3590    /// * `user_id` - The ID of the registered user to remove from the category
3591    ///
3592    /// # Returns
3593    /// Returns a `CategoryResponse` containing the updated category information without
3594    /// the removed user.
3595    ///
3596    /// # Errors
3597    /// Returns an error if:
3598    /// - Authentication fails or insufficient permissions
3599    /// - The category ID is invalid or does not exist
3600    /// - The user ID is invalid or does not exist
3601    /// - The user is not currently associated with the category
3602    /// - The HTTP request fails
3603    /// - The server returns an error status
3604    /// - The response cannot be parsed
3605    ///
3606    /// # Examples
3607    /// ```no_run
3608    /// # use amp_rs::ApiClient;
3609    /// # #[tokio::main]
3610    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3611    /// let client = ApiClient::new().await?;
3612    /// 
3613    /// let category_id = 1;
3614    /// let user_id = 123;
3615    /// 
3616    /// let updated_category = client.remove_registered_user_from_category(category_id, user_id).await?;
3617    /// println!("Removed user {} from category '{}'", user_id, updated_category.name);
3618    /// println!("Category now has {} users", updated_category.registered_users.len());
3619    /// # Ok(())
3620    /// # }
3621    /// ```
3622    ///
3623    /// # Related Methods
3624    /// - [`add_registered_user_to_category`](Self::add_registered_user_to_category) - Add user to category
3625    /// - [`get_category`](Self::get_category) - Get category information including users
3626    /// - [`get_registered_user`](Self::get_registered_user) - Get user information
3627    pub async fn remove_registered_user_from_category(
3628        &self,
3629        category_id: i64,
3630        user_id: i64,
3631    ) -> Result<CategoryResponse, Error> {
3632        self.request_json(
3633            Method::PUT,
3634            &[
3635                "categories",
3636                &category_id.to_string(),
3637                "registered_users",
3638                &user_id.to_string(),
3639                "remove",
3640            ],
3641            None::<&()>,
3642        )
3643        .await
3644    }
3645
3646    /// Associates an asset with a category.
3647    ///
3648    /// This method adds an asset to a category, allowing for organized grouping of assets.
3649    /// Assets can belong to multiple categories, and categories can contain multiple assets.
3650    /// This helps with asset management and permission organization.
3651    ///
3652    /// # Arguments
3653    /// * `category_id` - The ID of the category to add the asset to
3654    /// * `asset_uuid` - The UUID of the asset to add to the category
3655    ///
3656    /// # Returns
3657    /// Returns a `CategoryResponse` containing the updated category information including
3658    /// the newly added asset.
3659    ///
3660    /// # Errors
3661    /// Returns an error if:
3662    /// - Authentication fails or insufficient permissions
3663    /// - The category ID is invalid or does not exist
3664    /// - The asset UUID is invalid or does not exist
3665    /// - The asset is already associated with the category
3666    /// - The HTTP request fails
3667    /// - The server returns an error status
3668    /// - The response cannot be parsed
3669    ///
3670    /// # Examples
3671    /// ```no_run
3672    /// # use amp_rs::ApiClient;
3673    /// # #[tokio::main]
3674    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3675    /// let client = ApiClient::new().await?;
3676    /// 
3677    /// let category_id = 1;
3678    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
3679    /// 
3680    /// let updated_category = client.add_asset_to_category(category_id, asset_uuid).await?;
3681    /// println!("Added asset {} to category '{}'", asset_uuid, updated_category.name);
3682    /// println!("Category now has {} assets", updated_category.assets.len());
3683    /// # Ok(())
3684    /// # }
3685    /// ```
3686    ///
3687    /// # Related Methods
3688    /// - [`remove_asset_from_category`](Self::remove_asset_from_category) - Remove asset from category
3689    /// - [`get_category`](Self::get_category) - Get category information including assets
3690    /// - [`get_asset`](Self::get_asset) - Get asset information
3691    pub async fn add_asset_to_category(
3692        &self,
3693        category_id: i64,
3694        asset_uuid: &str,
3695    ) -> Result<CategoryResponse, Error> {
3696        self.request_json(
3697            Method::PUT,
3698            &[
3699                "categories",
3700                &category_id.to_string(),
3701                "assets",
3702                asset_uuid,
3703                "add",
3704            ],
3705            None::<&()>,
3706        )
3707        .await
3708    }
3709
3710    /// Removes an asset from a category.
3711    ///
3712    /// This method disassociates an asset from a category. The asset remains in the system
3713    /// but is no longer part of the specified category. This does not affect the asset's
3714    /// association with other categories.
3715    ///
3716    /// # Arguments
3717    /// * `category_id` - The ID of the category to remove the asset from
3718    /// * `asset_uuid` - The UUID of the asset to remove from the category
3719    ///
3720    /// # Returns
3721    /// Returns a `CategoryResponse` containing the updated category information without
3722    /// the removed asset.
3723    ///
3724    /// # Errors
3725    /// Returns an error if:
3726    /// - Authentication fails or insufficient permissions
3727    /// - The category ID is invalid or does not exist
3728    /// - The asset UUID is invalid or does not exist
3729    /// - The asset is not currently associated with the category
3730    /// - The HTTP request fails
3731    /// - The server returns an error status
3732    /// - The response cannot be parsed
3733    ///
3734    /// # Examples
3735    /// ```no_run
3736    /// # use amp_rs::ApiClient;
3737    /// # #[tokio::main]
3738    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3739    /// let client = ApiClient::new().await?;
3740    /// 
3741    /// let category_id = 1;
3742    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
3743    /// 
3744    /// let updated_category = client.remove_asset_from_category(category_id, asset_uuid).await?;
3745    /// println!("Removed asset {} from category '{}'", asset_uuid, updated_category.name);
3746    /// println!("Category now has {} assets", updated_category.assets.len());
3747    /// # Ok(())
3748    /// # }
3749    /// ```
3750    ///
3751    /// # Related Methods
3752    /// - [`add_asset_to_category`](Self::add_asset_to_category) - Add asset to category
3753    /// - [`get_category`](Self::get_category) - Get category information including assets
3754    /// - [`get_asset`](Self::get_asset) - Get asset information
3755    pub async fn remove_asset_from_category(
3756        &self,
3757        category_id: i64,
3758        asset_uuid: &str,
3759    ) -> Result<CategoryResponse, Error> {
3760        self.request_json(
3761            Method::PUT,
3762            &[
3763                "categories",
3764                &category_id.to_string(),
3765                "assets",
3766                asset_uuid,
3767                "remove",
3768            ],
3769            None::<&()>,
3770        )
3771        .await
3772    }
3773
3774    /// Validates a GAID (Green Address ID).
3775    ///
3776    /// # Arguments
3777    /// * `gaid` - The GAID string to validate
3778    ///
3779    /// # Returns
3780    /// Returns a `ValidateGaidResponse` indicating whether the GAID is valid
3781    ///
3782    /// # Errors
3783    /// Returns an error if:
3784    /// - Authentication fails
3785    /// - The HTTP request fails
3786    /// - The response cannot be parsed
3787    ///
3788    /// # Examples
3789    /// ```no_run
3790    /// # use amp_rs::ApiClient;
3791    /// # #[tokio::main]
3792    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3793    /// let client = ApiClient::new().await?;
3794    /// 
3795    /// let gaid = "GAbYScu6jkWUND2jo3L4KJxyvo55d";
3796    /// let validation = client.validate_gaid(gaid).await?;
3797    /// 
3798    /// if validation.is_valid {
3799    ///     println!("GAID {} is valid", gaid);
3800    /// } else {
3801    ///     println!("GAID {} is invalid: {:?}", gaid, validation.error);
3802    /// }
3803    /// # Ok(())
3804    /// # }
3805    /// ```
3806    pub async fn validate_gaid(
3807        &self,
3808        gaid: &str,
3809    ) -> Result<crate::model::ValidateGaidResponse, Error> {
3810        self.request_json(Method::GET, &["gaids", gaid, "validate"], None::<&()>)
3811            .await
3812    }
3813
3814    /// Gets the address associated with a GAID.
3815    ///
3816    /// # Arguments
3817    /// * `gaid` - The GAID to get the address for
3818    ///
3819    /// # Returns
3820    /// Returns an `AddressGaidResponse` containing the address
3821    ///
3822    /// # Errors
3823    /// Returns an error if:
3824    /// - The GAID is invalid
3825    /// - Authentication fails
3826    /// - The HTTP request fails
3827    /// - The response cannot be parsed
3828    ///
3829    /// # Examples
3830    /// ```no_run
3831    /// # use amp_rs::ApiClient;
3832    /// # #[tokio::main]
3833    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3834    /// let client = ApiClient::new().await?;
3835    /// 
3836    /// let gaid = "GAbYScu6jkWUND2jo3L4KJxyvo55d";
3837    /// let address_response = client.get_gaid_address(gaid).await?;
3838    /// 
3839    /// println!("Address for GAID {}: {}", gaid, address_response.address);
3840    /// # Ok(())
3841    /// # }
3842    /// ```
3843    pub async fn get_gaid_address(
3844        &self,
3845        gaid: &str,
3846    ) -> Result<crate::model::AddressGaidResponse, Error> {
3847        self.request_json(Method::GET, &["gaids", gaid, "address"], None::<&()>)
3848            .await
3849    }
3850
3851    /// Gets a list of all managers.
3852    ///
3853    /// # Returns
3854    /// Returns a vector of `Manager` objects
3855    ///
3856    /// # Errors
3857    /// Returns an error if:
3858    /// - Authentication fails
3859    /// - The HTTP request fails
3860    /// - The response cannot be parsed
3861    ///
3862    /// # Examples
3863    /// ```no_run
3864    /// # use amp_rs::ApiClient;
3865    /// # #[tokio::main]
3866    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3867    /// let client = ApiClient::new().await?;
3868    /// 
3869    /// let managers = client.get_managers().await?;
3870    /// for manager in managers {
3871    ///     println!("Manager: {} (ID: {})", manager.username, manager.id);
3872    /// }
3873    /// # Ok(())
3874    /// # }
3875    /// ```
3876    pub async fn get_managers(&self) -> Result<Vec<crate::model::Manager>, Error> {
3877        self.request_json(Method::GET, &["managers"], None::<&()>)
3878            .await
3879    }
3880
3881    /// Creates a new manager.
3882    ///
3883    /// # Arguments
3884    /// * `new_manager` - The manager creation request containing username and password
3885    ///
3886    /// # Returns
3887    /// Returns the created `Manager` object
3888    ///
3889    /// # Errors
3890    /// Returns an error if:
3891    /// - Authentication fails
3892    /// - The HTTP request fails
3893    /// - The manager creation request is invalid
3894    /// - The response cannot be parsed
3895    ///
3896    /// # Examples
3897    /// ```no_run
3898    /// # use amp_rs::{ApiClient, model::ManagerCreate};
3899    /// # #[tokio::main]
3900    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3901    /// let client = ApiClient::new().await?;
3902    /// 
3903    /// let new_manager = ManagerCreate {
3904    ///     username: "new_manager".to_string(),
3905    ///     password: "secure_password".to_string(),
3906    /// };
3907    /// 
3908    /// let manager = client.create_manager(&new_manager).await?;
3909    /// println!("Created manager: {} (ID: {})", manager.username, manager.id);
3910    /// # Ok(())
3911    /// # }
3912    /// ```
3913    pub async fn create_manager(
3914        &self,
3915        new_manager: &crate::model::ManagerCreate,
3916    ) -> Result<crate::model::Manager, Error> {
3917        self.request_json(Method::POST, &["managers", "create"], Some(new_manager))
3918            .await
3919    }
3920
3921    /// Gets all assignments for a specific asset.
3922    ///
3923    /// # Arguments
3924    /// * `asset_uuid` - The UUID of the asset to get assignments for
3925    ///
3926    /// # Returns
3927    /// Returns a vector of `Assignment` objects
3928    ///
3929    /// # Errors
3930    /// Returns an error if:
3931    /// - Authentication fails
3932    /// - The HTTP request fails
3933    /// - The asset UUID is invalid
3934    /// - The response cannot be parsed
3935    ///
3936    /// # Examples
3937    /// ```no_run
3938    /// # use amp_rs::ApiClient;
3939    /// # #[tokio::main]
3940    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3941    /// let client = ApiClient::new().await?;
3942    /// 
3943    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
3944    /// let assignments = client.get_asset_assignments(asset_uuid).await?;
3945    /// 
3946    /// for assignment in assignments {
3947    ///     println!("Assignment ID: {}, Amount: {}", assignment.id, assignment.amount);
3948    /// }
3949    /// # Ok(())
3950    /// # }
3951    /// ```
3952    pub async fn get_asset_assignments(&self, asset_uuid: &str) -> Result<Vec<Assignment>, Error> {
3953        self.request_json(
3954            Method::GET,
3955            &["assets", asset_uuid, "assignments"],
3956            None::<&()>,
3957        )
3958        .await
3959    }
3960
3961    /// Creates multiple asset assignments in batch.
3962    ///
3963    /// This method creates multiple asset assignments for the specified asset. Each assignment
3964    /// allocates a specific amount of the asset to a registered user. The assignments are
3965    /// created individually due to API limitations, but this method handles the batch processing
3966    /// automatically.
3967    ///
3968    /// # Arguments
3969    /// * `asset_uuid` - The UUID of the asset to create assignments for
3970    /// * `requests` - A slice of `CreateAssetAssignmentRequest` structs containing assignment details
3971    ///
3972    /// # Returns
3973    /// Returns a vector of `Assignment` structs representing the created assignments with their
3974    /// assigned IDs and status information.
3975    ///
3976    /// # Errors
3977    /// Returns an error if:
3978    /// - Authentication fails or insufficient permissions
3979    /// - The asset UUID is invalid or does not exist
3980    /// - Any assignment request contains invalid data (e.g., invalid user ID, negative amount)
3981    /// - Insufficient asset balance for the total requested assignments
3982    /// - Any individual assignment creation fails
3983    /// - The HTTP request fails
3984    /// - The server returns an error status
3985    /// - The response cannot be parsed
3986    ///
3987    /// # Examples
3988    /// ```no_run
3989    /// # use amp_rs::{ApiClient, model::CreateAssetAssignmentRequest};
3990    /// # #[tokio::main]
3991    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
3992    /// let client = ApiClient::new().await?;
3993    /// 
3994    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
3995    /// let requests = vec![
3996    ///     CreateAssetAssignmentRequest {
3997    ///         registered_user: 123,
3998    ///         amount: 1000,
3999    ///         vesting_timestamp: None,
4000    ///         ready_for_distribution: false,
4001    ///     },
4002    ///     CreateAssetAssignmentRequest {
4003    ///         registered_user: 456,
4004    ///         amount: 500,
4005    ///         vesting_timestamp: None,
4006    ///         ready_for_distribution: true,
4007    ///     },
4008    /// ];
4009    /// 
4010    /// let assignments = client.create_asset_assignments(asset_uuid, &requests).await?;
4011    /// println!("Created {} assignments", assignments.len());
4012    /// for assignment in assignments {
4013    ///     println!("Assignment {}: {} units to user {}", 
4014    ///              assignment.id, assignment.amount, assignment.registered_user);
4015    /// }
4016    /// # Ok(())
4017    /// # }
4018    /// ```
4019    ///
4020    /// # Related Methods
4021    /// - [`get_asset_assignments`](Self::get_asset_assignments) - List all assignments for an asset
4022    /// - [`delete_asset_assignment`](Self::delete_asset_assignment) - Remove an assignment
4023    /// - [`edit_asset_assignment`](Self::edit_asset_assignment) - Update assignment details
4024    /// - [`set_assignment_ready_for_distribution`](Self::set_assignment_ready_for_distribution) - Mark for distribution
4025    pub async fn create_asset_assignments(
4026        &self,
4027        asset_uuid: &str,
4028        requests: &[CreateAssetAssignmentRequest],
4029    ) -> Result<Vec<Assignment>, Error> {
4030        use crate::model::CreateAssetAssignmentRequestWrapper;
4031
4032        // The API only supports maximum length 1 per request, so we need to break
4033        // multiple assignments into separate CreateAssetAssignmentRequestWrapper instances
4034        let mut all_assignments = Vec::new();
4035
4036        for request in requests {
4037            let wrapper = CreateAssetAssignmentRequestWrapper {
4038                assignments: vec![request.clone()],
4039            };
4040
4041            let assignments: Vec<Assignment> = self
4042                .request_json(
4043                    Method::POST,
4044                    &["assets", asset_uuid, "assignments", "create"],
4045                    Some(&wrapper),
4046                )
4047                .await?;
4048
4049            all_assignments.extend(assignments);
4050        }
4051
4052        Ok(all_assignments)
4053    }
4054
4055    /// Gets a specific asset assignment by asset UUID and assignment ID.
4056    ///
4057    /// This method sends a GET request to retrieve detailed information about a specific asset
4058    /// assignment. Asset assignments represent the allocation of assets to users or entities,
4059    /// including information such as the assigned amount, recipient details, and assignment status.
4060    ///
4061    /// # Arguments
4062    /// * `asset_uuid` - The UUID of the asset for which to retrieve the assignment
4063    /// * `assignment_id` - The ID of the specific assignment to retrieve
4064    ///
4065    /// # Returns
4066    /// Returns an `Assignment` struct containing the assignment details including:
4067    /// - Assignment ID and amount
4068    /// - Recipient information
4069    /// - Assignment status and metadata
4070    /// - Creation and modification timestamps
4071    ///
4072    /// # Errors
4073    /// Returns an error if:
4074    /// - Authentication fails
4075    /// - The HTTP request fails
4076    /// - The server returns an error status
4077    /// - The asset UUID is invalid or does not exist
4078    /// - The assignment ID is invalid or does not exist
4079    /// - The assignment is not accessible to the current user
4080    /// - The response cannot be parsed as a valid Assignment
4081    ///
4082    /// # Example
4083    /// ```no_run
4084    /// # use amp_rs::ApiClient;
4085    /// # #[tokio::main]
4086    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4087    /// let client = ApiClient::new().await?;
4088    ///
4089    /// // Retrieve assignment with ID "123" for asset "550e8400-e29b-41d4-a716-446655440000"
4090    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
4091    /// let assignment_id = "123";
4092    ///
4093    /// let assignment = client.get_asset_assignment(asset_uuid, assignment_id).await?;
4094    ///
4095    /// println!("Assignment ID: {}", assignment.id);
4096    /// println!("Assigned amount: {}", assignment.amount);
4097    /// println!("Registered user: {}", assignment.registered_user);
4098    /// # Ok(())
4099    /// # }
4100    /// ```
4101    pub async fn get_asset_assignment(
4102        &self,
4103        asset_uuid: &str,
4104        assignment_id: &str,
4105    ) -> Result<Assignment, Error> {
4106        self.request_json(
4107            Method::GET,
4108            &["assets", asset_uuid, "assignments", assignment_id],
4109            None::<&()>,
4110        )
4111        .await
4112    }
4113
4114    /// Gets a specific manager by ID.
4115    ///
4116    /// # Arguments
4117    /// * `manager_id` - The ID of the manager to retrieve
4118    ///
4119    /// # Errors
4120    /// Returns an error if:
4121    /// - Authentication fails
4122    /// - The HTTP request fails
4123    /// - The server returns an error status
4124    /// - The response cannot be parsed as JSON
4125    pub async fn get_manager(&self, manager_id: i64) -> Result<crate::model::Manager, Error> {
4126        self.request_json(
4127            Method::GET,
4128            &["managers", &manager_id.to_string()],
4129            None::<&()>,
4130        )
4131        .await
4132    }
4133
4134    /// Removes a manager's permissions to modify a specific asset.
4135    ///
4136    /// This method revokes a manager's access to a specific asset, preventing them from
4137    /// performing asset management operations such as creating assignments, managing ownership,
4138    /// or modifying asset properties. The manager will no longer be able to access this asset
4139    /// through their management interface.
4140    ///
4141    /// # Arguments
4142    /// * `manager_id` - The ID of the manager to remove permissions from
4143    /// * `asset_uuid` - The UUID of the asset to remove permissions for
4144    ///
4145    /// # Returns
4146    /// Returns `Ok(())` on successful permission removal.
4147    ///
4148    /// # Errors
4149    /// Returns an error if:
4150    /// - Authentication fails or insufficient permissions
4151    /// - The manager ID is invalid or does not exist
4152    /// - The asset UUID is invalid or does not exist
4153    /// - The manager does not currently have permissions for this asset
4154    /// - The HTTP request fails
4155    /// - The server returns an error status
4156    ///
4157    /// # Examples
4158    /// ```no_run
4159    /// # use amp_rs::ApiClient;
4160    /// # #[tokio::main]
4161    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4162    /// let client = ApiClient::new().await?;
4163    /// 
4164    /// let manager_id = 123;
4165    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
4166    /// 
4167    /// client.manager_remove_asset(manager_id, asset_uuid).await?;
4168    /// println!("Removed asset {} from manager {}", asset_uuid, manager_id);
4169    /// # Ok(())
4170    /// # }
4171    /// ```
4172    ///
4173    /// # Related Methods
4174    /// - [`add_asset_to_manager`](Self::add_asset_to_manager) - Grant manager permissions for an asset
4175    /// - [`get_manager`](Self::get_manager) - Get manager information including current assets
4176    /// - [`revoke_manager`](Self::revoke_manager) - Remove all asset permissions from manager
4177    /// - [`lock_manager`](Self::lock_manager) - Lock manager account
4178    pub async fn manager_remove_asset(
4179        &self,
4180        manager_id: i64,
4181        asset_uuid: &str,
4182    ) -> Result<(), Error> {
4183        self.request_empty(
4184            Method::POST,
4185            &[
4186                "managers",
4187                &manager_id.to_string(),
4188                "assets",
4189                asset_uuid,
4190                "remove",
4191            ],
4192            None::<&()>,
4193        )
4194        .await
4195    }
4196
4197    /// Revokes all asset permissions for a manager.
4198    ///
4199    /// This method first retrieves the manager's current asset permissions,
4200    /// then removes the manager's access to each asset they currently have access to.
4201    ///
4202    /// # Arguments
4203    /// * `manager_id` - The ID of the manager to revoke permissions for
4204    ///
4205    /// # Errors
4206    /// Returns an error if:
4207    /// - Authentication fails
4208    /// - The HTTP request fails
4209    /// - The server returns an error status
4210    /// - Any individual asset removal fails
4211    pub async fn revoke_manager(&self, manager_id: i64) -> Result<(), Error> {
4212        // First, get the manager to see which assets they have access to
4213        let manager = self.get_manager(manager_id).await?;
4214
4215        // Remove the manager's access to each asset
4216        for asset_uuid in &manager.assets {
4217            self.manager_remove_asset(manager_id, asset_uuid).await?;
4218        }
4219
4220        Ok(())
4221    }
4222
4223    /// Gets the current manager information as raw JSON.
4224    ///
4225    /// This method calls the `/managers/me` endpoint to retrieve information
4226    /// about the currently authenticated manager.
4227    ///
4228    /// # Errors
4229    /// Returns an error if:
4230    /// - Authentication fails
4231    /// - The HTTP request fails
4232    /// - The server returns an error status
4233    /// - The response cannot be parsed as JSON
4234    pub async fn get_current_manager_raw(&self) -> Result<serde_json::Value, Error> {
4235        self.request_json(Method::GET, &["managers", "me"], None::<&()>)
4236            .await
4237    }
4238
4239    /// Locks a manager account to prevent further operations.
4240    ///
4241    /// This method sends a PUT request to lock the specified manager, preventing any further
4242    /// operations on that manager account. This is typically used for security purposes or
4243    /// when a manager needs to be temporarily disabled.
4244    ///
4245    /// # Arguments
4246    /// * `manager_id` - The ID of the manager to lock
4247    ///
4248    /// # Returns
4249    /// Returns `Ok(())` if the manager was successfully locked.
4250    ///
4251    /// # Errors
4252    /// Returns an error if:
4253    /// - Authentication fails
4254    /// - The HTTP request fails
4255    /// - The server returns an error status
4256    /// - The manager ID is invalid or does not exist
4257    /// - The manager is already locked
4258    ///
4259    /// # Example
4260    /// ```no_run
4261    /// # use amp_rs::ApiClient;
4262    /// # #[tokio::main]
4263    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4264    /// let client = ApiClient::new().await?;
4265    ///
4266    /// // Lock manager with ID 123
4267    /// client.lock_manager(123).await?;
4268    /// println!("Manager 123 has been locked successfully");
4269    /// # Ok(())
4270    /// # }
4271    /// ```
4272    pub async fn lock_manager(&self, manager_id: i64) -> Result<(), Error> {
4273        self.request_empty(
4274            Method::PUT,
4275            &["managers", &manager_id.to_string(), "lock"],
4276            None::<&()>,
4277        )
4278        .await
4279    }
4280
4281    /// Unlocks a manager account.
4282    ///
4283    /// # Arguments
4284    /// * `manager_id` - The ID of the manager to unlock
4285    ///
4286    /// # Errors
4287    /// Returns an error if:
4288    /// - Authentication fails
4289    /// - The HTTP request fails
4290    /// - The server returns an error status
4291    pub async fn unlock_manager(&self, manager_id: i64) -> Result<(), Error> {
4292        self.request_empty(
4293            Method::PUT,
4294            &["managers", &manager_id.to_string(), "unlock"],
4295            None::<&()>,
4296        )
4297        .await
4298    }
4299
4300    /// Authorizes a manager to manage a specific asset.
4301    ///
4302    /// This method sends a PUT request to authorize the specified manager to manage the given asset.
4303    /// Once authorized, the manager will have permissions to perform operations on the asset such as
4304    /// creating assignments, managing ownership, and other asset-related operations.
4305    ///
4306    /// # Arguments
4307    /// * `manager_id` - The ID of the manager to authorize
4308    /// * `asset_uuid` - The UUID of the asset to add to the manager's authorized assets
4309    ///
4310    /// # Returns
4311    /// Returns `Ok(())` if the manager was successfully authorized for the asset.
4312    ///
4313    /// # Errors
4314    /// Returns an error if:
4315    /// - Authentication fails or insufficient permissions
4316    /// - The HTTP request fails
4317    /// - The server returns an error status
4318    /// - The manager ID is invalid or does not exist
4319    /// - The asset UUID is invalid or does not exist
4320    /// - The manager is already authorized for this asset
4321    /// - The manager is locked and cannot be modified
4322    ///
4323    /// # Examples
4324    /// ```no_run
4325    /// # use amp_rs::ApiClient;
4326    /// # #[tokio::main]
4327    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4328    /// let client = ApiClient::new().await?;
4329    ///
4330    /// // Authorize manager 123 to manage asset with UUID "550e8400-e29b-41d4-a716-446655440000"
4331    /// let manager_id = 123;
4332    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
4333    ///
4334    /// client.add_asset_to_manager(manager_id, asset_uuid).await?;
4335    /// println!("Manager {} is now authorized to manage asset {}", manager_id, asset_uuid);
4336    /// # Ok(())
4337    /// # }
4338    /// ```
4339    ///
4340    /// # Related Methods
4341    /// - [`manager_remove_asset`](Self::manager_remove_asset) - Remove manager permissions for an asset
4342    /// - [`get_manager`](Self::get_manager) - Get manager information including current assets
4343    /// - [`get_manager_permissions`](Self::get_manager_permissions) - Get manager's current permissions
4344    /// - [`lock_manager`](Self::lock_manager) - Lock manager account
4345    pub async fn add_asset_to_manager(
4346        &self,
4347        manager_id: i64,
4348        asset_uuid: &str,
4349    ) -> Result<(), Error> {
4350        self.request_empty(
4351            Method::PUT,
4352            &[
4353                "managers",
4354                &manager_id.to_string(),
4355                "assets",
4356                asset_uuid,
4357                "add",
4358            ],
4359            None::<&()>,
4360        )
4361        .await
4362    }
4363
4364    /// Deletes a specific asset assignment.
4365    ///
4366    /// # Arguments
4367    /// * `asset_uuid` - The UUID of the asset
4368    /// * `assignment_id` - The ID of the assignment to delete
4369    ///
4370    /// # Errors
4371    /// Returns an error if:
4372    /// - Authentication fails
4373    /// - The HTTP request fails
4374    /// - The server returns an error status
4375    /// Removes an asset assignment.
4376    ///
4377    /// This method permanently deletes an asset assignment, returning the allocated assets
4378    /// back to the available pool. This operation cannot be undone. If the assignment has
4379    /// already been distributed, this operation may fail.
4380    ///
4381    /// # Arguments
4382    /// * `asset_uuid` - The UUID of the asset containing the assignment
4383    /// * `assignment_id` - The ID of the assignment to delete
4384    ///
4385    /// # Returns
4386    /// Returns `Ok(())` on successful deletion.
4387    ///
4388    /// # Errors
4389    /// Returns an error if:
4390    /// - Authentication fails or insufficient permissions
4391    /// - The asset UUID is invalid or does not exist
4392    /// - The assignment ID is invalid or does not exist
4393    /// - The assignment has already been distributed and cannot be deleted
4394    /// - The assignment is locked and cannot be modified
4395    /// - The HTTP request fails
4396    /// - The server returns an error status
4397    ///
4398    /// # Examples
4399    /// ```no_run
4400    /// # use amp_rs::ApiClient;
4401    /// # #[tokio::main]
4402    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
4403    /// let client = ApiClient::new().await?;
4404    /// 
4405    /// let asset_uuid = "550e8400-e29b-41d4-a716-446655440000";
4406    /// let assignment_id = "123";
4407    /// 
4408    /// client.delete_asset_assignment(asset_uuid, assignment_id).await?;
4409    /// println!("Successfully deleted assignment {}", assignment_id);
4410    /// # Ok(())
4411    /// # }
4412    /// ```
4413    ///
4414    /// # Related Methods
4415    /// - [`get_asset_assignment`](Self::get_asset_assignment) - Get assignment details before deletion
4416    /// - [`create_asset_assignments`](Self::create_asset_assignments) - Create new assignments
4417    /// - [`edit_asset_assignment`](Self::edit_asset_assignment) - Update assignment instead of deleting
4418    /// - [`lock_asset_assignment`](Self::lock_asset_assignment) - Lock assignment to prevent changes
4419    pub async fn delete_asset_assignment(
4420        &self,
4421        asset_uuid: &str,
4422        assignment_id: &str,
4423    ) -> Result<(), Error> {
4424        self.request_empty(
4425            Method::DELETE,
4426            &["assets", asset_uuid, "assignments", assignment_id, "delete"],
4427            None::<&()>,
4428        )
4429        .await
4430    }
4431
4432    /// Locks a specific asset assignment.
4433    ///
4434    /// # Arguments
4435    /// * `asset_uuid` - The UUID of the asset
4436    /// * `assignment_id` - The ID of the assignment to lock
4437    ///
4438    /// # Errors
4439    /// Returns an error if:
4440    /// - Authentication fails
4441    /// - The HTTP request fails
4442    /// - The server returns an error status
4443    pub async fn lock_asset_assignment(
4444        &self,
4445        asset_uuid: &str,
4446        assignment_id: &str,
4447    ) -> Result<Assignment, Error> {
4448        self.request_json(
4449            Method::PUT,
4450            &["assets", asset_uuid, "assignments", assignment_id, "lock"],
4451            None::<&()>,
4452        )
4453        .await
4454    }
4455
4456    /// Unlocks a specific asset assignment.
4457    ///
4458    /// # Arguments
4459    /// * `asset_uuid` - The UUID of the asset
4460    /// * `assignment_id` - The ID of the assignment to unlock
4461    ///
4462    /// # Errors
4463    /// Returns an error if:
4464    /// - Authentication fails
4465    /// - The HTTP request fails
4466    /// - The server returns an error status
4467    pub async fn unlock_asset_assignment(
4468        &self,
4469        asset_uuid: &str,
4470        assignment_id: &str,
4471    ) -> Result<Assignment, Error> {
4472        self.request_json(
4473            Method::PUT,
4474            &["assets", asset_uuid, "assignments", assignment_id, "unlock"],
4475            None::<&()>,
4476        )
4477        .await
4478    }
4479
4480    /// Adds categories to a registered user.
4481    ///
4482    /// # Arguments
4483    /// * `registered_user_id` - The ID of the registered user
4484    /// * `categories` - A slice of category IDs to add to the user
4485    ///
4486    /// # Errors
4487    /// Returns an error if:
4488    /// - Authentication fails
4489    /// - The HTTP request fails
4490    /// - The server returns an error status
4491    /// - The registered user ID is invalid
4492    /// - Any category ID is invalid
4493    pub async fn add_categories_to_registered_user(
4494        &self,
4495        registered_user_id: i64,
4496        categories: &[i64],
4497    ) -> Result<(), Error> {
4498        let request_body = CategoriesRequest {
4499            categories: categories.to_vec(),
4500        };
4501
4502        self.request_empty(
4503            Method::PUT,
4504            &[
4505                "registered_users",
4506                &registered_user_id.to_string(),
4507                "categories",
4508                "add",
4509            ],
4510            Some(request_body),
4511        )
4512        .await
4513    }
4514
4515    /// Removes categories from a registered user
4516    ///
4517    /// # Arguments
4518    /// * `registered_user_id` - The ID of the registered user
4519    /// * `categories` - A slice of category IDs to remove from the user
4520    ///
4521    /// # Returns
4522    /// Returns `Ok(())` if the categories are successfully removed, or an error if:
4523    /// - Authentication fails
4524    /// - The HTTP request fails
4525    /// - The server returns an error status
4526    /// - The registered user ID is invalid
4527    /// - Any category ID is not associated with the user
4528    pub async fn remove_categories_from_registered_user(
4529        &self,
4530        registered_user_id: i64,
4531        categories: &[i64],
4532    ) -> Result<(), Error> {
4533        let request_body = CategoriesRequest {
4534            categories: categories.to_vec(),
4535        };
4536
4537        self.request_empty(
4538            Method::PUT,
4539            &[
4540                "registered_users",
4541                &registered_user_id.to_string(),
4542                "categories",
4543                "delete",
4544            ],
4545            Some(request_body),
4546        )
4547        .await
4548    }
4549}
4550
4551fn get_amp_api_base_url() -> Result<Url, Error> {
4552    let url_str = env::var("AMP_API_BASE_URL")
4553        .unwrap_or_else(|_| "https://amp-test.blockstream.com/api".to_string());
4554    Url::parse(&url_str).map_err(Error::from)
4555}
4556
4557/// Creates a token strategy based on automatic environment detection
4558///
4559/// This function detects the current environment and creates the appropriate strategy:
4560/// - Mock strategy for mock environments (isolated, no persistence)
4561/// - Live strategy for live environments (full token management)
4562///
4563/// # Arguments
4564/// * `mock_token` - Optional token to use for mock environments
4565///
4566/// # Errors
4567/// Returns an error if strategy creation fails
4568pub async fn create_auto_token_strategy(
4569    mock_token: Option<String>,
4570) -> Result<Box<dyn TokenStrategy>, Error> {
4571    TokenEnvironment::create_auto_strategy(mock_token).await
4572}
4573
4574/// Creates a mock token strategy with the specified token
4575///
4576/// # Arguments
4577/// * `token` - The mock token to use
4578#[must_use]
4579pub fn create_mock_token_strategy(token: String) -> Box<dyn TokenStrategy> {
4580    Box::new(MockTokenStrategy::new(token))
4581}
4582
4583/// Creates a live token strategy with default configuration
4584///
4585/// # Errors
4586/// Returns an error if the `TokenManager` cannot be initialized
4587pub async fn create_live_token_strategy() -> Result<Box<dyn TokenStrategy>, Error> {
4588    let strategy = LiveTokenStrategy::new().await?;
4589    Ok(Box::new(strategy))
4590}
4591
4592/// Creates a token strategy for the specified environment
4593///
4594/// # Arguments
4595/// * `environment` - The target environment
4596/// * `mock_token` - Optional token to use for mock environments
4597///
4598/// # Errors
4599/// Returns an error if strategy creation fails
4600pub async fn create_token_strategy_for_environment(
4601    environment: TokenEnvironment,
4602    mock_token: Option<String>,
4603) -> Result<Box<dyn TokenStrategy>, Error> {
4604    environment.create_strategy(mock_token).await
4605}
4606
4607#[cfg(test)]
4608mod tests {
4609    use super::*;
4610    use tokio;
4611
4612    #[tokio::test]
4613    async fn test_mock_token_strategy_basic_functionality() {
4614        let mock_token = "mock_token_12_345".to_string();
4615        let strategy = MockTokenStrategy::new(mock_token.clone());
4616
4617        // Test get_token returns the mock token
4618        let result = strategy.get_token().await;
4619        assert!(result.is_ok());
4620        assert_eq!(result.unwrap(), mock_token);
4621
4622        // Test strategy type identification
4623        assert_eq!(strategy.strategy_type(), "mock");
4624
4625        // Test persistence is disabled
4626        assert!(!strategy.should_persist());
4627
4628        // Test clear_token is a no-op (should not fail)
4629        let clear_result = strategy.clear_token().await;
4630        assert!(clear_result.is_ok());
4631
4632        // Verify token is still available after clear (since it's a no-op for mock)
4633        let token_after_clear = strategy.get_token().await;
4634        assert!(token_after_clear.is_ok());
4635        assert_eq!(token_after_clear.unwrap(), mock_token);
4636    }
4637
4638    #[tokio::test]
4639    async fn test_mock_token_strategy_isolation() {
4640        let token1 = "token_instance_1".to_string();
4641        let token2 = "token_instance_2".to_string();
4642
4643        let strategy1 = MockTokenStrategy::new(token1.clone());
4644        let strategy2 = MockTokenStrategy::new(token2.clone());
4645
4646        // Test that different instances are isolated
4647        let result1 = strategy1.get_token().await.unwrap();
4648        let result2 = strategy2.get_token().await.unwrap();
4649
4650        assert_eq!(result1, token1);
4651        assert_eq!(result2, token2);
4652        assert_ne!(result1, result2);
4653
4654        // Test that operations on one don't affect the other
4655        let _ = strategy1.clear_token().await;
4656        let result2_after_clear = strategy2.get_token().await.unwrap();
4657        assert_eq!(result2_after_clear, token2);
4658    }
4659
4660    #[tokio::test]
4661    async fn test_live_token_strategy_creation() {
4662        // Test creating a live strategy with global instance
4663        let strategy_result = LiveTokenStrategy::new().await;
4664        assert!(strategy_result.is_ok());
4665
4666        let strategy = strategy_result.unwrap();
4667        assert_eq!(strategy.strategy_type(), "live");
4668        assert!(strategy.should_persist());
4669    }
4670
4671    #[tokio::test]
4672    async fn test_live_token_strategy_with_custom_manager() {
4673        // Create a custom token manager for testing
4674        let config = RetryConfig::for_tests();
4675        let base_url = Url::parse("http://localhost:8080").unwrap();
4676        let mock_token = "test_live_token".to_string();
4677
4678        let token_manager =
4679            Arc::new(TokenManager::with_mock_token(config, base_url, mock_token.clone()).unwrap());
4680
4681        let strategy = LiveTokenStrategy::with_token_manager(token_manager);
4682
4683        // Test strategy properties
4684        assert_eq!(strategy.strategy_type(), "live");
4685        assert!(strategy.should_persist());
4686
4687        // Test token retrieval
4688        let token_result = strategy.get_token().await;
4689        assert!(token_result.is_ok());
4690        assert_eq!(token_result.unwrap(), mock_token);
4691    }
4692
4693    #[tokio::test]
4694    async fn test_live_token_strategy_clear_token() {
4695        // Create a live strategy with a mock token manager
4696        let config = RetryConfig::for_tests();
4697        let base_url = Url::parse("http://localhost:8080").unwrap();
4698        let mock_token = "test_clear_token".to_string();
4699
4700        let token_manager =
4701            Arc::new(TokenManager::with_mock_token(config, base_url, mock_token.clone()).unwrap());
4702
4703        let strategy = LiveTokenStrategy::with_token_manager(token_manager);
4704
4705        // Verify token is available initially
4706        let initial_token = strategy.get_token().await;
4707        assert!(initial_token.is_ok());
4708        assert_eq!(initial_token.unwrap(), mock_token);
4709
4710        // Clear the token
4711        let clear_result = strategy.clear_token().await;
4712        assert!(clear_result.is_ok());
4713
4714        // Note: After clearing, the TokenManager would try to obtain a new token
4715        // In a real scenario, this would fail without proper credentials
4716        // But our mock token manager will still return the same token
4717    }
4718
4719    #[tokio::test]
4720    async fn test_strategy_type_identification() {
4721        let mock_strategy = MockTokenStrategy::new("test_token".to_string());
4722        let live_strategy = LiveTokenStrategy::new().await.unwrap();
4723
4724        // Test that we can identify strategy types for debugging
4725        assert_eq!(mock_strategy.strategy_type(), "mock");
4726        assert_eq!(live_strategy.strategy_type(), "live");
4727
4728        // Test persistence settings
4729        assert!(!mock_strategy.should_persist());
4730        assert!(live_strategy.should_persist());
4731    }
4732
4733    #[tokio::test]
4734    async fn test_strategy_debug_formatting() {
4735        let mock_strategy = MockTokenStrategy::new("debug_test_token".to_string());
4736        let debug_output = format!("{mock_strategy:?}");
4737
4738        // Verify debug output contains expected information
4739        assert!(debug_output.contains("MockTokenStrategy"));
4740        assert!(debug_output.contains("debug_test_token"));
4741    }
4742
4743    // Environment Detection Tests
4744
4745    #[test]
4746    fn test_token_environment_detect_live_via_amp_tests() {
4747        // Set up environment for live test detection
4748        env::set_var("AMP_TESTS", "live");
4749        env::set_var("AMP_USERNAME", "real_user");
4750        env::set_var("AMP_PASSWORD", "real_pass");
4751        env::remove_var("AMP_API_BASE_URL");
4752
4753        let environment = TokenEnvironment::detect();
4754        assert_eq!(environment, TokenEnvironment::Live);
4755
4756        // Clean up
4757        env::remove_var("AMP_TESTS");
4758        env::remove_var("AMP_USERNAME");
4759        env::remove_var("AMP_PASSWORD");
4760    }
4761
4762    #[test]
4763    fn test_token_environment_detect_mock_via_credentials() {
4764        // Set up environment for mock detection via username
4765        env::remove_var("AMP_TESTS");
4766        env::set_var("AMP_USERNAME", "mock_user");
4767        env::set_var("AMP_PASSWORD", "real_pass");
4768        env::remove_var("AMP_API_BASE_URL");
4769
4770        let environment = TokenEnvironment::detect();
4771        assert_eq!(environment, TokenEnvironment::Mock);
4772
4773        // Test mock detection via password
4774        env::set_var("AMP_USERNAME", "real_user");
4775        env::set_var("AMP_PASSWORD", "mock_pass");
4776
4777        let environment = TokenEnvironment::detect();
4778        assert_eq!(environment, TokenEnvironment::Mock);
4779
4780        // Clean up
4781        env::remove_var("AMP_USERNAME");
4782        env::remove_var("AMP_PASSWORD");
4783    }
4784
4785    #[test]
4786    fn test_token_environment_detect_mock_via_base_url() {
4787        // Set up environment for mock detection via localhost URL
4788        env::remove_var("AMP_TESTS");
4789        env::set_var("AMP_USERNAME", "real_user");
4790        env::set_var("AMP_PASSWORD", "real_pass");
4791        env::set_var("AMP_API_BASE_URL", "http://localhost:8080/api");
4792
4793        let environment = TokenEnvironment::detect();
4794        assert_eq!(environment, TokenEnvironment::Mock);
4795
4796        // Test with 127.0.0.1
4797        env::set_var("AMP_API_BASE_URL", "http://127.0.0.1:3000/api");
4798        let environment = TokenEnvironment::detect();
4799        assert_eq!(environment, TokenEnvironment::Mock);
4800
4801        // Test with mock in URL
4802        env::set_var("AMP_API_BASE_URL", "http://mock-server.example.com/api");
4803        let environment = TokenEnvironment::detect();
4804        assert_eq!(environment, TokenEnvironment::Mock);
4805
4806        // Clean up
4807        env::remove_var("AMP_USERNAME");
4808        env::remove_var("AMP_PASSWORD");
4809        env::remove_var("AMP_API_BASE_URL");
4810    }
4811
4812    #[test]
4813    fn test_token_environment_detect_live_via_real_credentials() {
4814        // Set up environment for live detection via real credentials
4815        env::remove_var("AMP_TESTS");
4816        env::set_var("AMP_USERNAME", "real_user");
4817        env::set_var("AMP_PASSWORD", "real_pass");
4818        env::set_var("AMP_API_BASE_URL", "https://amp-test.blockstream.com/api");
4819
4820        let environment = TokenEnvironment::detect();
4821        assert_eq!(environment, TokenEnvironment::Live);
4822
4823        // Clean up
4824        env::remove_var("AMP_USERNAME");
4825        env::remove_var("AMP_PASSWORD");
4826        env::remove_var("AMP_API_BASE_URL");
4827    }
4828
4829    #[test]
4830    fn test_token_environment_detect_mock_fallback() {
4831        // Set up environment with no credentials (fallback to mock)
4832        env::remove_var("AMP_TESTS");
4833        env::remove_var("AMP_USERNAME");
4834        env::remove_var("AMP_PASSWORD");
4835        env::remove_var("AMP_API_BASE_URL");
4836
4837        let environment = TokenEnvironment::detect();
4838        assert_eq!(environment, TokenEnvironment::Mock);
4839    }
4840
4841    #[test]
4842    fn test_has_mock_credentials() {
4843        // Test mock username detection
4844        assert!(TokenEnvironment::has_mock_credentials(
4845            "mock_user",
4846            "real_pass",
4847            ""
4848        ));
4849        assert!(TokenEnvironment::has_mock_credentials(
4850            "Mock_User",
4851            "real_pass",
4852            ""
4853        ));
4854        assert!(TokenEnvironment::has_mock_credentials(
4855            "user_mock",
4856            "real_pass",
4857            ""
4858        ));
4859
4860        // Test mock password detection
4861        assert!(TokenEnvironment::has_mock_credentials(
4862            "real_user",
4863            "mock_pass",
4864            ""
4865        ));
4866        assert!(TokenEnvironment::has_mock_credentials(
4867            "real_user",
4868            "Mock_Pass",
4869            ""
4870        ));
4871        assert!(TokenEnvironment::has_mock_credentials(
4872            "real_user",
4873            "pass_mock",
4874            ""
4875        ));
4876
4877        // Test mock URL detection
4878        assert!(TokenEnvironment::has_mock_credentials(
4879            "real_user",
4880            "real_pass",
4881            "http://localhost:8080"
4882        ));
4883        assert!(TokenEnvironment::has_mock_credentials(
4884            "real_user",
4885            "real_pass",
4886            "http://127.0.0.1:3000"
4887        ));
4888        assert!(TokenEnvironment::has_mock_credentials(
4889            "real_user",
4890            "real_pass",
4891            "http://mock-server.com"
4892        ));
4893        assert!(TokenEnvironment::has_mock_credentials(
4894            "real_user",
4895            "real_pass",
4896            "http://Mock-Server.com"
4897        ));
4898
4899        // Test non-mock credentials
4900        assert!(!TokenEnvironment::has_mock_credentials(
4901            "real_user",
4902            "real_pass",
4903            "https://amp-test.blockstream.com"
4904        ));
4905        assert!(!TokenEnvironment::has_mock_credentials("", "", ""));
4906    }
4907
4908    #[test]
4909    fn test_token_environment_should_persist_tokens() {
4910        assert!(!TokenEnvironment::Mock.should_persist_tokens());
4911        assert!(TokenEnvironment::Live.should_persist_tokens());
4912
4913        // Auto should delegate to detect()
4914        env::set_var("AMP_TESTS", "live");
4915        assert!(TokenEnvironment::Auto.should_persist_tokens());
4916
4917        env::set_var("AMP_USERNAME", "mock_user");
4918        env::set_var("AMP_PASSWORD", "some_password");
4919        env::remove_var("AMP_TESTS");
4920        env::remove_var("AMP_API_BASE_URL");
4921        assert!(!TokenEnvironment::Auto.should_persist_tokens());
4922
4923        // Clean up
4924        env::remove_var("AMP_USERNAME");
4925        env::remove_var("AMP_PASSWORD");
4926    }
4927
4928    #[test]
4929    fn test_token_environment_is_mock_and_is_live() {
4930        assert!(TokenEnvironment::Mock.is_mock());
4931        assert!(!TokenEnvironment::Mock.is_live());
4932
4933        assert!(!TokenEnvironment::Live.is_mock());
4934        assert!(TokenEnvironment::Live.is_live());
4935
4936        // Auto should delegate to detect()
4937        env::set_var("AMP_USERNAME", "mock_user");
4938        env::set_var("AMP_PASSWORD", "some_password");
4939        env::remove_var("AMP_TESTS");
4940        env::remove_var("AMP_API_BASE_URL");
4941        assert!(TokenEnvironment::Auto.is_mock());
4942        assert!(!TokenEnvironment::Auto.is_live());
4943
4944        env::set_var("AMP_TESTS", "live");
4945        assert!(!TokenEnvironment::Auto.is_mock());
4946        assert!(TokenEnvironment::Auto.is_live());
4947
4948        // Clean up
4949        env::remove_var("AMP_USERNAME");
4950        env::remove_var("AMP_PASSWORD");
4951        env::remove_var("AMP_TESTS");
4952    }
4953
4954    #[tokio::test]
4955    async fn test_token_environment_create_strategy_mock() {
4956        let mock_token = "test_mock_token".to_string();
4957        let strategy = TokenEnvironment::Mock
4958            .create_strategy(Some(mock_token.clone()))
4959            .await
4960            .unwrap();
4961
4962        assert_eq!(strategy.strategy_type(), "mock");
4963        assert!(!strategy.should_persist());
4964
4965        let token = strategy.get_token().await.unwrap();
4966        assert_eq!(token, mock_token);
4967    }
4968
4969    #[tokio::test]
4970    async fn test_token_environment_create_strategy_live() {
4971        let strategy = TokenEnvironment::Live.create_strategy(None).await.unwrap();
4972
4973        assert_eq!(strategy.strategy_type(), "live");
4974        assert!(strategy.should_persist());
4975    }
4976
4977    #[tokio::test]
4978    async fn test_token_environment_create_auto_strategy() {
4979        // Test with mock environment - need both username and password for proper detection
4980        env::set_var("AMP_USERNAME", "mock_user");
4981        env::set_var("AMP_PASSWORD", "some_password");
4982        env::remove_var("AMP_TESTS");
4983        env::remove_var("AMP_API_BASE_URL");
4984
4985        let mock_token = "auto_mock_token".to_string();
4986        let strategy = TokenEnvironment::create_auto_strategy(Some(mock_token.clone()))
4987            .await
4988            .unwrap();
4989
4990        assert_eq!(strategy.strategy_type(), "mock");
4991        let token = strategy.get_token().await.unwrap();
4992        assert_eq!(token, mock_token);
4993
4994        // Clean up
4995        env::remove_var("AMP_USERNAME");
4996        env::remove_var("AMP_PASSWORD");
4997    }
4998
4999    #[tokio::test]
5000    async fn test_mock_token_strategy_factory_methods() {
5001        // Test with_default_token
5002        let strategy = MockTokenStrategy::with_default_token();
5003        assert_eq!(strategy.strategy_type(), "mock");
5004        let token = strategy.get_token().await.unwrap();
5005        assert_eq!(token, "mock_token_default");
5006
5007        // Test for_test
5008        let strategy = MockTokenStrategy::for_test("my_test");
5009        let token = strategy.get_token().await.unwrap();
5010        assert_eq!(token, "mock_token_my_test");
5011    }
5012
5013    #[tokio::test]
5014    async fn test_live_token_strategy_factory_methods() {
5015        // Test for_testing
5016        let strategy = LiveTokenStrategy::for_testing().await.unwrap();
5017        assert_eq!(strategy.strategy_type(), "live");
5018        assert!(strategy.should_persist());
5019    }
5020
5021    #[tokio::test]
5022    async fn test_standalone_factory_functions() {
5023        // Test create_mock_token_strategy
5024        let mock_token = "standalone_mock".to_string();
5025        let strategy = create_mock_token_strategy(mock_token.clone());
5026        assert_eq!(strategy.strategy_type(), "mock");
5027        let token = strategy.get_token().await.unwrap();
5028        assert_eq!(token, mock_token);
5029
5030        // Test create_live_token_strategy
5031        let strategy = create_live_token_strategy().await.unwrap();
5032        assert_eq!(strategy.strategy_type(), "live");
5033
5034        // Test create_auto_token_strategy with mock environment
5035        env::set_var("AMP_USERNAME", "mock_user");
5036        env::set_var("AMP_PASSWORD", "some_password");
5037        env::remove_var("AMP_TESTS");
5038        env::remove_var("AMP_API_BASE_URL");
5039
5040        let auto_mock_token = "auto_standalone_mock".to_string();
5041        let strategy = create_auto_token_strategy(Some(auto_mock_token.clone()))
5042            .await
5043            .unwrap();
5044        assert_eq!(strategy.strategy_type(), "mock");
5045        let token = strategy.get_token().await.unwrap();
5046        assert_eq!(token, auto_mock_token);
5047
5048        // Test create_token_strategy_for_environment
5049        let env_mock_token = "env_mock".to_string();
5050        let strategy = create_token_strategy_for_environment(
5051            TokenEnvironment::Mock,
5052            Some(env_mock_token.clone()),
5053        )
5054        .await
5055        .unwrap();
5056        assert_eq!(strategy.strategy_type(), "mock");
5057        let token = strategy.get_token().await.unwrap();
5058        assert_eq!(token, env_mock_token);
5059
5060        // Clean up
5061        env::remove_var("AMP_USERNAME");
5062        env::remove_var("AMP_PASSWORD");
5063    }
5064
5065    #[test]
5066    fn test_environment_detection_with_various_credential_combinations() {
5067        // Test case 1: AMP_TESTS=live overrides everything
5068        env::set_var("AMP_TESTS", "live");
5069        env::set_var("AMP_USERNAME", "mock_user");
5070        env::set_var("AMP_PASSWORD", "mock_pass");
5071        env::set_var("AMP_API_BASE_URL", "http://localhost:8080");
5072        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Live);
5073
5074        // Test case 2: Mock username with real password and URL
5075        env::remove_var("AMP_TESTS");
5076        env::set_var("AMP_USERNAME", "mock_user");
5077        env::set_var("AMP_PASSWORD", "real_password");
5078        env::set_var("AMP_API_BASE_URL", "https://amp-test.blockstream.com/api");
5079        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
5080
5081        // Test case 3: Real username with mock password
5082        env::set_var("AMP_USERNAME", "real_user");
5083        env::set_var("AMP_PASSWORD", "mock_password");
5084        env::set_var("AMP_API_BASE_URL", "https://amp-test.blockstream.com/api");
5085        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
5086
5087        // Test case 4: Real credentials with localhost URL
5088        env::set_var("AMP_USERNAME", "real_user");
5089        env::set_var("AMP_PASSWORD", "real_password");
5090        env::set_var("AMP_API_BASE_URL", "http://localhost:3000/api");
5091        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
5092
5093        // Test case 5: All real credentials
5094        env::set_var("AMP_USERNAME", "real_user");
5095        env::set_var("AMP_PASSWORD", "real_password");
5096        env::set_var("AMP_API_BASE_URL", "https://amp-test.blockstream.com/api");
5097        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Live);
5098
5099        // Test case 6: Empty credentials
5100        env::remove_var("AMP_USERNAME");
5101        env::remove_var("AMP_PASSWORD");
5102        env::remove_var("AMP_API_BASE_URL");
5103        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
5104
5105        // Test case 7: Only username set
5106        env::set_var("AMP_USERNAME", "real_user");
5107        env::remove_var("AMP_PASSWORD");
5108        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
5109
5110        // Test case 8: Only password set
5111        env::remove_var("AMP_USERNAME");
5112        env::set_var("AMP_PASSWORD", "real_password");
5113        assert_eq!(TokenEnvironment::detect(), TokenEnvironment::Mock);
5114
5115        // Clean up all environment variables
5116        env::remove_var("AMP_TESTS");
5117        env::remove_var("AMP_USERNAME");
5118        env::remove_var("AMP_PASSWORD");
5119        env::remove_var("AMP_API_BASE_URL");
5120    }
5121}