tlq_client/
client.rs

1use crate::{
2    config::{Config, ConfigBuilder},
3    error::{Result, TlqError},
4    message::*,
5    retry::RetryStrategy,
6};
7use serde::{de::DeserializeOwned, Serialize};
8use std::time::Duration;
9use tokio::io::{AsyncReadExt, AsyncWriteExt};
10use tokio::net::TcpStream;
11use tokio::time::timeout;
12use uuid::Uuid;
13
14const MAX_MESSAGE_SIZE: usize = 65536;
15
16/// The main client for interacting with TLQ (Tiny Little Queue) servers.
17///
18/// `TlqClient` provides an async, type-safe interface for all TLQ operations including
19/// adding messages, retrieving messages, and managing queue state. The client handles
20/// automatic retry with exponential backoff for transient failures.
21///
22/// # Examples
23///
24/// Basic usage:
25/// ```no_run
26/// use tlq_client::TlqClient;
27///
28/// #[tokio::main]
29/// async fn main() -> Result<(), tlq_client::TlqError> {
30///     let client = TlqClient::new("localhost", 1337)?;
31///     
32///     // Add a message
33///     let message = client.add_message("Hello, World!").await?;
34///     println!("Added message: {}", message.id);
35///     
36///     // Get messages
37///     let messages = client.get_messages(1).await?;
38///     if let Some(msg) = messages.first() {
39///         println!("Retrieved: {}", msg.body);
40///     }
41///     
42///     Ok(())
43/// }
44/// ```
45pub struct TlqClient {
46    config: Config,
47    base_url: String,
48}
49
50impl TlqClient {
51    /// Creates a new TLQ client with default configuration.
52    ///
53    /// This is the simplest way to create a client, using default values for
54    /// timeout (30s), max retries (3), and retry delay (100ms).
55    ///
56    /// # Arguments
57    ///
58    /// * `host` - The hostname or IP address of the TLQ server
59    /// * `port` - The port number of the TLQ server
60    ///
61    /// # Examples
62    ///
63    /// ```no_run
64    /// use tlq_client::TlqClient;
65    ///
66    /// # fn example() -> Result<(), tlq_client::TlqError> {
67    /// let client = TlqClient::new("localhost", 1337)?;
68    /// # Ok(())
69    /// # }
70    /// ```
71    ///
72    /// # Errors
73    ///
74    /// Currently this method always returns `Ok`, but the `Result` is preserved
75    /// for future compatibility.
76    pub fn new(host: impl Into<String>, port: u16) -> Result<Self> {
77        let config = ConfigBuilder::new().host(host).port(port).build();
78
79        Ok(Self::with_config(config))
80    }
81
82    /// Creates a new TLQ client with custom configuration.
83    ///
84    /// Use this method when you need to customize timeout, retry behavior,
85    /// or other client settings.
86    ///
87    /// # Arguments
88    ///
89    /// * `config` - A [`Config`] instance with your desired settings
90    ///
91    /// # Examples
92    ///
93    /// ```no_run
94    /// use tlq_client::{TlqClient, ConfigBuilder};
95    /// use std::time::Duration;
96    ///
97    /// # fn example() {
98    /// let config = ConfigBuilder::new()
99    ///     .host("queue.example.com")
100    ///     .port(8080)
101    ///     .timeout(Duration::from_secs(5))
102    ///     .max_retries(2)
103    ///     .build();
104    ///
105    /// let client = TlqClient::with_config(config);
106    /// # }
107    /// ```
108    pub fn with_config(config: Config) -> Self {
109        let base_url = format!("{}:{}", config.host, config.port);
110        Self { config, base_url }
111    }
112
113    /// Returns a [`ConfigBuilder`] for creating custom configurations.
114    ///
115    /// This is a convenience method that's equivalent to [`ConfigBuilder::new()`].
116    ///
117    /// # Examples
118    ///
119    /// ```no_run
120    /// use tlq_client::TlqClient;
121    /// use std::time::Duration;
122    ///
123    /// # fn example() {
124    /// let client = TlqClient::with_config(
125    ///     TlqClient::builder()
126    ///         .host("localhost")
127    ///         .port(1337)
128    ///         .timeout(Duration::from_secs(10))
129    ///         .build()
130    /// );
131    /// # }
132    /// ```
133    pub fn builder() -> ConfigBuilder {
134        ConfigBuilder::new()
135    }
136
137    async fn request<T, R>(&self, endpoint: &str, body: &T) -> Result<R>
138    where
139        T: Serialize,
140        R: DeserializeOwned,
141    {
142        let retry_strategy = RetryStrategy::new(self.config.max_retries, self.config.retry_delay);
143
144        retry_strategy
145            .execute(|| async { self.single_request(endpoint, body).await })
146            .await
147    }
148
149    async fn single_request<T, R>(&self, endpoint: &str, body: &T) -> Result<R>
150    where
151        T: Serialize,
152        R: DeserializeOwned,
153    {
154        let json_body = serde_json::to_vec(body)?;
155
156        let request = format!(
157            "POST {} HTTP/1.1\r\n\
158             Host: {}\r\n\
159             Content-Type: application/json\r\n\
160             Content-Length: {}\r\n\
161             Connection: close\r\n\
162             \r\n",
163            endpoint,
164            self.base_url,
165            json_body.len()
166        );
167
168        let mut stream = timeout(self.config.timeout, TcpStream::connect(&self.base_url))
169            .await
170            .map_err(|_| TlqError::Timeout(self.config.timeout.as_millis() as u64))?
171            .map_err(|e| TlqError::Connection(e.to_string()))?;
172
173        stream.write_all(request.as_bytes()).await?;
174        stream.write_all(&json_body).await?;
175        stream.flush().await?;
176
177        let mut response = Vec::new();
178        stream.read_to_end(&mut response).await?;
179
180        let response_str = String::from_utf8_lossy(&response);
181        let body = Self::parse_http_response(&response_str)?;
182        serde_json::from_str(body).map_err(Into::into)
183    }
184
185    /// Performs a health check against the TLQ server.
186    ///
187    /// This method sends a GET request to the `/hello` endpoint to verify
188    /// that the server is responding. It uses a fixed 5-second timeout
189    /// regardless of the client's configured timeout.
190    ///
191    /// # Returns
192    ///
193    /// * `Ok(true)` if the server responds with HTTP 200 OK
194    /// * `Ok(false)` if the server responds but not with 200 OK
195    /// * `Err` if there's a connection error or timeout
196    ///
197    /// # Examples
198    ///
199    /// ```no_run
200    /// use tlq_client::TlqClient;
201    ///
202    /// #[tokio::main]
203    /// async fn main() -> Result<(), tlq_client::TlqError> {
204    ///     let client = TlqClient::new("localhost", 1337)?;
205    ///
206    ///     if client.health_check().await? {
207    ///         println!("Server is healthy");
208    ///     } else {
209    ///         println!("Server is not responding correctly");
210    ///     }
211    ///     
212    ///     Ok(())
213    /// }
214    /// ```
215    ///
216    /// # Errors
217    ///
218    /// Returns [`TlqError::Connection`] for network issues, or [`TlqError::Timeout`]
219    /// if the server doesn't respond within 5 seconds.
220    pub async fn health_check(&self) -> Result<bool> {
221        let mut stream = timeout(Duration::from_secs(5), TcpStream::connect(&self.base_url))
222            .await
223            .map_err(|_| TlqError::Timeout(5000))?
224            .map_err(|e| TlqError::Connection(e.to_string()))?;
225
226        let request = format!(
227            "GET /hello HTTP/1.1\r\n\
228             Host: {}\r\n\
229             Connection: close\r\n\
230             \r\n",
231            self.base_url
232        );
233
234        stream.write_all(request.as_bytes()).await?;
235        stream.flush().await?;
236
237        let mut response = Vec::new();
238        stream.read_to_end(&mut response).await?;
239
240        let response_str = String::from_utf8_lossy(&response);
241        Ok(response_str.contains("200 OK"))
242    }
243
244    /// Adds a new message to the TLQ server.
245    ///
246    /// The message will be assigned a UUID v7 identifier and placed in the queue
247    /// with state [`MessageState::Ready`]. Messages have a maximum size limit of 64KB.
248    ///
249    /// # Arguments
250    ///
251    /// * `body` - The message content (any type that can be converted to String)
252    ///
253    /// # Returns
254    ///
255    /// Returns the created [`Message`] with its assigned ID and metadata.
256    ///
257    /// # Examples
258    ///
259    /// ```no_run
260    /// use tlq_client::TlqClient;
261    ///
262    /// #[tokio::main]
263    /// async fn main() -> Result<(), tlq_client::TlqError> {
264    ///     let client = TlqClient::new("localhost", 1337)?;
265    ///
266    ///     // Add a simple string message
267    ///     let message = client.add_message("Hello, World!").await?;
268    ///     println!("Created message {} with body: {}", message.id, message.body);
269    ///
270    ///     // Add a formatted message
271    ///     let user_data = "important data";
272    ///     let message = client.add_message(format!("Processing: {}", user_data)).await?;
273    ///     
274    ///     Ok(())
275    /// }
276    /// ```
277    ///
278    /// # Errors
279    ///
280    /// * [`TlqError::MessageTooLarge`] if the message exceeds 64KB (65,536 bytes)
281    /// * [`TlqError::Connection`] for network connectivity issues
282    /// * [`TlqError::Timeout`] if the request times out
283    /// * [`TlqError::Server`] for server-side errors (4xx/5xx HTTP responses)
284    pub async fn add_message(&self, body: impl Into<String>) -> Result<Message> {
285        let body = body.into();
286
287        if body.len() > MAX_MESSAGE_SIZE {
288            return Err(TlqError::MessageTooLarge { size: body.len() });
289        }
290
291        let request = AddMessageRequest { body };
292        let message: Message = self.request("/add", &request).await?;
293        Ok(message)
294    }
295
296    /// Retrieves multiple messages from the TLQ server.
297    ///
298    /// This method fetches up to `count` messages from the queue. Messages are returned
299    /// in the order they were added and their state is changed to [`MessageState::Processing`].
300    /// The server may return fewer messages than requested if there are not enough
301    /// messages in the queue.
302    ///
303    /// # Arguments
304    ///
305    /// * `count` - Maximum number of messages to retrieve (must be greater than 0)
306    ///
307    /// # Returns
308    ///
309    /// Returns a vector of [`Message`] objects. The vector may be empty if no messages
310    /// are available in the queue.
311    ///
312    /// # Examples
313    ///
314    /// ```no_run
315    /// use tlq_client::TlqClient;
316    ///
317    /// #[tokio::main]
318    /// async fn main() -> Result<(), tlq_client::TlqError> {
319    ///     let client = TlqClient::new("localhost", 1337)?;
320    ///
321    ///     // Get up to 5 messages from the queue
322    ///     let messages = client.get_messages(5).await?;
323    ///     
324    ///     for message in messages {
325    ///         println!("Processing message {}: {}", message.id, message.body);
326    ///         
327    ///         // Process the message...
328    ///         
329    ///         // Delete when done
330    ///         client.delete_message(message.id).await?;
331    ///     }
332    ///     
333    ///     Ok(())
334    /// }
335    /// ```
336    ///
337    /// # Errors
338    ///
339    /// * [`TlqError::Validation`] if count is 0
340    /// * [`TlqError::Connection`] for network connectivity issues  
341    /// * [`TlqError::Timeout`] if the request times out
342    /// * [`TlqError::Server`] for server-side errors (4xx/5xx HTTP responses)
343    pub async fn get_messages(&self, count: u32) -> Result<Vec<Message>> {
344        if count == 0 {
345            return Err(TlqError::Validation(
346                "Count must be greater than 0".to_string(),
347            ));
348        }
349
350        let request = GetMessagesRequest { count };
351        let messages: Vec<Message> = self.request("/get", &request).await?;
352        Ok(messages)
353    }
354
355    /// Retrieves a single message from the TLQ server.
356    ///
357    /// This is a convenience method equivalent to calling [`get_messages(1)`](Self::get_messages)
358    /// and taking the first result. If no messages are available, returns `None`.
359    ///
360    /// # Returns
361    ///
362    /// * `Ok(Some(message))` if a message was retrieved
363    /// * `Ok(None)` if no messages are available in the queue
364    /// * `Err` for connection or server errors
365    ///
366    /// # Examples
367    ///
368    /// ```no_run
369    /// use tlq_client::TlqClient;
370    ///
371    /// #[tokio::main]
372    /// async fn main() -> Result<(), tlq_client::TlqError> {
373    ///     let client = TlqClient::new("localhost", 1337)?;
374    ///
375    ///     // Get a single message
376    ///     match client.get_message().await? {
377    ///         Some(message) => {
378    ///             println!("Got message: {}", message.body);
379    ///             client.delete_message(message.id).await?;
380    ///         }
381    ///         None => println!("No messages available"),
382    ///     }
383    ///     
384    ///     Ok(())
385    /// }
386    /// ```
387    ///
388    /// # Errors
389    ///
390    /// * [`TlqError::Connection`] for network connectivity issues
391    /// * [`TlqError::Timeout`] if the request times out  
392    /// * [`TlqError::Server`] for server-side errors (4xx/5xx HTTP responses)
393    pub async fn get_message(&self) -> Result<Option<Message>> {
394        let messages = self.get_messages(1).await?;
395        Ok(messages.into_iter().next())
396    }
397
398    /// Deletes a single message from the TLQ server.
399    ///
400    /// This is a convenience method that calls [`delete_messages`](Self::delete_messages)
401    /// with a single message ID.
402    ///
403    /// # Arguments
404    ///
405    /// * `id` - The UUID of the message to delete
406    ///
407    /// # Returns
408    ///
409    /// Returns a string indicating the result of the operation (typically "Success" or a count).
410    ///
411    /// # Examples
412    ///
413    /// ```no_run
414    /// use tlq_client::TlqClient;
415    ///
416    /// #[tokio::main]
417    /// async fn main() -> Result<(), tlq_client::TlqError> {
418    ///     let client = TlqClient::new("localhost", 1337)?;
419    ///
420    ///     if let Some(message) = client.get_message().await? {
421    ///         let result = client.delete_message(message.id).await?;
422    ///         println!("Delete result: {}", result);
423    ///     }
424    ///     
425    ///     Ok(())
426    /// }
427    /// ```
428    ///
429    /// # Errors
430    ///
431    /// * [`TlqError::Connection`] for network connectivity issues
432    /// * [`TlqError::Timeout`] if the request times out
433    /// * [`TlqError::Server`] for server-side errors (4xx/5xx HTTP responses)
434    pub async fn delete_message(&self, id: Uuid) -> Result<String> {
435        self.delete_messages(&[id]).await
436    }
437
438    /// Deletes multiple messages from the TLQ server.
439    ///
440    /// This method removes the specified messages from the queue permanently.
441    /// Messages can be in any state when deleted.
442    ///
443    /// # Arguments
444    ///
445    /// * `ids` - A slice of message UUIDs to delete (must not be empty)
446    ///
447    /// # Returns
448    ///
449    /// Returns a string indicating the number of messages deleted or "Success".
450    ///
451    /// # Examples
452    ///
453    /// ```no_run
454    /// use tlq_client::TlqClient;
455    ///
456    /// #[tokio::main]
457    /// async fn main() -> Result<(), tlq_client::TlqError> {
458    ///     let client = TlqClient::new("localhost", 1337)?;
459    ///
460    ///     let messages = client.get_messages(3).await?;
461    ///     if !messages.is_empty() {
462    ///         let ids: Vec<_> = messages.iter().map(|m| m.id).collect();
463    ///         let result = client.delete_messages(&ids).await?;
464    ///         println!("Deleted {} messages", result);
465    ///     }
466    ///     
467    ///     Ok(())
468    /// }
469    /// ```
470    ///
471    /// # Errors
472    ///
473    /// * [`TlqError::Validation`] if the `ids` slice is empty
474    /// * [`TlqError::Connection`] for network connectivity issues
475    /// * [`TlqError::Timeout`] if the request times out
476    /// * [`TlqError::Server`] for server-side errors (4xx/5xx HTTP responses)
477    pub async fn delete_messages(&self, ids: &[Uuid]) -> Result<String> {
478        if ids.is_empty() {
479            return Err(TlqError::Validation("No message IDs provided".to_string()));
480        }
481
482        let request = DeleteMessagesRequest { ids: ids.to_vec() };
483        let response: String = self.request("/delete", &request).await?;
484        Ok(response)
485    }
486
487    /// Retries a single failed message on the TLQ server.
488    ///
489    /// This is a convenience method that calls [`retry_messages`](Self::retry_messages)
490    /// with a single message ID. The message state will be changed from
491    /// [`MessageState::Failed`] back to [`MessageState::Ready`].
492    ///
493    /// # Arguments
494    ///
495    /// * `id` - The UUID of the message to retry
496    ///
497    /// # Returns
498    ///
499    /// Returns a string indicating the result of the operation (typically "Success" or a count).
500    ///
501    /// # Examples
502    ///
503    /// ```no_run
504    /// use tlq_client::{TlqClient, MessageState};
505    ///
506    /// #[tokio::main]
507    /// async fn main() -> Result<(), tlq_client::TlqError> {
508    ///     let client = TlqClient::new("localhost", 1337)?;
509    ///
510    ///     // Find failed messages and retry them
511    ///     let messages = client.get_messages(10).await?;
512    ///     for message in messages {
513    ///         if message.state == MessageState::Failed {
514    ///             let result = client.retry_message(message.id).await?;
515    ///             println!("Retry result: {}", result);
516    ///         }
517    ///     }
518    ///     
519    ///     Ok(())
520    /// }
521    /// ```
522    ///
523    /// # Errors
524    ///
525    /// * [`TlqError::Connection`] for network connectivity issues
526    /// * [`TlqError::Timeout`] if the request times out
527    /// * [`TlqError::Server`] for server-side errors (4xx/5xx HTTP responses)
528    pub async fn retry_message(&self, id: Uuid) -> Result<String> {
529        self.retry_messages(&[id]).await
530    }
531
532    /// Retries multiple failed messages on the TLQ server.
533    ///
534    /// This method changes the state of the specified messages from [`MessageState::Failed`]
535    /// back to [`MessageState::Ready`], making them available for processing again.
536    /// The retry count for each message will be incremented.
537    ///
538    /// # Arguments
539    ///
540    /// * `ids` - A slice of message UUIDs to retry (must not be empty)
541    ///
542    /// # Returns
543    ///
544    /// Returns a string indicating the number of messages retried or "Success".
545    ///
546    /// # Examples
547    ///
548    /// ```no_run
549    /// use tlq_client::{TlqClient, MessageState};
550    ///
551    /// #[tokio::main]
552    /// async fn main() -> Result<(), tlq_client::TlqError> {
553    ///     let client = TlqClient::new("localhost", 1337)?;
554    ///
555    ///     // Get all messages and retry the failed ones
556    ///     let messages = client.get_messages(100).await?;
557    ///     let failed_ids: Vec<_> = messages
558    ///         .iter()
559    ///         .filter(|m| m.state == MessageState::Failed)
560    ///         .map(|m| m.id)
561    ///         .collect();
562    ///
563    ///     if !failed_ids.is_empty() {
564    ///         let result = client.retry_messages(&failed_ids).await?;
565    ///         println!("Retried {} failed messages", result);
566    ///     }
567    ///     
568    ///     Ok(())
569    /// }
570    /// ```
571    ///
572    /// # Errors
573    ///
574    /// * [`TlqError::Validation`] if the `ids` slice is empty
575    /// * [`TlqError::Connection`] for network connectivity issues
576    /// * [`TlqError::Timeout`] if the request times out
577    /// * [`TlqError::Server`] for server-side errors (4xx/5xx HTTP responses)
578    pub async fn retry_messages(&self, ids: &[Uuid]) -> Result<String> {
579        if ids.is_empty() {
580            return Err(TlqError::Validation("No message IDs provided".to_string()));
581        }
582
583        let request = RetryMessagesRequest { ids: ids.to_vec() };
584        let response: String = self.request("/retry", &request).await?;
585        Ok(response)
586    }
587
588    /// Removes all messages from the TLQ server queue.
589    ///
590    /// This method permanently deletes all messages in the queue regardless of their state.
591    /// Use with caution as this operation cannot be undone.
592    ///
593    /// # Returns
594    ///
595    /// Returns a string indicating the result of the operation (typically "Success").
596    ///
597    /// # Examples
598    ///
599    /// ```no_run
600    /// use tlq_client::TlqClient;
601    ///
602    /// #[tokio::main]
603    /// async fn main() -> Result<(), tlq_client::TlqError> {
604    ///     let client = TlqClient::new("localhost", 1337)?;
605    ///
606    ///     // Clear all messages from the queue
607    ///     let result = client.purge_queue().await?;
608    ///     println!("Purge result: {}", result);
609    ///     
610    ///     Ok(())
611    /// }
612    /// ```
613    ///
614    /// # Errors
615    ///
616    /// * [`TlqError::Connection`] for network connectivity issues
617    /// * [`TlqError::Timeout`] if the request times out
618    /// * [`TlqError::Server`] for server-side errors (4xx/5xx HTTP responses)
619    pub async fn purge_queue(&self) -> Result<String> {
620        let response: String = self.request("/purge", &serde_json::json!({})).await?;
621        Ok(response)
622    }
623
624    // Helper function to parse HTTP response - extracted for testing
625    fn parse_http_response(response: &str) -> Result<&str> {
626        if let Some(body_start) = response.find("\r\n\r\n") {
627            let headers = &response[..body_start];
628            let body = &response[body_start + 4..];
629
630            if let Some(status_line) = headers.lines().next() {
631                let parts: Vec<&str> = status_line.split_whitespace().collect();
632                if parts.len() >= 2 {
633                    if let Ok(status_code) = parts[1].parse::<u16>() {
634                        if status_code >= 400 {
635                            return Err(TlqError::Server {
636                                status: status_code,
637                                message: body.to_string(),
638                            });
639                        }
640                    }
641                }
642            }
643
644            Ok(body)
645        } else {
646            Err(TlqError::Connection("Invalid HTTP response".to_string()))
647        }
648    }
649}
650
651#[cfg(test)]
652mod tests {
653    use super::*;
654
655    #[test]
656    fn test_parse_http_response_success() {
657        let response =
658            "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n{\"message\":\"success\"}";
659
660        let result = TlqClient::parse_http_response(response);
661        assert!(result.is_ok());
662        assert_eq!(result.unwrap(), "{\"message\":\"success\"}");
663    }
664
665    #[test]
666    fn test_parse_http_response_server_error() {
667        let response = "HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain\r\n\r\nInternal server error occurred";
668
669        let result = TlqClient::parse_http_response(response);
670        match result {
671            Err(TlqError::Server { status, message }) => {
672                assert_eq!(status, 500);
673                assert_eq!(message, "Internal server error occurred");
674            }
675            _ => panic!("Expected server error"),
676        }
677    }
678
679    #[test]
680    fn test_parse_http_response_client_error() {
681        let response = "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\n\r\nBad request";
682
683        let result = TlqClient::parse_http_response(response);
684        match result {
685            Err(TlqError::Server { status, message }) => {
686                assert_eq!(status, 400);
687                assert_eq!(message, "Bad request");
688            }
689            _ => panic!("Expected client error"),
690        }
691    }
692
693    #[test]
694    fn test_parse_http_response_no_headers_separator() {
695        let response =
696            "HTTP/1.1 200 OK\nContent-Type: application/json\n{\"incomplete\":\"response\"}";
697
698        let result = TlqClient::parse_http_response(response);
699        match result {
700            Err(TlqError::Connection(msg)) => {
701                assert_eq!(msg, "Invalid HTTP response");
702            }
703            _ => panic!("Expected connection error"),
704        }
705    }
706
707    #[test]
708    fn test_parse_http_response_malformed_status_line() {
709        let response = "INVALID_STATUS_LINE\r\n\r\n{\"data\":\"test\"}";
710
711        let result = TlqClient::parse_http_response(response);
712        // Should still succeed because we only check if parts.len() >= 2 and parse fails gracefully
713        assert!(result.is_ok());
714        assert_eq!(result.unwrap(), "{\"data\":\"test\"}");
715    }
716
717    #[test]
718    fn test_parse_http_response_empty_body() {
719        let response = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
720
721        let result = TlqClient::parse_http_response(response);
722        assert!(result.is_ok());
723        assert_eq!(result.unwrap(), "");
724    }
725
726    #[test]
727    fn test_parse_http_response_with_extra_headers() {
728        let response = "HTTP/1.1 201 Created\r\nContent-Type: application/json\r\nServer: TLQ/1.0\r\nConnection: close\r\n\r\n{\"id\":\"123\",\"status\":\"created\"}";
729
730        let result = TlqClient::parse_http_response(response);
731        assert!(result.is_ok());
732        assert_eq!(result.unwrap(), "{\"id\":\"123\",\"status\":\"created\"}");
733    }
734
735    #[test]
736    fn test_parse_http_response_status_code_edge_cases() {
737        // Test various status codes around the 400 boundary
738
739        // 399 should be success (< 400)
740        let response_399 = "HTTP/1.1 399 Custom Success\r\n\r\n{\"ok\":true}";
741        let result = TlqClient::parse_http_response(response_399);
742        assert!(result.is_ok());
743
744        // 400 should be error (>= 400)
745        let response_400 = "HTTP/1.1 400 Bad Request\r\n\r\nBad request";
746        let result = TlqClient::parse_http_response(response_400);
747        assert!(matches!(result, Err(TlqError::Server { status: 400, .. })));
748
749        // 599 should be error
750        let response_599 = "HTTP/1.1 599 Custom Error\r\n\r\nCustom error";
751        let result = TlqClient::parse_http_response(response_599);
752        assert!(matches!(result, Err(TlqError::Server { status: 599, .. })));
753    }
754
755    #[test]
756    fn test_max_message_size_constant() {
757        assert_eq!(MAX_MESSAGE_SIZE, 65536);
758    }
759
760    #[test]
761    fn test_client_creation() {
762        let client = TlqClient::new("test-host", 9999);
763        assert!(client.is_ok());
764
765        let client = client.unwrap();
766        assert_eq!(client.base_url, "test-host:9999");
767    }
768
769    #[test]
770    fn test_client_with_config() {
771        let config = Config {
772            host: "custom-host".to_string(),
773            port: 8080,
774            timeout: Duration::from_secs(10),
775            max_retries: 5,
776            retry_delay: Duration::from_millis(200),
777        };
778
779        let client = TlqClient::with_config(config);
780        assert_eq!(client.base_url, "custom-host:8080");
781        assert_eq!(client.config.max_retries, 5);
782        assert_eq!(client.config.timeout, Duration::from_secs(10));
783    }
784
785    #[test]
786    fn test_message_size_validation() {
787        let _client = TlqClient::new("localhost", 1337).unwrap();
788
789        // Test exact limit
790        let message_at_limit = "x".repeat(MAX_MESSAGE_SIZE);
791        let result = std::panic::catch_unwind(|| {
792            // We can't actually test async methods in sync tests without tokio,
793            // but we can verify the constant is correct
794            assert_eq!(message_at_limit.len(), MAX_MESSAGE_SIZE);
795        });
796        assert!(result.is_ok());
797
798        // Test over limit
799        let message_over_limit = "x".repeat(MAX_MESSAGE_SIZE + 1);
800        assert_eq!(message_over_limit.len(), MAX_MESSAGE_SIZE + 1);
801    }
802
803    #[tokio::test]
804    async fn test_add_message_size_validation() {
805        let client = TlqClient::new("localhost", 1337).unwrap();
806
807        // Test message at exact size limit (should be rejected because it's over the limit)
808        let large_message = "x".repeat(MAX_MESSAGE_SIZE + 1);
809        let result = client.add_message(large_message).await;
810
811        match result {
812            Err(TlqError::MessageTooLarge { size }) => {
813                assert_eq!(size, MAX_MESSAGE_SIZE + 1);
814            }
815            _ => panic!("Expected MessageTooLarge error"),
816        }
817
818        // Test empty message (should be valid)
819        let empty_message = "";
820        // We can't actually test without a server, but we can verify it passes size validation
821        assert!(empty_message.len() <= MAX_MESSAGE_SIZE);
822
823        // Test message exactly at limit (should be valid)
824        let max_message = "x".repeat(MAX_MESSAGE_SIZE);
825        // Size check should pass
826        assert_eq!(max_message.len(), MAX_MESSAGE_SIZE);
827    }
828
829    #[tokio::test]
830    async fn test_get_messages_validation() {
831        let client = TlqClient::new("localhost", 1337).unwrap();
832
833        // Test zero count (should be rejected)
834        let result = client.get_messages(0).await;
835        match result {
836            Err(TlqError::Validation(msg)) => {
837                assert_eq!(msg, "Count must be greater than 0");
838            }
839            _ => panic!("Expected validation error for zero count"),
840        }
841
842        // Test valid counts - these should pass without validation errors
843        let _ = client.get_messages(1).await; // Should be valid
844        let _ = client.get_messages(100).await; // Should be valid
845        let _ = client.get_messages(u32::MAX).await; // Should be valid
846    }
847
848    #[tokio::test]
849    async fn test_delete_messages_validation() {
850        let client = TlqClient::new("localhost", 1337).unwrap();
851
852        // Test empty IDs array
853        let result = client.delete_messages(&[]).await;
854        match result {
855            Err(TlqError::Validation(msg)) => {
856                assert_eq!(msg, "No message IDs provided");
857            }
858            _ => panic!("Expected validation error for empty IDs"),
859        }
860
861        // Test delete_message (single ID) - should not have validation issue
862        use uuid::Uuid;
863        let test_id = Uuid::now_v7();
864        // We can't test the actual call without a server, but we can verify
865        // it would call delete_messages with a single-item array
866        assert!(!vec![test_id].is_empty());
867    }
868
869    #[tokio::test]
870    async fn test_retry_messages_validation() {
871        let client = TlqClient::new("localhost", 1337).unwrap();
872
873        // Test empty IDs array
874        let result = client.retry_messages(&[]).await;
875        match result {
876            Err(TlqError::Validation(msg)) => {
877                assert_eq!(msg, "No message IDs provided");
878            }
879            _ => panic!("Expected validation error for empty IDs"),
880        }
881
882        // Test retry_message (single ID) - should not have validation issue
883        use uuid::Uuid;
884        let test_id = Uuid::now_v7();
885        // We can't test the actual call without a server, but we can verify
886        // it would call retry_messages with a single-item array
887        assert!(!vec![test_id].is_empty());
888    }
889
890    #[test]
891    fn test_client_builder_edge_cases() {
892        // Test builder with minimum values
893        let config = TlqClient::builder()
894            .host("")
895            .port(0)
896            .timeout_ms(0)
897            .max_retries(0)
898            .retry_delay_ms(0)
899            .build();
900
901        let client = TlqClient::with_config(config);
902        assert_eq!(client.base_url, ":0");
903        assert_eq!(client.config.max_retries, 0);
904        assert_eq!(client.config.timeout, Duration::from_millis(0));
905
906        // Test builder with maximum reasonable values
907        let config = TlqClient::builder()
908            .host("very-long-hostname-that-might-be-used-in-some-environments")
909            .port(65535)
910            .timeout_ms(600000) // 10 minutes
911            .max_retries(100)
912            .retry_delay_ms(10000) // 10 seconds
913            .build();
914
915        let client = TlqClient::with_config(config);
916        assert!(client.base_url.contains("very-long-hostname"));
917        assert_eq!(client.config.max_retries, 100);
918        assert_eq!(client.config.timeout, Duration::from_secs(600));
919    }
920
921    #[test]
922    fn test_config_validation() {
923        use crate::config::ConfigBuilder;
924        use std::time::Duration;
925
926        // Test various duration configurations
927        let config1 = ConfigBuilder::new()
928            .timeout(Duration::from_nanos(1))
929            .build();
930        assert_eq!(config1.timeout, Duration::from_nanos(1));
931
932        let config2 = ConfigBuilder::new()
933            .retry_delay(Duration::from_secs(3600)) // 1 hour
934            .build();
935        assert_eq!(config2.retry_delay, Duration::from_secs(3600));
936
937        // Test edge case ports
938        let config3 = ConfigBuilder::new().port(1).build();
939        assert_eq!(config3.port, 1);
940
941        let config4 = ConfigBuilder::new().port(65535).build();
942        assert_eq!(config4.port, 65535);
943
944        // Test very high retry counts
945        let config5 = ConfigBuilder::new().max_retries(1000).build();
946        assert_eq!(config5.max_retries, 1000);
947    }
948}