riglr_web_tools/
tweetscout.rs

1//! TweetScout integration for Twitter/X account analysis and credibility scoring
2//!
3//! This module provides tools for accessing TweetScout API to analyze Twitter/X accounts,
4//! calculate credibility scores, and analyze social networks for crypto influencer detection.
5
6use crate::{client::WebClient, error::WebToolError};
7use riglr_core::provider::ApplicationContext;
8use riglr_macros::tool;
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::env;
13use tracing::{debug, info};
14
15/// Environment variable name for TweetScout API key
16const TWEETSCOUT_API_KEY_ENV: &str = "TWEETSCOUT_API_KEY";
17
18/// Configuration for TweetScout API access
19#[derive(Debug, Clone)]
20pub struct TweetScoutConfig {
21    /// API base URL (default: https://api.tweetscout.io/api)
22    pub base_url: String,
23    /// API key for authentication
24    pub api_key: Option<String>,
25    /// Rate limit requests per minute (default: 60)
26    pub rate_limit_per_minute: u32,
27    /// Timeout for API requests in seconds (default: 30)
28    pub request_timeout: u64,
29}
30
31impl Default for TweetScoutConfig {
32    fn default() -> Self {
33        Self {
34            base_url: "https://api.tweetscout.io/api".to_string(),
35            api_key: env::var(TWEETSCOUT_API_KEY_ENV).ok(),
36            rate_limit_per_minute: 60,
37            request_timeout: 30,
38        }
39    }
40}
41
42/// Helper function to get TweetScout API key from ApplicationContext
43fn get_api_key_from_context(context: &ApplicationContext) -> Result<String, WebToolError> {
44    context
45        .config
46        .providers
47        .tweetscout_api_key
48        .clone()
49        .ok_or_else(|| {
50            WebToolError::Config(
51                "TweetScout API key not configured. Set TWEETSCOUT_API_KEY in your environment."
52                    .to_string(),
53            )
54        })
55}
56
57/// Account information response from TweetScout
58#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
59pub struct AccountInfo {
60    /// User ID
61    pub id: Option<String>,
62    /// Display name
63    pub name: Option<String>,
64    /// Username/handle
65    pub screen_name: Option<String>,
66    /// Profile description/bio
67    pub description: Option<String>,
68    /// Avatar image URL
69    pub avatar: Option<String>,
70    /// Banner image URL
71    pub banner: Option<String>,
72    /// Number of followers
73    pub followers_count: Option<i64>,
74    /// Number of accounts following (friends)
75    pub friends_count: Option<i64>,
76    /// Number of tweets/posts
77    pub statuses_count: Option<i64>,
78    /// Account registration date
79    pub register_date: Option<String>,
80    /// Verification status
81    pub verified: Option<bool>,
82}
83
84/// Score response from TweetScout
85#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
86pub struct ScoreResponse {
87    /// Credibility score (0-100)
88    pub score: f64,
89}
90
91/// Account information for followers/friends lists
92#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
93pub struct Account {
94    /// User ID
95    pub id: Option<String>,
96    /// Display name
97    pub name: Option<String>,
98    /// Username/handle (Note: API returns screeName, handling typo)
99    #[serde(rename = "screeName")]
100    pub screen_name: Option<String>,
101    /// Profile description
102    pub description: Option<String>,
103    /// Avatar URL
104    pub avatar: Option<String>,
105    /// Banner URL
106    pub banner: Option<String>,
107    /// Followers count (Note: API uses camelCase)
108    #[serde(rename = "followersCount")]
109    pub followers_count: Option<i64>,
110    /// Friends/following count
111    #[serde(rename = "friendsCount")]
112    pub friends_count: Option<i64>,
113    /// Number of posts
114    pub statuses_count: Option<i64>,
115    /// Registration date
116    #[serde(rename = "registerDate")]
117    pub register_date: Option<String>,
118    /// Verification status
119    pub verified: Option<bool>,
120    /// Account score
121    pub score: Option<f64>,
122}
123
124/// Error response from TweetScout API
125#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
126pub struct ErrorResponse {
127    /// Error message
128    pub message: String,
129}
130
131/// Comprehensive account analysis result
132#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
133pub struct AccountAnalysis {
134    /// Username analyzed
135    pub username: String,
136    /// Basic account information
137    pub info: AccountInfo,
138    /// Credibility score (0-100)
139    pub credibility_score: f64,
140    /// Score classification
141    pub score_level: ScoreLevel,
142    /// Account age in days
143    pub account_age_days: Option<i64>,
144    /// Average tweets per day
145    pub avg_tweets_per_day: Option<f64>,
146    /// Follower to following ratio
147    pub follower_ratio: Option<f64>,
148    /// Engagement metrics
149    pub engagement: EngagementMetrics,
150    /// Risk indicators
151    pub risk_indicators: Vec<String>,
152    /// Summary assessment
153    pub assessment: String,
154}
155
156/// Credibility score level classification
157#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
158pub enum ScoreLevel {
159    /// Excellent credibility (80-100)
160    #[serde(rename = "excellent")]
161    Excellent,
162    /// Good credibility (60-80)
163    #[serde(rename = "good")]
164    Good,
165    /// Fair credibility (40-60)
166    #[serde(rename = "fair")]
167    Fair,
168    /// Poor credibility (20-40)
169    #[serde(rename = "poor")]
170    Poor,
171    /// Very poor credibility (0-20)
172    #[serde(rename = "very_poor")]
173    VeryPoor,
174}
175
176/// Engagement metrics for an account
177#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
178pub struct EngagementMetrics {
179    /// Total followers
180    pub followers: i64,
181    /// Total following
182    pub following: i64,
183    /// Total posts
184    pub posts: i64,
185    /// Engagement rate estimate
186    pub engagement_rate: f64,
187    /// Whether account is likely a bot
188    pub likely_bot: bool,
189    /// Whether account is likely spam
190    pub likely_spam: bool,
191}
192
193/// Social network analysis result
194#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
195pub struct SocialNetworkAnalysis {
196    /// Username analyzed
197    pub username: String,
198    /// Top followers with scores
199    pub top_followers: Vec<ScoredAccount>,
200    /// Top friends (following) with scores
201    pub top_friends: Vec<ScoredAccount>,
202    /// Average follower score
203    pub avg_follower_score: f64,
204    /// Average friend score
205    pub avg_friend_score: f64,
206    /// Quality of network
207    pub network_quality: NetworkQuality,
208    /// Key influencers in network
209    pub key_influencers: Vec<String>,
210    /// Network assessment
211    pub assessment: String,
212}
213
214/// Account with score for network analysis
215#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
216pub struct ScoredAccount {
217    /// Username
218    pub username: String,
219    /// Display name
220    pub name: String,
221    /// Follower count
222    pub followers: i64,
223    /// Score
224    pub score: f64,
225    /// Whether verified
226    pub verified: bool,
227    /// Influence level
228    pub influence_level: String,
229}
230
231/// Network quality assessment
232#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
233pub enum NetworkQuality {
234    /// High quality network
235    #[serde(rename = "high")]
236    High,
237    /// Medium quality network
238    #[serde(rename = "medium")]
239    Medium,
240    /// Low quality network
241    #[serde(rename = "low")]
242    Low,
243    /// Suspicious network
244    #[serde(rename = "suspicious")]
245    Suspicious,
246}
247
248/// Get basic information about a Twitter/X account.
249#[tool]
250pub async fn get_account_info(
251    context: &ApplicationContext,
252    username: String,
253) -> crate::error::Result<AccountInfo> {
254    debug!("Fetching account info for: {}", username);
255
256    let config = TweetScoutConfig::default();
257    let client = WebClient::default();
258
259    let api_key = get_api_key_from_context(context)?;
260
261    let url = format!("{}/info/{}", config.base_url, username);
262
263    let mut headers = HashMap::new();
264    headers.insert("ApiKey".to_string(), api_key);
265
266    info!("Requesting account info from TweetScout for: {}", username);
267
268    let response_text = client
269        .get_with_headers(&url, headers)
270        .await
271        .map_err(|e| WebToolError::Network(format!("Failed to fetch account info: {}", e)))?;
272
273    let info: AccountInfo = serde_json::from_str(&response_text).map_err(|e| {
274        WebToolError::Parsing(format!("Failed to parse TweetScout response: {}", e))
275    })?;
276
277    info!(
278        "Successfully fetched info for @{} - Followers: {:?}, Verified: {:?}",
279        username, info.followers_count, info.verified
280    );
281
282    Ok(info)
283}
284
285/// Get the credibility score for a Twitter/X account.
286/// Returns a score from 0-100 indicating account trustworthiness.
287#[tool]
288pub async fn get_account_score(
289    context: &ApplicationContext,
290    username: String,
291) -> crate::error::Result<ScoreResponse> {
292    debug!("Fetching credibility score for: {}", username);
293
294    let config = TweetScoutConfig::default();
295    let client = WebClient::default();
296
297    let api_key = get_api_key_from_context(context)?;
298
299    let url = format!("{}/score/{}", config.base_url, username);
300
301    let mut headers = HashMap::new();
302    headers.insert("ApiKey".to_string(), api_key);
303
304    info!(
305        "Requesting credibility score from TweetScout for: {}",
306        username
307    );
308
309    let response_text = client
310        .get_with_headers(&url, headers)
311        .await
312        .map_err(|e| WebToolError::Network(format!("Failed to fetch score: {}", e)))?;
313
314    let score: ScoreResponse = serde_json::from_str(&response_text)
315        .map_err(|e| WebToolError::Parsing(format!("Failed to parse score response: {}", e)))?;
316
317    info!(
318        "Successfully fetched score for @{}: {:.1}/100",
319        username, score.score
320    );
321
322    Ok(score)
323}
324
325/// Get the top 20 followers of a Twitter/X account with their scores.
326#[tool]
327pub async fn get_top_followers(
328    context: &ApplicationContext,
329    username: String,
330) -> crate::error::Result<Vec<Account>> {
331    debug!("Fetching top followers for: {}", username);
332
333    let config = TweetScoutConfig::default();
334    let client = WebClient::default();
335
336    let api_key = get_api_key_from_context(context)?;
337
338    let url = format!("{}/top-followers/{}", config.base_url, username);
339
340    let mut headers = HashMap::new();
341    headers.insert("ApiKey".to_string(), api_key);
342
343    info!("Requesting top followers from TweetScout for: {}", username);
344
345    let response_text = client
346        .get_with_headers(&url, headers)
347        .await
348        .map_err(|e| WebToolError::Network(format!("Failed to fetch followers: {}", e)))?;
349
350    let followers: Vec<Account> = serde_json::from_str(&response_text)
351        .map_err(|e| WebToolError::Parsing(format!("Failed to parse followers response: {}", e)))?;
352
353    info!(
354        "Successfully fetched {} top followers for @{}",
355        followers.len(),
356        username
357    );
358
359    Ok(followers)
360}
361
362/// Get the top 20 friends (accounts being followed) of a Twitter/X account with their scores.
363#[tool]
364pub async fn get_top_friends(
365    context: &ApplicationContext,
366    username: String,
367) -> crate::error::Result<Vec<Account>> {
368    debug!("Fetching top friends for: {}", username);
369
370    let config = TweetScoutConfig::default();
371    let client = WebClient::default();
372
373    let api_key = get_api_key_from_context(context)?;
374
375    let url = format!("{}/top-friends/{}", config.base_url, username);
376
377    let mut headers = HashMap::new();
378    headers.insert("ApiKey".to_string(), api_key);
379
380    info!("Requesting top friends from TweetScout for: {}", username);
381
382    let response_text = client
383        .get_with_headers(&url, headers)
384        .await
385        .map_err(|e| WebToolError::Network(format!("Failed to fetch friends: {}", e)))?;
386
387    let friends: Vec<Account> = serde_json::from_str(&response_text)
388        .map_err(|e| WebToolError::Parsing(format!("Failed to parse friends response: {}", e)))?;
389
390    info!(
391        "Successfully fetched {} top friends for @{}",
392        friends.len(),
393        username
394    );
395
396    Ok(friends)
397}
398
399/// Perform comprehensive analysis of a Twitter/X account including credibility scoring.
400/// Combines account info and score into a detailed assessment.
401#[tool]
402pub async fn analyze_account(
403    context: &ApplicationContext,
404    username: String,
405) -> crate::error::Result<AccountAnalysis> {
406    debug!("Performing comprehensive analysis for: {}", username);
407
408    // Fetch account info and score in parallel would be better, but let's do sequentially for simplicity
409    let info = get_account_info(context, username.clone()).await?;
410    let score_resp = get_account_score(context, username.clone()).await?;
411
412    let account_age_days = calculate_account_age(&info);
413    let avg_tweets_per_day = calculate_avg_tweets_per_day(&info, account_age_days);
414    let follower_ratio = calculate_follower_ratio(&info);
415    let score_level = determine_score_level(score_resp.score);
416    let engagement = build_engagement_metrics(&info, score_resp.score);
417    let risk_indicators =
418        build_risk_indicators(&info, &engagement, follower_ratio, score_resp.score);
419    let assessment = build_assessment(&username, score_resp.score, &score_level);
420
421    Ok(AccountAnalysis {
422        username,
423        info,
424        credibility_score: score_resp.score,
425        score_level,
426        account_age_days,
427        avg_tweets_per_day,
428        follower_ratio,
429        engagement,
430        risk_indicators,
431        assessment,
432    })
433}
434
435/// Calculate account age in days from registration date
436fn calculate_account_age(info: &AccountInfo) -> Option<i64> {
437    info.register_date.as_ref().and({
438        // Parse date and calculate days (simplified, would need proper date parsing)
439        // For now, return None as proper date parsing would require chrono
440        None
441    })
442}
443
444/// Calculate average tweets per day
445fn calculate_avg_tweets_per_day(info: &AccountInfo, account_age_days: Option<i64>) -> Option<f64> {
446    if let (Some(_tweets), Some(_age)) = (info.statuses_count, account_age_days) {
447        None // Would calculate if we had proper age
448    } else {
449        None
450    }
451}
452
453/// Calculate follower to following ratio
454fn calculate_follower_ratio(info: &AccountInfo) -> Option<f64> {
455    if let (Some(followers), Some(following)) = (info.followers_count, info.friends_count) {
456        if following > 0 {
457            Some(followers as f64 / following as f64)
458        } else {
459            None
460        }
461    } else {
462        None
463    }
464}
465
466/// Determine score level classification from numeric score
467fn determine_score_level(score: f64) -> ScoreLevel {
468    match score as i32 {
469        80..=100 => ScoreLevel::Excellent,
470        60..=79 => ScoreLevel::Good,
471        40..=59 => ScoreLevel::Fair,
472        20..=39 => ScoreLevel::Poor,
473        _ => ScoreLevel::VeryPoor,
474    }
475}
476
477/// Build engagement metrics with bot/spam detection
478fn build_engagement_metrics(info: &AccountInfo, score: f64) -> EngagementMetrics {
479    let followers = info.followers_count.unwrap_or(0);
480    let following = info.friends_count.unwrap_or(0);
481    let posts = info.statuses_count.unwrap_or(0);
482
483    // Simple bot detection heuristics
484    let likely_bot = score < 30.0
485        || (following > followers * 10 && followers < 100)
486        || (posts > 100000 && followers < 1000);
487
488    let likely_spam = score < 20.0 || (following > 5000 && followers < 100);
489
490    let engagement_rate = if posts > 0 {
491        ((followers + following) as f64 / posts as f64).min(100.0)
492    } else {
493        0.0
494    };
495
496    EngagementMetrics {
497        followers,
498        following,
499        posts,
500        engagement_rate,
501        likely_bot,
502        likely_spam,
503    }
504}
505
506/// Build list of risk indicators for the account
507fn build_risk_indicators(
508    info: &AccountInfo,
509    engagement: &EngagementMetrics,
510    follower_ratio: Option<f64>,
511    score: f64,
512) -> Vec<String> {
513    let mut risk_indicators = Vec::new();
514
515    if score < 40.0 {
516        risk_indicators.push("Low credibility score".to_string());
517    }
518
519    if engagement.likely_bot {
520        risk_indicators.push("Likely bot account".to_string());
521    }
522
523    if engagement.likely_spam {
524        risk_indicators.push("Likely spam account".to_string());
525    }
526
527    if info.verified != Some(true) && engagement.followers > 10000 {
528        risk_indicators.push("Large unverified account".to_string());
529    }
530
531    if let Some(ratio) = follower_ratio {
532        if ratio < 0.1 && engagement.followers < 1000 {
533            risk_indicators.push("Very low follower ratio".to_string());
534        }
535    }
536
537    if engagement.posts == 0 {
538        risk_indicators.push("No posts/tweets".to_string());
539    }
540
541    risk_indicators
542}
543
544/// Build textual assessment based on score level
545fn build_assessment(username: &str, score: f64, score_level: &ScoreLevel) -> String {
546    match score_level {
547        ScoreLevel::Excellent => format!(
548            "@{} has excellent credibility ({:.1}/100). This appears to be a highly trustworthy account.",
549            username, score
550        ),
551        ScoreLevel::Good => format!(
552            "@{} has good credibility ({:.1}/100). This account appears legitimate and trustworthy.",
553            username, score
554        ),
555        ScoreLevel::Fair => format!(
556            "@{} has fair credibility ({:.1}/100). Exercise some caution when engaging with this account.",
557            username, score
558        ),
559        ScoreLevel::Poor => format!(
560            "@{} has poor credibility ({:.1}/100). Be cautious - this account shows concerning patterns.",
561            username, score
562        ),
563        ScoreLevel::VeryPoor => format!(
564            "@{} has very poor credibility ({:.1}/100). High risk - avoid engagement with this account.",
565            username, score
566        ),
567    }
568}
569
570/// Analyze the social network of a Twitter/X account including followers and friends.
571/// Provides insights into the quality and influence of an account's network.
572#[tool]
573pub async fn analyze_social_network(
574    context: &ApplicationContext,
575    username: String,
576) -> crate::error::Result<SocialNetworkAnalysis> {
577    debug!("Analyzing social network for: {}", username);
578
579    // Fetch followers and friends
580    let followers = get_top_followers(context, username.clone()).await?;
581    let friends = get_top_friends(context, username.clone()).await?;
582
583    // Convert to scored accounts
584    let top_followers: Vec<ScoredAccount> = followers
585        .iter()
586        .map(|acc| ScoredAccount {
587            username: acc.screen_name.clone().unwrap_or_default(),
588            name: acc.name.clone().unwrap_or_default(),
589            followers: acc.followers_count.unwrap_or(0),
590            score: acc.score.unwrap_or(0.0),
591            verified: acc.verified.unwrap_or(false),
592            influence_level: classify_influence(acc.followers_count.unwrap_or(0)),
593        })
594        .collect();
595
596    let top_friends: Vec<ScoredAccount> = friends
597        .iter()
598        .map(|acc| ScoredAccount {
599            username: acc.screen_name.clone().unwrap_or_default(),
600            name: acc.name.clone().unwrap_or_default(),
601            followers: acc.followers_count.unwrap_or(0),
602            score: acc.score.unwrap_or(0.0),
603            verified: acc.verified.unwrap_or(false),
604            influence_level: classify_influence(acc.followers_count.unwrap_or(0)),
605        })
606        .collect();
607
608    // Calculate average scores
609    let avg_follower_score = if !top_followers.is_empty() {
610        top_followers.iter().map(|a| a.score).sum::<f64>() / top_followers.len() as f64
611    } else {
612        0.0
613    };
614
615    let avg_friend_score = if !top_friends.is_empty() {
616        top_friends.iter().map(|a| a.score).sum::<f64>() / top_friends.len() as f64
617    } else {
618        0.0
619    };
620
621    // Identify key influencers (high follower count + good score)
622    let mut key_influencers: Vec<String> = top_followers
623        .iter()
624        .chain(top_friends.iter())
625        .filter(|acc| acc.followers > 10000 && acc.score > 50.0)
626        .map(|acc| format!("@{}", acc.username))
627        .collect();
628    key_influencers.dedup();
629    key_influencers.truncate(5); // Keep top 5
630
631    // Determine network quality
632    let avg_network_score = (avg_follower_score + avg_friend_score) / 2.0;
633    let network_quality = if avg_network_score > 70.0 {
634        NetworkQuality::High
635    } else if avg_network_score > 50.0 {
636        NetworkQuality::Medium
637    } else if avg_network_score > 30.0 {
638        NetworkQuality::Low
639    } else {
640        NetworkQuality::Suspicious
641    };
642
643    // Generate assessment
644    let assessment = match network_quality {
645        NetworkQuality::High => format!(
646            "@{} has a high-quality network with an average score of {:.1}. Strong connections with credible accounts.",
647            username, avg_network_score
648        ),
649        NetworkQuality::Medium => format!(
650            "@{} has a medium-quality network with an average score of {:.1}. Mixed credibility in connections.",
651            username, avg_network_score
652        ),
653        NetworkQuality::Low => format!(
654            "@{} has a low-quality network with an average score of {:.1}. Many connections show poor credibility.",
655            username, avg_network_score
656        ),
657        NetworkQuality::Suspicious => format!(
658            "@{} has a suspicious network with an average score of {:.1}. High risk of bot/spam connections.",
659            username, avg_network_score
660        ),
661    };
662
663    Ok(SocialNetworkAnalysis {
664        username,
665        top_followers,
666        top_friends,
667        avg_follower_score,
668        avg_friend_score,
669        network_quality,
670        key_influencers,
671        assessment,
672    })
673}
674
675/// Classify influence level based on follower count
676fn classify_influence(followers: i64) -> String {
677    match followers {
678        f if f >= 1000000 => "Mega Influencer".to_string(),
679        f if f >= 100000 => "Macro Influencer".to_string(),
680        f if f >= 10000 => "Mid-tier Influencer".to_string(),
681        f if f >= 1000 => "Micro Influencer".to_string(),
682        _ => "Regular User".to_string(),
683    }
684}
685
686/// Quick credibility check for a Twitter/X account.
687/// Returns a simple assessment of whether an account is trustworthy.
688#[tool]
689pub async fn is_account_credible(
690    context: &ApplicationContext,
691    username: String,
692    threshold: Option<f64>,
693) -> crate::error::Result<CredibilityCheck> {
694    debug!("Performing quick credibility check for: {}", username);
695
696    let threshold = threshold.unwrap_or(50.0); // Default threshold of 50/100
697    let score_resp = get_account_score(context, username.clone()).await?;
698
699    let is_credible = score_resp.score >= threshold;
700
701    let verdict = if score_resp.score >= 80.0 {
702        "HIGHLY CREDIBLE"
703    } else if score_resp.score >= 60.0 {
704        "CREDIBLE"
705    } else if score_resp.score >= 40.0 {
706        "QUESTIONABLE"
707    } else if score_resp.score >= 20.0 {
708        "LOW CREDIBILITY"
709    } else {
710        "NOT CREDIBLE"
711    };
712
713    let recommendation = if is_credible {
714        format!(
715            "@{} meets credibility threshold ({:.1}/{:.1}). Safe to engage.",
716            username, score_resp.score, threshold
717        )
718    } else {
719        format!(
720            "@{} below credibility threshold ({:.1}/{:.1}). Exercise caution.",
721            username, score_resp.score, threshold
722        )
723    };
724
725    Ok(CredibilityCheck {
726        username,
727        score: score_resp.score,
728        threshold,
729        is_credible,
730        verdict: verdict.to_string(),
731        recommendation,
732    })
733}
734
735/// Simple credibility check result
736#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
737pub struct CredibilityCheck {
738    /// Username checked
739    pub username: String,
740    /// Credibility score (0-100)
741    pub score: f64,
742    /// Threshold used
743    pub threshold: f64,
744    /// Whether account meets credibility threshold
745    pub is_credible: bool,
746    /// Simple verdict
747    pub verdict: String,
748    /// Recommendation
749    pub recommendation: String,
750}
751
752#[cfg(test)]
753mod tests {
754    use super::*;
755
756    #[test]
757    fn test_tweetscout_config_default() {
758        let config = TweetScoutConfig::default();
759        assert_eq!(config.base_url, "https://api.tweetscout.io/api");
760        assert_eq!(config.rate_limit_per_minute, 60);
761        assert_eq!(config.request_timeout, 30);
762    }
763
764    #[test]
765    fn test_score_level_serialization() {
766        let level = ScoreLevel::Good;
767        let json = serde_json::to_string(&level).unwrap();
768        assert_eq!(json, "\"good\"");
769
770        let level: ScoreLevel = serde_json::from_str("\"excellent\"").unwrap();
771        assert!(matches!(level, ScoreLevel::Excellent));
772    }
773
774    #[test]
775    fn test_network_quality_serialization() {
776        let quality = NetworkQuality::High;
777        let json = serde_json::to_string(&quality).unwrap();
778        assert_eq!(json, "\"high\"");
779
780        let quality: NetworkQuality = serde_json::from_str("\"suspicious\"").unwrap();
781        assert!(matches!(quality, NetworkQuality::Suspicious));
782    }
783
784    #[test]
785    fn test_account_info_deserialization() {
786        let json = r#"{
787            "id": "123456",
788            "name": "Test User",
789            "screen_name": "testuser",
790            "followers_count": 1000,
791            "verified": true
792        }"#;
793
794        let info: AccountInfo = serde_json::from_str(json).unwrap();
795        assert_eq!(info.id, Some("123456".to_string()));
796        assert_eq!(info.screen_name, Some("testuser".to_string()));
797        assert_eq!(info.followers_count, Some(1000));
798        assert_eq!(info.verified, Some(true));
799    }
800
801    #[test]
802    fn test_score_response_deserialization() {
803        let json = r#"{
804            "score": 75.5
805        }"#;
806
807        let response: ScoreResponse = serde_json::from_str(json).unwrap();
808        assert!((response.score - 75.5).abs() < 0.001);
809    }
810
811    #[test]
812    fn test_account_deserialization_with_typo() {
813        // Test that we handle the API's typo "screeName" correctly
814        let json = r#"{
815            "id": "123",
816            "screeName": "testuser",
817            "followersCount": 500,
818            "friendsCount": 200,
819            "score": 65.0
820        }"#;
821
822        let account: Account = serde_json::from_str(json).unwrap();
823        assert_eq!(account.screen_name, Some("testuser".to_string()));
824        assert_eq!(account.followers_count, Some(500));
825        assert_eq!(account.friends_count, Some(200));
826        assert_eq!(account.score, Some(65.0));
827    }
828
829    #[test]
830    fn test_classify_influence() {
831        assert_eq!(classify_influence(2000000), "Mega Influencer");
832        assert_eq!(classify_influence(500000), "Macro Influencer");
833        assert_eq!(classify_influence(50000), "Mid-tier Influencer");
834        assert_eq!(classify_influence(5000), "Micro Influencer");
835        assert_eq!(classify_influence(500), "Regular User");
836    }
837}