Skip to main content

scope_cli/cli/
insights.rs

1//! # Insights Command
2//!
3//! Infers the type of blockchain target (address, token, transaction) from input,
4//! auto-detects chain, and runs relevant Scope analyses to produce unified insights.
5
6use crate::cli::address::{self, AddressArgs};
7use crate::cli::crawl::{Period, fetch_analytics_for_input};
8use crate::cli::output::Output;
9use crate::cli::tx::{fetch_transaction_report, format_tx_markdown};
10use crate::outln;
11use clap::Args;
12use scope::chains::{
13    ChainClientFactory, infer_chain_from_address, infer_chain_from_hash, native_symbol,
14};
15use scope::config::Config;
16
17use super::health_args::HealthArgs;
18use scope::display::report;
19use scope::error::Result;
20use scope::market::health::PegStatus;
21use scope::market::{MarketSummary, MarketVenue, VenueRegistry, fetch_token_summary};
22use scope::tokens::{TokenAliases, is_stablecoin_symbol};
23
24/// Target type inferred from user input.
25#[derive(Debug, Clone)]
26pub enum InferredTarget {
27    /// Blockchain address (EVM, Tron, or Solana).
28    Address { chain: String },
29    /// Transaction hash.
30    Transaction { chain: String },
31    /// Token symbol, name, or contract address.
32    Token { chain: String },
33}
34
35/// Arguments for the insights command.
36#[derive(Debug, Args)]
37#[command(after_help = "\x1b[1mExamples:\x1b[0m
38  scope insights 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2
39  scope insights @main-wallet                             \x1b[2m# address book shortcut\x1b[0m
40  scope insights 0xabc123def456... --decode --trace
41  scope insights USDC
42  scope insights DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy --chain solana")]
43pub struct InsightsArgs {
44    /// Target to analyze: address, transaction hash, or token (symbol/name/address).
45    ///
46    /// Scope infers the type and chain from format:
47    /// - `@label` = address book shortcut (e.g. @main-wallet)
48    /// - `0x...` (42 chars) = EVM address → ethereum
49    /// - `T...` (34 chars) = Tron address → tron
50    /// - Base58 (32–44 chars) = Solana address → solana
51    /// - `0x...` (66 chars) = EVM tx hash
52    /// - 64 hex chars = Tron tx hash
53    /// - Base58 (80–90 chars) = Solana signature
54    /// - Otherwise = token symbol/name (e.g. USDC, WETH)
55    pub target: String,
56
57    /// The token's contract: use it and skip the symbol lookup.
58    #[arg(long = "token", value_name = "CONTRACT")]
59    pub token_contract: Option<String>,
60
61    /// Override detected chain (ethereum, polygon, solana, tron, etc.).
62    #[arg(short, long)]
63    pub chain: Option<String>,
64
65    /// Include decoded transaction input (for tx targets).
66    #[arg(long)]
67    pub decode: bool,
68
69    /// Include internal transaction trace (for tx targets).
70    #[arg(long)]
71    pub trace: bool,
72
73    /// Health threshold overrides for the stablecoin market check.
74    #[command(flatten)]
75    pub health: HealthArgs,
76}
77
78/// Infers the target type and chain from the input string.
79pub fn infer_target(input: &str, chain_override: Option<&str>) -> InferredTarget {
80    let trimmed = input.trim();
81
82    if let Some(chain) = chain_override {
83        let chain = chain.to_lowercase();
84        // With override, we still need to infer type
85        if infer_chain_from_hash(trimmed).is_some() {
86            return InferredTarget::Transaction { chain };
87        }
88        if TokenAliases::is_address(trimmed) {
89            return InferredTarget::Address { chain };
90        }
91        return InferredTarget::Token { chain };
92    }
93
94    // Transaction hash (format implies chain)
95    if let Some(chain) = infer_chain_from_hash(trimmed) {
96        return InferredTarget::Transaction {
97            chain: chain.to_string(),
98        };
99    }
100
101    // Address (format implies chain)
102    if TokenAliases::is_address(trimmed) {
103        let chain = infer_chain_from_address(trimmed).unwrap_or(scope::chains::DEFAULT_CHAIN);
104        return InferredTarget::Address {
105            chain: chain.to_string(),
106        };
107    }
108
109    // Default: token (symbol or name)
110    InferredTarget::Token {
111        chain: scope::chains::DEFAULT_CHAIN.to_string(),
112    }
113}
114
115/// Runs the insights command.
116pub async fn run(
117    mut args: InsightsArgs,
118    config: &Config,
119    clients: &dyn ChainClientFactory,
120    out: &Output,
121) -> Result<()> {
122    // Resolve address book label → address + chain
123    if let Some((address, chain)) =
124        crate::cli::address_book::resolve_address_book_input(&args.target, config)?
125    {
126        args.target = address;
127        if args.chain.is_none() {
128            args.chain = Some(chain);
129        }
130    }
131
132    let chain_override = args.chain.as_deref();
133    let target = infer_target(&args.target, chain_override);
134
135    let sp = crate::cli::progress::Spinner::new(
136        &format!(
137            "Analyzing {} on {}...",
138            target_type_label(&target),
139            chain_label(&target)
140        ),
141        out,
142    );
143
144    let mut output = String::new();
145    output.push_str("# Scope Insights\n\n");
146    output.push_str(&format!("**Target:** `{}`\n\n", args.target));
147    output.push_str(&format!(
148        "**Detected:** {} on {}\n\n",
149        target_type_label(&target),
150        chain_label(&target)
151    ));
152    output.push_str("---\n\n");
153
154    match &target {
155        InferredTarget::Address { chain } => {
156            output.push_str("## Observations\n\n");
157            let addr_args = AddressArgs {
158                address: args.target.clone(),
159                chain: chain.clone(),
160                format: Some(scope::config::OutputFormat::Markdown),
161                include_txs: false,
162                include_tokens: true,
163                limit: 10,
164                report: None,
165                dossier: false,
166            };
167            let client = clients.create_chain_client(chain)?;
168            let report = address::analyze_address(&addr_args, client.as_ref(), out).await?;
169
170            // Contract vs EOA (EVM chains support get_code)
171            let code_result = client.get_code(&args.target).await;
172            let is_contract = code_result
173                .as_ref()
174                .is_ok_and(|c| !c.is_empty() && c != "0x");
175            if code_result.is_ok() {
176                output.push_str(&format!(
177                    "- **Type:** {}\n",
178                    if is_contract {
179                        "Contract"
180                    } else {
181                        "Externally Owned Account (EOA)"
182                    }
183                ));
184            }
185
186            output.push_str(&format!(
187                "- **Native balance:** {} ({})\n",
188                report.balance.formatted,
189                scope::chains::native_symbol(chain)
190            ));
191            if let Some(ref usd) = report.balance.usd {
192                output.push_str(&format!("- **USD value:** ${:.2}\n", usd));
193            }
194            output.push_str(&format!(
195                "- **Transaction count:** {}\n",
196                report.transaction_count
197            ));
198            if let Some(ref tokens) = report.tokens
199                && !tokens.is_empty()
200            {
201                output.push_str(&format!(
202                    "- **Token holdings:** {} different tokens\n",
203                    tokens.len()
204                ));
205                output.push_str("\n### Token Balances\n\n");
206                for tb in tokens.iter().take(10) {
207                    output.push_str(&format!(
208                        "- {}: {} ({})\n",
209                        tb.symbol, tb.formatted_balance, tb.contract_address
210                    ));
211                }
212                if tokens.len() > 10 {
213                    output.push_str(&format!("\n*...and {} more*\n", tokens.len() - 10));
214                }
215            }
216
217            // Risk assessment (compliance engine)
218            let risk_assessment =
219                scope::compliance::risk::RiskEngine::from_env_with_http(clients.http())
220                    .assess_address(&args.target, chain)
221                    .await
222                    .ok();
223
224            if let Some(ref risk) = risk_assessment {
225                output.push_str(&risk_line(risk));
226            }
227
228            // Meta analysis
229            let meta = meta_analysis_address(
230                is_contract,
231                report.balance.usd,
232                report.tokens.as_ref().map(|t| t.len()).unwrap_or(0),
233                risk_assessment.as_ref().map(|r| r.overall_score),
234                risk_assessment.as_ref().map(|r| &r.risk_level),
235            );
236            output.push_str("\n### Synthesis\n\n");
237            output.push_str(&format!("{}\n\n", meta.synthesis));
238            output.push_str(&format!("**Key takeaway:** {}\n\n", meta.key_takeaway));
239            if !meta.recommendations.is_empty() {
240                output.push_str("**Consider:**\n");
241                for rec in &meta.recommendations {
242                    output.push_str(&format!("- {}\n", rec));
243                }
244            }
245            output.push_str("\n---\n\n");
246            let full_report = if let Some(ref risk) = risk_assessment {
247                crate::cli::address_report::generate_dossier_report(&report, risk)
248            } else {
249                crate::cli::address_report::generate_address_report(&report)
250            };
251            output.push_str(&full_report);
252        }
253        InferredTarget::Transaction { chain } => {
254            output.push_str("## Observations\n\n");
255            let tx_report =
256                fetch_transaction_report(&args.target, chain, args.decode, args.trace, clients)
257                    .await?;
258
259            let tx_type = classify_tx_type(
260                &tx_report.transaction.input,
261                tx_report.transaction.to.as_deref(),
262            );
263            output.push_str(&format!("- **Type:** {}\n", tx_type));
264
265            output.push_str(&format!(
266                "- **Status:** {}\n",
267                if tx_report.transaction.status {
268                    "Success"
269                } else {
270                    "Failed"
271                }
272            ));
273            output.push_str(&format!("- **From:** `{}`\n", tx_report.transaction.from));
274            output.push_str(&format!(
275                "- **To:** `{}`\n",
276                tx_report
277                    .transaction
278                    .to
279                    .as_deref()
280                    .unwrap_or("Contract Creation")
281            ));
282
283            let (formatted_value, high_value) =
284                format_tx_value(&tx_report.transaction.value, chain);
285            output.push_str(&format!("- **Value:** {}\n", formatted_value));
286            if high_value {
287                output.push_str("- ⚠️ **High-value transfer**\n");
288            }
289
290            output.push_str(&format!("- **Fee:** {}\n", tx_report.gas.transaction_fee));
291
292            // Meta analysis
293            let meta = meta_analysis_tx(
294                tx_type,
295                tx_report.transaction.status,
296                high_value,
297                &tx_report.transaction.from,
298                tx_report.transaction.to.as_deref(),
299            );
300            output.push_str("\n### Synthesis\n\n");
301            output.push_str(&format!("{}\n\n", meta.synthesis));
302            output.push_str(&format!("**Key takeaway:** {}\n\n", meta.key_takeaway));
303            if !meta.recommendations.is_empty() {
304                output.push_str("**Consider:**\n");
305                for rec in &meta.recommendations {
306                    output.push_str(&format!("- {}\n", rec));
307                }
308            }
309            output.push_str("\n---\n\n");
310            output.push_str(&format_tx_markdown(&tx_report));
311        }
312        InferredTarget::Token { chain } => {
313            output.push_str("## Observations\n\n");
314            let analytics = fetch_analytics_for_input(
315                // --token: the contract is the input; an address passes through.
316                args.token_contract.as_deref().unwrap_or(&args.target),
317                chain,
318                Period::Hour24,
319                10,
320                clients,
321                Some(&sp),
322                out,
323            )
324            .await?;
325
326            // Token risk summary (interpretive bullets)
327            let risk_summary = report::token_risk_summary(&analytics);
328            output.push_str(&format!(
329                "- **Risk:** {} {}/10 ({})\n",
330                risk_summary.emoji, risk_summary.score, risk_summary.level
331            ));
332            if !risk_summary.concerns.is_empty() {
333                for c in &risk_summary.concerns {
334                    output.push_str(&format!("- ⚠️ {}\n", c));
335                }
336            }
337            if !risk_summary.positives.is_empty() {
338                for p in &risk_summary.positives {
339                    output.push_str(&format!("- ✅ {}\n", p));
340                }
341            }
342
343            output.push_str(&format!(
344                "- **Token:** {} ({})\n",
345                analytics.token.symbol, analytics.token.name
346            ));
347            output.push_str(&format!(
348                "- **Address:** `{}`\n",
349                analytics.token.contract_address
350            ));
351            output.push_str(&format!(
352                "- **Price:** {}\n",
353                scope::display::format::format_price_usd(analytics.price_usd)
354            ));
355            output.push_str(&format!(
356                "- **Liquidity (24h):** {}\n",
357                scope::display::format_usd(analytics.liquidity_usd)
358            ));
359            output.push_str(&format!(
360                "- **Volume (24h):** {}\n",
361                scope::display::format_usd(analytics.volume_24h)
362            ));
363
364            // Top holder context
365            if let Some(top) = analytics.holders.first() {
366                output.push_str(&format!(
367                    "- **Top holder:** `{}` ({:.1}%)\n",
368                    top.address, top.percentage
369                ));
370                if top.percentage > 30.0 {
371                    output.push_str("  - ⚠️ High concentration risk\n");
372                }
373            }
374            output.push_str(&format!(
375                "- **Holders displayed:** {}\n",
376                analytics.holders.len()
377            ));
378
379            // Stablecoin: auto-include market/peg via venue registry
380            let mut peg_status: Option<PegStatus> = None;
381            if is_stablecoin_symbol(&analytics.token.symbol)
382                && let Ok(registry) = VenueRegistry::load()
383            {
384                // Try the default venue first, fall back to any available CEX
385                let venue_id = if registry.contains(scope::market::DEFAULT_VENUE) {
386                    scope::market::DEFAULT_VENUE
387                } else {
388                    registry
389                        .list()
390                        .first()
391                        .copied()
392                        .unwrap_or(scope::market::DEFAULT_VENUE)
393                };
394                let thresholds = args.health.resolve(config);
395                if let (Some(desc), Ok(venue)) = (
396                    registry.get(venue_id),
397                    MarketVenue::resolve_with_http(venue_id, &registry, clients.http()),
398                ) && let Ok(summary) = fetch_token_summary(&venue, &analytics, &thresholds).await
399                {
400                    peg_status = Some(summary.peg_status());
401                    output.push_str(&peg_line(&desc.name, &summary.pair, &summary));
402                }
403            }
404
405            // Meta analysis
406            let top_holder_pct = analytics.holders.first().map(|h| h.percentage);
407            let meta = meta_analysis_token(
408                &risk_summary,
409                is_stablecoin_symbol(&analytics.token.symbol),
410                peg_status,
411                top_holder_pct,
412                analytics.liquidity_usd,
413            );
414            output.push_str("\n### Synthesis\n\n");
415            output.push_str(&format!("{}\n\n", meta.synthesis));
416            output.push_str(&format!("**Key takeaway:** {}\n\n", meta.key_takeaway));
417            if !meta.recommendations.is_empty() {
418                output.push_str("**Consider:**\n");
419                for rec in &meta.recommendations {
420                    output.push_str(&format!("- {}\n", rec));
421                }
422            }
423            output.push_str("\n---\n\n");
424            output.push_str(&report::generate_report(&analytics));
425        }
426    }
427
428    sp.finish("Insights complete.");
429    outln!(out, "{}", output)?;
430    Ok(())
431}
432
433fn target_type_label(target: &InferredTarget) -> &'static str {
434    match target {
435        InferredTarget::Address { .. } => "Address",
436        InferredTarget::Transaction { .. } => "Transaction",
437        InferredTarget::Token { .. } => "Token",
438    }
439}
440
441fn chain_label(target: &InferredTarget) -> &str {
442    match target {
443        InferredTarget::Address { chain } => chain,
444        InferredTarget::Transaction { chain } => chain,
445        InferredTarget::Token { chain } => chain,
446    }
447}
448
449/// Classifies EVM transaction from input data selector.
450fn classify_tx_type(input: &str, to: Option<&str>) -> &'static str {
451    if to.is_none() {
452        return "Contract Creation";
453    }
454    let selector = input
455        .trim_start_matches("0x")
456        .chars()
457        .take(8)
458        .collect::<String>();
459    let sel = selector.to_lowercase();
460    match sel.as_str() {
461        "a9059cbb" => "ERC-20 Transfer",
462        "095ea7b3" => "ERC-20 Approve",
463        "23b872dd" => "ERC-20 Transfer From",
464        "38ed1739" | "5c11d795" | "4a25d94a" | "8803dbee" | "7ff36ab5" | "18cbafe5"
465        | "fb3bdb41" | "b6f9de95" => "DEX Swap",
466        "ac9650d8" | "5ae401dc" => "Multicall",
467        _ if input.is_empty() || input == "0x" => "Native Transfer",
468        _ => "Contract Call",
469    }
470}
471
472/// Formats raw value to human-readable (e.g. wei → ETH).
473fn format_tx_value(value_str: &str, chain: &str) -> (String, bool) {
474    let wei: u128 = if value_str.starts_with("0x") {
475        let hex_part = value_str.trim_start_matches("0x");
476        if hex_part.is_empty() {
477            0
478        } else {
479            u128::from_str_radix(hex_part, 16).unwrap_or(0)
480        }
481    } else {
482        value_str.parse().unwrap_or(0)
483    };
484    let decimals =
485        scope::chains::registry::chain(chain).map_or(18, |c| i32::from(c.native_decimals));
486    let divisor = 10_f64.powi(decimals);
487    let human = wei as f64 / divisor;
488    let symbol = native_symbol(chain);
489    let formatted = format!("≈ {:.6} {}", human, symbol);
490    // "High value" threshold: > 10 native units
491    let high_value = human > 10.0;
492    (formatted, high_value)
493}
494
495/// The words for one peg status, written once. The market line, the
496/// executive summary, the key takeaway and the recommendation all read it.
497struct PegWords {
498    /// Label on the market line.
499    label: &'static str,
500    /// Sentence for the executive summary, when there is something to say.
501    synthesis: Option<&'static str>,
502    /// Key takeaway that replaces the generic one, when the peg is a concern.
503    takeaway: Option<&'static str>,
504    /// Whether to recommend verifying the peg on other venues.
505    verify_elsewhere: bool,
506}
507
508fn peg_words(status: PegStatus) -> PegWords {
509    const DEVIATION_SYNTHESIS: &str =
510        "Stablecoin peg deviation detected — verify on multiple venues.";
511    const DEVIATION_TAKEAWAY: &str =
512        "Stablecoin deviating from peg — check additional venues before trading.";
513    match status {
514        PegStatus::OnPeg => PegWords {
515            label: "Peg healthy",
516            synthesis: Some("Stablecoin peg is healthy on observed venue."),
517            takeaway: None,
518            verify_elsewhere: false,
519        },
520        PegStatus::BookUnhealthy => PegWords {
521            label: "Peg on target, but market health checks fail",
522            synthesis: Some(
523                "Stablecoin peg is on target, but the market's health checks fail — verify on multiple venues.",
524            ),
525            takeaway: Some(
526                "Stablecoin market health checks fail — check additional venues before trading.",
527            ),
528            verify_elsewhere: true,
529        },
530        PegStatus::SlightDeviation => PegWords {
531            label: "Slight peg deviation",
532            synthesis: Some(DEVIATION_SYNTHESIS),
533            takeaway: Some(DEVIATION_TAKEAWAY),
534            verify_elsewhere: true,
535        },
536        PegStatus::Deviation => PegWords {
537            label: "Peg deviation",
538            synthesis: Some(DEVIATION_SYNTHESIS),
539            takeaway: Some(DEVIATION_TAKEAWAY),
540            verify_elsewhere: true,
541        },
542        PegStatus::NoPrice => PegWords {
543            label: "Peg unknown (no mid price: a side has no valid level)",
544            synthesis: None,
545            takeaway: None,
546            verify_elsewhere: true,
547        },
548    }
549}
550
551/// The peg line of the insights report. The verdict is the market
552/// summary's own ([`MarketSummary::peg_status`]); this only words it.
553fn peg_line(venue: &str, pair: &str, summary: &MarketSummary) -> String {
554    let label = peg_words(summary.peg_status()).label;
555    let deviation = summary
556        .mid_price
557        .and_then(|mid| summary.peg_deviation_bps(mid))
558        .map(|bps| format!(" (deviation: {bps:.1} bps)"))
559        .unwrap_or_default();
560    format!("- **Market ({venue} {pair}):** {label}{deviation}\n")
561}
562
563/// Meta-analysis: synthesizes observations into an executive summary, key takeaway, and recommendations.
564struct MetaAnalysis {
565    synthesis: String,
566    key_takeaway: String,
567    recommendations: Vec<String>,
568}
569
570/// The report's risk line. A partial score is not a verdict, so an
571/// incomplete assessment lists what had no data instead of a score.
572fn risk_line(risk: &scope::compliance::risk::RiskAssessment) -> String {
573    let emoji = risk.risk_level.emoji();
574    if risk.not_assessed.is_empty() {
575        return format!(
576            "\n- **Risk:** {} {:.1}/10 ({:?})\n",
577            emoji, risk.overall_score, risk.risk_level
578        );
579    }
580    let mut line = format!(
581        "\n- **Risk:** {} not assessed in full. No data for:\n",
582        emoji
583    );
584    for reason in &risk.not_assessed {
585        line.push_str(&format!("  - {}\n", reason));
586    }
587    line
588}
589
590fn meta_analysis_address(
591    is_contract: bool,
592    usd_value: Option<f64>,
593    token_count: usize,
594    risk_score: Option<f32>,
595    risk_level: Option<&scope::compliance::risk::RiskLevel>,
596) -> MetaAnalysis {
597    let mut synthesis_parts = Vec::new();
598    let profile = if is_contract {
599        "contract"
600    } else {
601        "wallet (EOA)"
602    };
603    synthesis_parts.push(format!("A {} on chain.", profile));
604
605    if let Some(usd) = usd_value {
606        if usd > 1_000_000.0 {
607            synthesis_parts.push("Significant value held.".to_string());
608        } else if usd > 10_000.0 {
609            synthesis_parts.push("Moderate value.".to_string());
610        } else if usd < 1.0 {
611            synthesis_parts.push("Minimal value.".to_string());
612        }
613    }
614
615    if token_count > 5 {
616        synthesis_parts.push("Diversified token exposure.".to_string());
617    } else if token_count == 1 && token_count > 0 {
618        synthesis_parts.push("Concentrated in a single token.".to_string());
619    }
620
621    // An incomplete assessment gives no score to read.
622    let (risk_score, risk_level) = match risk_level {
623        Some(scope::compliance::risk::RiskLevel::Incomplete) => (None, None),
624        _ => (risk_score, risk_level),
625    };
626    if let (Some(score), Some(level)) = (risk_score, risk_level) {
627        if score >= 7.0 {
628            synthesis_parts.push(format!("Elevated risk ({:?}).", level));
629        } else if score <= 3.0 {
630            synthesis_parts.push("Low risk profile.".to_string());
631        }
632    }
633
634    let synthesis = if synthesis_parts.is_empty() {
635        "Address analyzed with available on-chain data.".to_string()
636    } else {
637        synthesis_parts.join(" ")
638    };
639
640    let key_takeaway = if let (Some(score), Some(level)) = (risk_score, risk_level) {
641        if score >= 7.0 {
642            format!(
643                "Risk assessment warrants closer scrutiny ({:.1}/10).",
644                score
645            )
646        } else {
647            format!("Overall risk: {:?} ({:.1}/10).", level, score)
648        }
649    } else if is_contract {
650        "Contract address — verify intended interaction before use.".to_string()
651    } else if usd_value.map(|u| u > 100_000.0).unwrap_or(false) {
652        "High-value wallet — standard due diligence applies.".to_string()
653    } else {
654        "Review full report for transaction and token details.".to_string()
655    };
656
657    let mut recommendations = Vec::new();
658    if risk_score.map(|s| s >= 6.0).unwrap_or(false) {
659        recommendations.push("Monitor for unusual transaction patterns.".to_string());
660    }
661    if token_count > 0 {
662        recommendations.push("Verify token contracts before large interactions.".to_string());
663    }
664    if is_contract {
665        recommendations.push("Confirm contract source and audit status.".to_string());
666    }
667
668    MetaAnalysis {
669        synthesis,
670        key_takeaway,
671        recommendations,
672    }
673}
674
675fn meta_analysis_tx(
676    tx_type: &str,
677    status: bool,
678    high_value: bool,
679    _from: &str,
680    _to: Option<&str>,
681) -> MetaAnalysis {
682    let mut synthesis_parts = Vec::new();
683
684    if !status {
685        synthesis_parts.push("Transaction failed.".to_string());
686    }
687
688    synthesis_parts.push(format!("{} between parties.", tx_type));
689
690    if high_value {
691        synthesis_parts.push("High-value transfer.".to_string());
692    }
693
694    let synthesis = synthesis_parts.join(" ");
695
696    let key_takeaway = if !status {
697        "Failed transaction — check revert reason and contract state.".to_string()
698    } else if high_value && tx_type == "Native Transfer" {
699        "Large native transfer — verify recipient and intent.".to_string()
700    } else if high_value {
701        "High-value operation — standard verification recommended.".to_string()
702    } else {
703        format!("Routine {} — review full details if needed.", tx_type)
704    };
705
706    let mut recommendations = Vec::new();
707    if !status {
708        recommendations.push("Inspect contract logs for revert reason.".to_string());
709    }
710    if high_value {
711        recommendations.push("Confirm recipient address and amount.".to_string());
712    }
713    if tx_type.contains("Approval") {
714        recommendations.push("Verify approved spender and allowance amount.".to_string());
715    }
716
717    MetaAnalysis {
718        synthesis,
719        key_takeaway,
720        recommendations,
721    }
722}
723
724fn meta_analysis_token(
725    risk_summary: &report::TokenRiskSummary,
726    is_stablecoin: bool,
727    peg: Option<PegStatus>,
728    top_holder_pct: Option<f64>,
729    liquidity_usd: f64,
730) -> MetaAnalysis {
731    let mut synthesis_parts = Vec::new();
732
733    if risk_summary.score <= 3 {
734        synthesis_parts.push("Low-risk token with healthy metrics.".to_string());
735    } else if risk_summary.score >= 7 {
736        synthesis_parts.push("Elevated risk — multiple concerns identified.".to_string());
737    } else {
738        synthesis_parts.push("Moderate risk — mixed signals.".to_string());
739    }
740
741    let peg_words = peg.filter(|_| is_stablecoin).map(peg_words);
742    if let Some(sentence) = peg_words.as_ref().and_then(|w| w.synthesis) {
743        synthesis_parts.push(sentence.to_string());
744    }
745
746    if top_holder_pct.map(|p| p > 30.0).unwrap_or(false) {
747        synthesis_parts.push("Concentration risk: top holder holds significant share.".to_string());
748    }
749
750    // The liquidity classes of the report and compliance (token_risk).
751    use scope::compliance::token_risk::{LiquidityTier, liquidity_tier};
752    let tier = liquidity_tier(liquidity_usd);
753    match tier {
754        LiquidityTier::Good => synthesis_parts.push("Strong liquidity depth.".to_string()),
755        LiquidityTier::VeryLow | LiquidityTier::Low => {
756            synthesis_parts.push("Limited liquidity — slippage risk for larger trades.".to_string())
757        }
758        LiquidityTier::Moderate => {}
759    }
760
761    let synthesis = synthesis_parts.join(" ");
762
763    let key_takeaway = if risk_summary.score >= 7 {
764        format!(
765            "High risk ({}): {} — exercise caution.",
766            risk_summary.score,
767            risk_summary
768                .concerns
769                .first()
770                .cloned()
771                .unwrap_or_else(|| "multiple factors".to_string())
772        )
773    } else if let Some(takeaway) = peg_words.as_ref().and_then(|w| w.takeaway) {
774        takeaway.to_string()
775    } else if !risk_summary.positives.is_empty() && risk_summary.concerns.is_empty() {
776        "Favorable risk profile — standard diligence applies.".to_string()
777    } else {
778        format!(
779            "Risk {}/10 ({}) — weigh concerns against use case.",
780            risk_summary.score, risk_summary.level
781        )
782    };
783
784    let mut recommendations = Vec::new();
785    if risk_summary.score >= 6 {
786        recommendations
787            .push("Consider smaller position sizes or avoid until risk clears.".to_string());
788    }
789    if top_holder_pct.map(|p| p > 25.0).unwrap_or(false) {
790        recommendations.push("Monitor top holder movements for distribution changes.".to_string());
791    }
792    if is_stablecoin && peg_words.as_ref().is_none_or(|w| w.verify_elsewhere) {
793        recommendations.push("Verify peg across multiple DEX/CEX venues.".to_string());
794    }
795    if matches!(tier, LiquidityTier::VeryLow | LiquidityTier::Low) && risk_summary.score <= 5 {
796        recommendations.push("Use limit orders or split trades to manage slippage.".to_string());
797    }
798
799    MetaAnalysis {
800        synthesis,
801        key_takeaway,
802        recommendations,
803    }
804}
805
806#[cfg(test)]
807mod tests {
808    use super::*;
809    use scope::market::{BookSource, HealthThresholds, OrderBook, OrderBookLevel};
810
811    fn summary_with_peg(peg_target: f64, mid_price: Option<f64>) -> MarketSummary {
812        let level = |price| OrderBookLevel {
813            price,
814            quantity: 1000.0,
815        };
816        let book = OrderBook {
817            pair: "X/USDT".to_string(),
818            source: BookSource::Exchange,
819            bids: vec![level(peg_target)],
820            asks: vec![level(peg_target)],
821        };
822        let thresholds = HealthThresholds {
823            peg_target,
824            ..HealthThresholds::default()
825        };
826        let mut summary = MarketSummary::from_order_book(&book, &thresholds, None);
827        summary.mid_price = mid_price;
828        // A one-level book fails the level checks; these tests judge the peg.
829        summary.healthy = true;
830        summary
831    }
832
833    #[test]
834    fn test_peg_line_uses_the_summary_peg_target_not_one() {
835        // 6 bps above a 0.5 peg is healthy; measured against 1.0 it would be 5000 bps.
836        let on_peg = summary_with_peg(0.5, Some(0.5003));
837        assert_eq!(on_peg.peg_status(), PegStatus::OnPeg);
838        let line = peg_line("v", "X/USDT", &on_peg);
839        assert!(
840            line.contains("Peg healthy") && line.contains("6.0 bps"),
841            "{line}"
842        );
843        assert_eq!(
844            summary_with_peg(0.5, Some(0.5020)).peg_status(),
845            PegStatus::SlightDeviation
846        );
847        assert_eq!(
848            summary_with_peg(0.5, Some(0.52)).peg_status(),
849            PegStatus::Deviation
850        );
851    }
852
853    #[test]
854    fn test_peg_line_never_says_healthy_without_a_mid() {
855        let summary = summary_with_peg(1.0, None);
856        assert_eq!(summary.peg_status(), PegStatus::NoPrice);
857        assert!(!peg_line("v", "X/USDT", &summary).contains("Peg healthy"));
858    }
859
860    #[test]
861    fn test_peg_line_never_says_healthy_when_the_market_summary_is_unhealthy() {
862        let mut summary = summary_with_peg(1.0, Some(1.0));
863        summary.healthy = false;
864        assert_eq!(summary.peg_status(), PegStatus::BookUnhealthy);
865        assert!(!peg_line("v", "X/USDT", &summary).contains("Peg healthy"));
866    }
867
868    #[test]
869    fn test_book_unhealthy_is_worded_the_same_on_every_line() {
870        let mut summary = summary_with_peg(1.0, Some(1.0));
871        summary.healthy = false;
872        let status = summary.peg_status();
873        let line = peg_line("v", "X/USDT", &summary);
874        assert!(line.contains("market health checks fail"), "{line}");
875        let risk = report::TokenRiskSummary {
876            score: 3,
877            level: "Low",
878            emoji: "🟢",
879            concerns: vec![],
880            positives: vec![],
881        };
882        let meta = meta_analysis_token(&risk, true, Some(status), None, 5_000_000.0);
883        assert!(
884            meta.synthesis.contains("health checks fail"),
885            "{}",
886            meta.synthesis
887        );
888        assert!(!meta.synthesis.contains("deviation detected"));
889        assert!(meta.key_takeaway.contains("health checks fail"));
890        assert!(meta.recommendations.iter().any(|r| r.contains("peg")));
891    }
892
893    #[test]
894    fn test_peg_bands_follow_the_configured_peg_range() {
895        // With a range of 0.01, a mid 0.4% off the peg is still on the peg.
896        let mut summary = summary_with_peg(1.0, Some(1.004));
897        summary.peg_range = 0.01;
898        assert_eq!(summary.peg_status(), PegStatus::OnPeg);
899        summary.peg_range = 0.001;
900        assert_eq!(summary.peg_status(), PegStatus::SlightDeviation);
901    }
902
903    use async_trait::async_trait;
904    use scope::chains::{
905        Balance as ChainBalance, ChainClient, ChainClientFactory, DexDataSource,
906        Token as ChainToken, TokenBalance as ChainTokenBalance, Transaction as ChainTransaction,
907    };
908
909    fn quiet() -> crate::cli::output::Output {
910        crate::cli::output::Output::capture().0
911    }
912
913    // ====================================================================
914    // Mock Chain Client for testing run() paths
915    // ====================================================================
916
917    struct MockChainClient;
918
919    #[async_trait]
920    impl ChainClient for MockChainClient {
921        fn chain_name(&self) -> &str {
922            "ethereum"
923        }
924        fn native_token_symbol(&self) -> &str {
925            "ETH"
926        }
927        async fn get_balance(&self, _address: &str) -> scope::error::Result<ChainBalance> {
928            Ok(ChainBalance {
929                raw: "1000000000000000000".to_string(),
930                formatted: "1.0 ETH".to_string(),
931                decimals: 18,
932                symbol: "ETH".to_string(),
933                usd_value: Some(2500.0),
934            })
935        }
936        async fn enrich_balance_usd(&self, balance: &mut ChainBalance) {
937            balance.usd_value = Some(2500.0);
938        }
939        async fn get_transaction(&self, _hash: &str) -> scope::error::Result<ChainTransaction> {
940            Ok(ChainTransaction {
941                hash: "0xabc123def456789012345678901234567890123456789012345678901234abcd"
942                    .to_string(),
943                block_number: Some(12345678),
944                timestamp: Some(1700000000),
945                from: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
946                to: Some("0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string()),
947                value: "1000000000000000000".to_string(),
948                gas_limit: 21000,
949                gas_used: Some(21000),
950                gas_price: "20000000000".to_string(),
951                nonce: 42,
952                input: "0xa9059cbb0000000000000000000000001234".to_string(),
953                status: Some(true),
954            })
955        }
956        async fn get_transactions(
957            &self,
958            _address: &str,
959            _limit: u32,
960        ) -> scope::error::Result<Vec<ChainTransaction>> {
961            Ok(vec![])
962        }
963        async fn get_block_number(&self) -> scope::error::Result<u64> {
964            Ok(12345678)
965        }
966        async fn get_token_balances(
967            &self,
968            _address: &str,
969        ) -> scope::error::Result<Vec<ChainTokenBalance>> {
970            Ok(vec![
971                ChainTokenBalance {
972                    token: ChainToken {
973                        contract_address: "0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string(),
974                        symbol: "USDT".to_string(),
975                        name: "Tether USD".to_string(),
976                        decimals: 6,
977                    },
978                    balance: "1000000".to_string(),
979                    formatted_balance: "1.0".to_string(),
980                    usd_value: Some(1.0),
981                },
982                ChainTokenBalance {
983                    token: ChainToken {
984                        contract_address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".to_string(),
985                        symbol: "USDC".to_string(),
986                        name: "USD Coin".to_string(),
987                        decimals: 6,
988                    },
989                    balance: "5000000".to_string(),
990                    formatted_balance: "5.0".to_string(),
991                    usd_value: Some(5.0),
992                },
993            ])
994        }
995        async fn get_code(&self, _address: &str) -> scope::error::Result<String> {
996            Ok("0x".to_string()) // EOA
997        }
998    }
999
1000    struct MockFactory;
1001
1002    impl ChainClientFactory for MockFactory {
1003        fn http(&self) -> std::sync::Arc<dyn scope::http::HttpClient> {
1004            scope::http::test_transport()
1005        }
1006        fn create_chain_client(&self, _chain: &str) -> scope::error::Result<Box<dyn ChainClient>> {
1007            Ok(Box::new(MockChainClient))
1008        }
1009        fn create_dex_client(&self) -> Box<dyn DexDataSource> {
1010            let http: std::sync::Arc<dyn scope::http::HttpClient> =
1011                std::sync::Arc::new(scope::http::NativeHttpClient::new().unwrap());
1012            scope::chains::DefaultClientFactory {
1013                chains_config: Default::default(),
1014                http,
1015            }
1016            .create_dex_client()
1017        }
1018    }
1019
1020    // Mock that returns a contract address
1021    struct MockContractClient;
1022
1023    #[async_trait]
1024    impl ChainClient for MockContractClient {
1025        fn chain_name(&self) -> &str {
1026            "ethereum"
1027        }
1028        fn native_token_symbol(&self) -> &str {
1029            "ETH"
1030        }
1031        async fn get_balance(&self, _address: &str) -> scope::error::Result<ChainBalance> {
1032            Ok(ChainBalance {
1033                raw: "0".to_string(),
1034                formatted: "0.0 ETH".to_string(),
1035                decimals: 18,
1036                symbol: "ETH".to_string(),
1037                usd_value: Some(0.0),
1038            })
1039        }
1040        async fn enrich_balance_usd(&self, _balance: &mut ChainBalance) {}
1041        async fn get_transaction(&self, hash: &str) -> scope::error::Result<ChainTransaction> {
1042            Ok(ChainTransaction {
1043                hash: hash.to_string(),
1044                block_number: Some(100),
1045                timestamp: Some(1700000000),
1046                from: "0xfrom".to_string(),
1047                to: None, // contract creation
1048                value: "0".to_string(),
1049                gas_limit: 100000,
1050                gas_used: Some(80000),
1051                gas_price: "10000000000".to_string(),
1052                nonce: 0,
1053                input: "0x60806040".to_string(),
1054                status: Some(false), // failed tx
1055            })
1056        }
1057        async fn get_transactions(
1058            &self,
1059            _address: &str,
1060            _limit: u32,
1061        ) -> scope::error::Result<Vec<ChainTransaction>> {
1062            Ok(vec![])
1063        }
1064        async fn get_block_number(&self) -> scope::error::Result<u64> {
1065            Ok(100)
1066        }
1067        async fn get_token_balances(
1068            &self,
1069            _address: &str,
1070        ) -> scope::error::Result<Vec<ChainTokenBalance>> {
1071            Ok(vec![])
1072        }
1073        async fn get_code(&self, _address: &str) -> scope::error::Result<String> {
1074            Ok("0x6080604052".to_string()) // contract
1075        }
1076    }
1077
1078    struct MockContractFactory;
1079
1080    impl ChainClientFactory for MockContractFactory {
1081        fn http(&self) -> std::sync::Arc<dyn scope::http::HttpClient> {
1082            scope::http::test_transport()
1083        }
1084        fn create_chain_client(&self, _chain: &str) -> scope::error::Result<Box<dyn ChainClient>> {
1085            Ok(Box::new(MockContractClient))
1086        }
1087        fn create_dex_client(&self) -> Box<dyn DexDataSource> {
1088            let http: std::sync::Arc<dyn scope::http::HttpClient> =
1089                std::sync::Arc::new(scope::http::NativeHttpClient::new().unwrap());
1090            scope::chains::DefaultClientFactory {
1091                chains_config: Default::default(),
1092                http,
1093            }
1094            .create_dex_client()
1095        }
1096    }
1097
1098    // Mock DexDataSource for token tests
1099    struct MockDexDataSource;
1100
1101    #[async_trait]
1102    impl DexDataSource for MockDexDataSource {
1103        async fn get_token_price(&self, _chain: &str, _address: &str) -> Option<f64> {
1104            Some(1.0)
1105        }
1106
1107        async fn get_native_token_price(&self, _chain: &str) -> Option<f64> {
1108            Some(2500.0)
1109        }
1110
1111        async fn get_token_data(
1112            &self,
1113            _chain: &str,
1114            address: &str,
1115        ) -> scope::error::Result<scope::chains::dex::DexTokenData> {
1116            use scope::chains::{DexPair, PricePoint, VolumePoint};
1117            Ok(scope::chains::dex::DexTokenData {
1118                address: address.to_string(),
1119                symbol: "TEST".to_string(),
1120                name: "Test Token".to_string(),
1121                price_usd: 1.5,
1122                price_change_24h: 5.2,
1123                price_change_6h: 2.1,
1124                price_change_1h: 0.5,
1125                price_change_5m: 0.1,
1126                volume_24h: 1_000_000.0,
1127                volume_6h: 250_000.0,
1128                volume_1h: 50_000.0,
1129                liquidity_usd: 500_000.0,
1130                market_cap: Some(10_000_000.0),
1131                fdv: Some(12_000_000.0),
1132                pairs: vec![DexPair {
1133                    dex_name: "Uniswap V3".to_string(),
1134                    pair_address: "0xpair123".to_string(),
1135                    base_token: "TEST".to_string(),
1136                    quote_token: "USDC".to_string(),
1137                    price_usd: 1.5,
1138                    liquidity_usd: 500_000.0,
1139                    liquidity_base: None,
1140                    volume_24h: 1_000_000.0,
1141                    price_change_24h: 5.2,
1142                    buys_24h: 100,
1143                    sells_24h: 80,
1144                    buys_6h: 20,
1145                    sells_6h: 15,
1146                    buys_1h: 5,
1147                    sells_1h: 3,
1148                    pair_created_at: Some(1690000000),
1149                    url: Some("https://dexscreener.com/ethereum/0xpair123".to_string()),
1150                }],
1151                price_history: vec![PricePoint {
1152                    timestamp: 1690000000,
1153                    price: 1.5,
1154                }],
1155                volume_history: vec![VolumePoint {
1156                    timestamp: 1690000000,
1157                    volume: 1_000_000.0,
1158                }],
1159                total_buys_24h: 100,
1160                total_sells_24h: 80,
1161                total_buys_6h: 20,
1162                total_sells_6h: 15,
1163                total_buys_1h: 5,
1164                total_sells_1h: 3,
1165                earliest_pair_created_at: Some(1690000000),
1166                image_url: None,
1167                websites: Vec::new(),
1168                socials: Vec::new(),
1169                dexscreener_url: Some("https://dexscreener.com/ethereum/test".to_string()),
1170            })
1171        }
1172
1173        async fn search_tokens(
1174            &self,
1175            _query: &str,
1176            _chain: Option<&str>,
1177        ) -> scope::error::Result<Vec<scope::chains::TokenSearchResult>> {
1178            Ok(vec![scope::chains::TokenSearchResult {
1179                address: "0xTEST1234567890123456789012345678901234567".to_string(),
1180                symbol: "TEST".to_string(),
1181                name: "Test Token".to_string(),
1182                chain: "ethereum".to_string(),
1183                price_usd: Some(1.5),
1184                volume_24h: 1_000_000.0,
1185                liquidity_usd: 500_000.0,
1186                market_cap: Some(10_000_000.0),
1187            }])
1188        }
1189    }
1190
1191    // Mock ChainClient that returns holders with high concentration
1192    struct MockTokenChainClient;
1193
1194    #[async_trait]
1195    impl ChainClient for MockTokenChainClient {
1196        fn chain_name(&self) -> &str {
1197            "ethereum"
1198        }
1199        fn native_token_symbol(&self) -> &str {
1200            "ETH"
1201        }
1202        async fn get_balance(&self, _address: &str) -> scope::error::Result<ChainBalance> {
1203            Ok(ChainBalance {
1204                raw: "1000000000000000000".to_string(),
1205                formatted: "1.0 ETH".to_string(),
1206                decimals: 18,
1207                symbol: "ETH".to_string(),
1208                usd_value: Some(2500.0),
1209            })
1210        }
1211        async fn enrich_balance_usd(&self, balance: &mut ChainBalance) {
1212            balance.usd_value = Some(2500.0);
1213        }
1214        async fn get_transaction(&self, _hash: &str) -> scope::error::Result<ChainTransaction> {
1215            Ok(ChainTransaction {
1216                hash: "0xabc123".to_string(),
1217                block_number: Some(12345678),
1218                timestamp: Some(1700000000),
1219                from: "0xfrom".to_string(),
1220                to: Some("0xto".to_string()),
1221                value: "0".to_string(),
1222                gas_limit: 21000,
1223                gas_used: Some(21000),
1224                gas_price: "20000000000".to_string(),
1225                nonce: 42,
1226                input: "0x".to_string(),
1227                status: Some(true),
1228            })
1229        }
1230        async fn get_transactions(
1231            &self,
1232            _address: &str,
1233            _limit: u32,
1234        ) -> scope::error::Result<Vec<ChainTransaction>> {
1235            Ok(vec![])
1236        }
1237        async fn get_block_number(&self) -> scope::error::Result<u64> {
1238            Ok(12345678)
1239        }
1240        async fn get_token_balances(
1241            &self,
1242            _address: &str,
1243        ) -> scope::error::Result<Vec<ChainTokenBalance>> {
1244            Ok(vec![])
1245        }
1246        async fn get_code(&self, _address: &str) -> scope::error::Result<String> {
1247            Ok("0x".to_string())
1248        }
1249        async fn get_token_holders(
1250            &self,
1251            _address: &str,
1252            _limit: u32,
1253        ) -> scope::error::Result<Vec<scope::chains::TokenHolder>> {
1254            // Return holders with high concentration (>30%) to trigger warning
1255            Ok(vec![
1256                scope::chains::TokenHolder {
1257                    address: "0x1111111111111111111111111111111111111111".to_string(),
1258                    balance: "3500000000000000000000000".to_string(),
1259                    formatted_balance: "3500000.0".to_string(),
1260                    percentage: 35.0, // >30% triggers concentration warning
1261                    rank: 1,
1262                },
1263                scope::chains::TokenHolder {
1264                    address: "0x2222222222222222222222222222222222222222".to_string(),
1265                    balance: "1500000000000000000000000".to_string(),
1266                    formatted_balance: "1500000.0".to_string(),
1267                    percentage: 15.0,
1268                    rank: 2,
1269                },
1270                scope::chains::TokenHolder {
1271                    address: "0x3333333333333333333333333333333333333333".to_string(),
1272                    balance: "1000000000000000000000000".to_string(),
1273                    formatted_balance: "1000000.0".to_string(),
1274                    percentage: 10.0,
1275                    rank: 3,
1276                },
1277            ])
1278        }
1279    }
1280
1281    // Factory for token tests with mocks
1282    struct MockTokenFactory;
1283
1284    impl ChainClientFactory for MockTokenFactory {
1285        fn http(&self) -> std::sync::Arc<dyn scope::http::HttpClient> {
1286            scope::http::test_transport()
1287        }
1288        fn create_chain_client(&self, _chain: &str) -> scope::error::Result<Box<dyn ChainClient>> {
1289            Ok(Box::new(MockTokenChainClient))
1290        }
1291        fn create_dex_client(&self) -> Box<dyn DexDataSource> {
1292            Box::new(MockDexDataSource)
1293        }
1294    }
1295
1296    // ====================================================================
1297    // run() function tests with mocks
1298    // ====================================================================
1299
1300    #[tokio::test]
1301    async fn test_run_address_eoa() {
1302        let config = Config::default();
1303        let factory = MockFactory;
1304        let args = InsightsArgs {
1305            target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
1306            token_contract: None,
1307            chain: None,
1308            decode: false,
1309            trace: false,
1310            health: Default::default(),
1311        };
1312        let result = run(args, &config, &factory, &quiet()).await;
1313        assert!(result.is_ok());
1314    }
1315
1316    #[tokio::test]
1317    async fn test_run_address_contract() {
1318        let config = Config::default();
1319        let factory = MockContractFactory;
1320        let args = InsightsArgs {
1321            target: "0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string(),
1322            token_contract: None,
1323            chain: None,
1324            decode: false,
1325            trace: false,
1326            health: Default::default(),
1327        };
1328        let result = run(args, &config, &factory, &quiet()).await;
1329        assert!(result.is_ok());
1330    }
1331
1332    #[tokio::test]
1333    async fn test_run_transaction() {
1334        let config = Config::default();
1335        let factory = MockFactory;
1336        let args = InsightsArgs {
1337            target: "0xabc123def456789012345678901234567890123456789012345678901234abcd"
1338                .to_string(),
1339            token_contract: None,
1340            chain: None,
1341            decode: false,
1342            trace: false,
1343            health: Default::default(),
1344        };
1345        let result = run(args, &config, &factory, &quiet()).await;
1346        assert!(result.is_ok());
1347    }
1348
1349    #[tokio::test]
1350    async fn test_run_transaction_failed() {
1351        let config = Config::default();
1352        let factory = MockContractFactory;
1353        let args = InsightsArgs {
1354            target: "0xabc123def456789012345678901234567890123456789012345678901234abcd"
1355                .to_string(),
1356            token_contract: None,
1357            chain: Some("ethereum".to_string()),
1358            decode: true,
1359            trace: false,
1360            health: Default::default(),
1361        };
1362        let result = run(args, &config, &factory, &quiet()).await;
1363        assert!(result.is_ok());
1364    }
1365
1366    #[tokio::test]
1367    async fn test_run_address_with_chain_override() {
1368        let config = Config::default();
1369        let factory = MockFactory;
1370        let args = InsightsArgs {
1371            target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
1372            token_contract: None,
1373            chain: Some("polygon".to_string()),
1374            decode: false,
1375            trace: false,
1376            health: Default::default(),
1377        };
1378        let result = run(args, &config, &factory, &quiet()).await;
1379        assert!(result.is_ok());
1380    }
1381
1382    #[tokio::test]
1383    async fn test_insights_run_token() {
1384        let config = Config::default();
1385        let factory = MockTokenFactory;
1386        let args = InsightsArgs {
1387            target: "TEST".to_string(),
1388            token_contract: None,
1389            chain: Some("ethereum".to_string()),
1390            decode: false,
1391            trace: false,
1392            health: Default::default(),
1393        };
1394        let (out, cap) = crate::cli::output::Output::capture();
1395        run(args, &config, &factory, &out).await.unwrap();
1396        // format_usd adds the "$" itself; a "$" in the format string printed "$$500.00K".
1397        let text = cap.out();
1398        assert!(text.contains("**Liquidity (24h):** $500.00K"), "{}", text);
1399        assert!(text.contains("**Volume (24h):** $1.00M"), "{}", text);
1400        assert!(!text.contains("$$"), "{}", text);
1401    }
1402
1403    #[tokio::test]
1404    async fn test_insights_run_token_with_concentration_warning() {
1405        let config = Config::default();
1406        let factory = MockTokenFactory;
1407        let args = InsightsArgs {
1408            target: "0xTEST1234567890123456789012345678901234567".to_string(),
1409            token_contract: None,
1410            chain: Some("ethereum".to_string()),
1411            decode: false,
1412            trace: false,
1413            health: Default::default(),
1414        };
1415        let result = run(args, &config, &factory, &quiet()).await;
1416        assert!(result.is_ok());
1417    }
1418
1419    // ====================================================================
1420    // Existing tests below
1421    // ====================================================================
1422
1423    #[test]
1424    fn test_infer_target_evm_address() {
1425        let t = infer_target("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", None);
1426        assert!(matches!(t, InferredTarget::Address { chain } if chain == "ethereum"));
1427    }
1428
1429    #[test]
1430    fn test_infer_target_tron_address() {
1431        let t = infer_target("TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf", None);
1432        assert!(matches!(t, InferredTarget::Address { chain } if chain == "tron"));
1433    }
1434
1435    #[test]
1436    fn test_infer_target_solana_address() {
1437        let t = infer_target("DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy", None);
1438        assert!(matches!(t, InferredTarget::Address { chain } if chain == "solana"));
1439    }
1440
1441    #[test]
1442    fn test_infer_target_evm_tx_hash() {
1443        let t = infer_target(
1444            "0xabc123def456789012345678901234567890123456789012345678901234abcd",
1445            None,
1446        );
1447        assert!(matches!(t, InferredTarget::Transaction { chain } if chain == "ethereum"));
1448    }
1449
1450    #[test]
1451    fn test_infer_target_tron_tx_hash() {
1452        let t = infer_target(
1453            "abc123def456789012345678901234567890123456789012345678901234abcd",
1454            None,
1455        );
1456        assert!(matches!(t, InferredTarget::Transaction { chain } if chain == "tron"));
1457    }
1458
1459    #[test]
1460    fn test_infer_target_token_symbol() {
1461        let t = infer_target("USDC", None);
1462        assert!(matches!(t, InferredTarget::Token { chain } if chain == "ethereum"));
1463    }
1464
1465    #[test]
1466    fn test_infer_target_chain_override() {
1467        let t = infer_target(
1468            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
1469            Some("polygon"),
1470        );
1471        assert!(matches!(t, InferredTarget::Address { chain } if chain == "polygon"));
1472    }
1473
1474    #[test]
1475    fn test_infer_target_token_with_chain_override() {
1476        let t = infer_target("USDC", Some("solana"));
1477        assert!(matches!(t, InferredTarget::Token { chain } if chain == "solana"));
1478    }
1479
1480    #[test]
1481    fn test_classify_tx_type() {
1482        assert_eq!(
1483            classify_tx_type("0xa9059cbb1234...", Some("0xto")),
1484            "ERC-20 Transfer"
1485        );
1486        assert_eq!(
1487            classify_tx_type("0x095ea7b3abcd...", Some("0xto")),
1488            "ERC-20 Approve"
1489        );
1490        assert_eq!(classify_tx_type("0x", Some("0xto")), "Native Transfer");
1491        assert_eq!(classify_tx_type("", None), "Contract Creation");
1492    }
1493
1494    #[test]
1495    fn test_format_tx_value() {
1496        let (fmt, high) = format_tx_value("0xDE0B6B3A7640000", "ethereum"); // 1 ETH
1497        assert!(fmt.contains("1.0") && fmt.contains("ETH"));
1498        assert!(!high);
1499        let (_, high2) = format_tx_value("0x52B7D2DCC80CD2E4000000", "ethereum"); // 100 ETH
1500        assert!(high2);
1501    }
1502
1503    #[test]
1504    fn test_is_stablecoin() {
1505        assert!(is_stablecoin_symbol("USDC"));
1506        assert!(is_stablecoin_symbol("usdt"));
1507        assert!(is_stablecoin_symbol("DAI"));
1508        assert!(is_stablecoin_symbol("BUSD"));
1509        assert!(is_stablecoin_symbol("TUSD"));
1510        assert!(is_stablecoin_symbol("USDP"));
1511        assert!(is_stablecoin_symbol("FRAX"));
1512        assert!(is_stablecoin_symbol("LUSD"));
1513        assert!(is_stablecoin_symbol("GUSD"));
1514        assert!(!is_stablecoin_symbol("ETH"));
1515        assert!(!is_stablecoin_symbol("PEPE"));
1516        assert!(!is_stablecoin_symbol("WBTC"));
1517    }
1518
1519    #[test]
1520    fn test_is_stablecoin_empty_string() {
1521        assert!(!is_stablecoin_symbol(""));
1522    }
1523
1524    #[test]
1525    fn test_is_stablecoin_case_insensitive() {
1526        // to_uppercase() makes comparison case-insensitive
1527        assert!(is_stablecoin_symbol("UsDc"));
1528        assert!(is_stablecoin_symbol("FraX"));
1529        assert!(!is_stablecoin_symbol("SOL")); // SOL is not a stablecoin
1530    }
1531
1532    // ====================================================================
1533    // target_type_label and chain_label tests
1534    // ====================================================================
1535
1536    #[test]
1537    fn test_target_type_label_address() {
1538        let t = InferredTarget::Address {
1539            chain: "ethereum".to_string(),
1540        };
1541        assert_eq!(target_type_label(&t), "Address");
1542    }
1543
1544    #[test]
1545    fn test_target_type_label_transaction() {
1546        let t = InferredTarget::Transaction {
1547            chain: "ethereum".to_string(),
1548        };
1549        assert_eq!(target_type_label(&t), "Transaction");
1550    }
1551
1552    #[test]
1553    fn test_target_type_label_token() {
1554        let t = InferredTarget::Token {
1555            chain: "ethereum".to_string(),
1556        };
1557        assert_eq!(target_type_label(&t), "Token");
1558    }
1559
1560    #[test]
1561    fn test_chain_label_address() {
1562        let t = InferredTarget::Address {
1563            chain: "polygon".to_string(),
1564        };
1565        assert_eq!(chain_label(&t), "polygon");
1566    }
1567
1568    #[test]
1569    fn test_chain_label_transaction() {
1570        let t = InferredTarget::Transaction {
1571            chain: "tron".to_string(),
1572        };
1573        assert_eq!(chain_label(&t), "tron");
1574    }
1575
1576    #[test]
1577    fn test_chain_label_token() {
1578        let t = InferredTarget::Token {
1579            chain: "solana".to_string(),
1580        };
1581        assert_eq!(chain_label(&t), "solana");
1582    }
1583
1584    // ====================================================================
1585    // classify_tx_type — expanded edge cases
1586    // ====================================================================
1587
1588    #[test]
1589    fn test_classify_tx_type_dex_swaps() {
1590        assert_eq!(
1591            classify_tx_type("0x38ed173900000...", Some("0xrouter")),
1592            "DEX Swap"
1593        );
1594        assert_eq!(
1595            classify_tx_type("0x5c11d79500000...", Some("0xrouter")),
1596            "DEX Swap"
1597        );
1598        assert_eq!(
1599            classify_tx_type("0x4a25d94a00000...", Some("0xrouter")),
1600            "DEX Swap"
1601        );
1602        assert_eq!(
1603            classify_tx_type("0x8803dbee00000...", Some("0xrouter")),
1604            "DEX Swap"
1605        );
1606        assert_eq!(
1607            classify_tx_type("0x7ff36ab500000...", Some("0xrouter")),
1608            "DEX Swap"
1609        );
1610        assert_eq!(
1611            classify_tx_type("0x18cbafe500000...", Some("0xrouter")),
1612            "DEX Swap"
1613        );
1614        assert_eq!(
1615            classify_tx_type("0xfb3bdb4100000...", Some("0xrouter")),
1616            "DEX Swap"
1617        );
1618        assert_eq!(
1619            classify_tx_type("0xb6f9de9500000...", Some("0xrouter")),
1620            "DEX Swap"
1621        );
1622    }
1623
1624    #[test]
1625    fn test_classify_tx_type_multicall() {
1626        assert_eq!(
1627            classify_tx_type("0xac9650d800000...", Some("0xcontract")),
1628            "Multicall"
1629        );
1630        assert_eq!(
1631            classify_tx_type("0x5ae401dc00000...", Some("0xcontract")),
1632            "Multicall"
1633        );
1634    }
1635
1636    #[test]
1637    fn test_classify_tx_type_transfer_from() {
1638        assert_eq!(
1639            classify_tx_type("0x23b872dd00000...", Some("0xtoken")),
1640            "ERC-20 Transfer From"
1641        );
1642    }
1643
1644    #[test]
1645    fn test_classify_tx_type_contract_call() {
1646        assert_eq!(
1647            classify_tx_type("0xdeadbeef00000...", Some("0xcontract")),
1648            "Contract Call"
1649        );
1650    }
1651
1652    #[test]
1653    fn test_classify_tx_type_native_transfer_empty() {
1654        assert_eq!(classify_tx_type("", Some("0xrecipient")), "Native Transfer");
1655    }
1656
1657    // ====================================================================
1658    // format_tx_value — expanded edge cases
1659    // ====================================================================
1660
1661    #[test]
1662    fn test_format_tx_value_zero() {
1663        let (fmt, high) = format_tx_value("0x0", "ethereum");
1664        assert!(fmt.contains("0.000000"));
1665        assert!(fmt.contains("ETH"));
1666        assert!(!high);
1667    }
1668
1669    #[test]
1670    fn test_format_tx_value_empty_hex() {
1671        let (fmt, high) = format_tx_value("0x", "ethereum");
1672        assert!(fmt.contains("0.000000"));
1673        assert!(!high);
1674    }
1675
1676    #[test]
1677    fn test_format_tx_value_decimal_string() {
1678        let (fmt, high) = format_tx_value("1000000000000000000", "ethereum"); // 1 ETH
1679        assert!(fmt.contains("1.0"));
1680        assert!(fmt.contains("ETH"));
1681        assert!(!high);
1682    }
1683
1684    #[test]
1685    fn test_format_tx_value_solana() {
1686        let (fmt, high) = format_tx_value("1000000000", "solana"); // 1 SOL (9 decimals)
1687        assert!(fmt.contains("1.0"));
1688        assert!(fmt.contains("SOL"));
1689        assert!(!high);
1690    }
1691
1692    #[test]
1693    fn test_format_tx_value_tron() {
1694        let (fmt, high) = format_tx_value("1000000", "tron"); // 1 TRX (6 decimals)
1695        assert!(fmt.contains("1.0"));
1696        assert!(fmt.contains("TRX"));
1697        assert!(!high);
1698    }
1699
1700    #[test]
1701    fn test_format_tx_value_polygon() {
1702        let (fmt, _) = format_tx_value("1000000000000000000", "polygon");
1703        assert!(fmt.contains("MATIC") || fmt.contains("POL"));
1704    }
1705
1706    #[test]
1707    fn test_format_tx_value_bsc() {
1708        let (fmt, _) = format_tx_value("1000000000000000000", "bsc");
1709        assert!(fmt.contains("BNB"));
1710    }
1711
1712    #[test]
1713    fn test_format_tx_value_arbitrum() {
1714        let (fmt, _) = format_tx_value("1000000000000000000", "arbitrum");
1715        assert!(fmt.contains("ETH"));
1716    }
1717
1718    #[test]
1719    fn test_format_tx_value_optimism() {
1720        let (fmt, _) = format_tx_value("1000000000000000000", "optimism");
1721        assert!(fmt.contains("ETH"));
1722    }
1723
1724    #[test]
1725    fn test_format_tx_value_base() {
1726        let (fmt, _) = format_tx_value("1000000000000000000", "base");
1727        assert!(fmt.contains("ETH"));
1728    }
1729
1730    #[test]
1731    fn test_format_tx_value_aegis() {
1732        // aegis uses 18 decimals; native_symbol returns "???" for unknown chains
1733        let (fmt, _) = format_tx_value("1000000000000000000", "aegis");
1734        assert!(fmt.contains("1.0") || fmt.contains("1.000000"));
1735    }
1736
1737    #[test]
1738    fn test_format_tx_value_unknown_chain_defaults_18_decimals() {
1739        let (fmt, _) = format_tx_value("1000000000000000000", "unknown_chain");
1740        assert!(fmt.contains("1") || fmt.contains("ETH"));
1741    }
1742
1743    #[test]
1744    fn test_format_tx_value_invalid_hex_parse() {
1745        let (fmt, high) = format_tx_value("0xZZZZ", "ethereum");
1746        assert!(fmt.contains("0.000000"));
1747        assert!(!high);
1748    }
1749
1750    #[test]
1751    fn test_format_tx_value_high_value_threshold() {
1752        // > 10 native units = high value
1753        let (_, high) = format_tx_value("11000000000000000000", "ethereum"); // 11 ETH
1754        assert!(high);
1755        let (_, high2) = format_tx_value("10000000000000000000", "ethereum"); // 10 ETH
1756        assert!(!high2); // exactly 10 is not > 10
1757    }
1758
1759    // ====================================================================
1760    // meta_analysis_address tests
1761    // ====================================================================
1762
1763    #[test]
1764    fn test_meta_analysis_address_contract_high_value() {
1765        let meta = meta_analysis_address(true, Some(2_000_000.0), 10, None, None);
1766        assert!(meta.synthesis.contains("contract"));
1767        assert!(meta.synthesis.contains("Significant value"));
1768        assert!(meta.synthesis.contains("Diversified"));
1769        assert!(meta.recommendations.iter().any(|r| r.contains("contract")));
1770    }
1771
1772    #[test]
1773    fn test_risk_line_gives_a_score_only_for_a_complete_assessment() {
1774        use scope::compliance::risk::{RiskAssessment, RiskLevel};
1775        let mut risk = RiskAssessment {
1776            address: "0x1".into(),
1777            chain: "ethereum".into(),
1778            overall_score: 2.4,
1779            risk_level: RiskLevel::Low,
1780            factors: Vec::new(),
1781            not_assessed: Vec::new(),
1782            assessed_at: chrono::Utc::now(),
1783            recommendations: Vec::new(),
1784        };
1785        assert!(
1786            risk_line(&risk).contains("2.4/10 (Low)"),
1787            "{}",
1788            risk_line(&risk)
1789        );
1790
1791        risk.risk_level = RiskLevel::Incomplete;
1792        risk.not_assessed = vec!["Source of Funds: no Etherscan API key".into()];
1793        let line = risk_line(&risk);
1794        assert!(!line.contains("/10"), "{line}");
1795        assert!(line.contains("not assessed in full"), "{line}");
1796        assert!(
1797            line.contains("  - Source of Funds: no Etherscan API key"),
1798            "{line}"
1799        );
1800    }
1801
1802    #[test]
1803    fn test_meta_analysis_does_not_call_an_incomplete_assessment_low() {
1804        // A score from factors without data is not a verdict.
1805        let level = scope::compliance::risk::RiskLevel::Incomplete;
1806        let meta = meta_analysis_address(false, Some(50.0), 0, Some(2.0), Some(&level));
1807        assert!(!meta.synthesis.contains("Low risk"), "{}", meta.synthesis);
1808        assert!(
1809            !meta.key_takeaway.contains("Overall risk"),
1810            "{}",
1811            meta.key_takeaway
1812        );
1813
1814        let low = scope::compliance::risk::RiskLevel::Low;
1815        let meta = meta_analysis_address(false, Some(50.0), 0, Some(2.0), Some(&low));
1816        assert!(meta.synthesis.contains("Low risk"), "{}", meta.synthesis);
1817    }
1818
1819    #[test]
1820    fn test_meta_analysis_address_eoa_moderate_value() {
1821        let meta = meta_analysis_address(false, Some(50_000.0), 3, None, None);
1822        assert!(meta.synthesis.contains("wallet (EOA)"));
1823        assert!(meta.synthesis.contains("Moderate value"));
1824    }
1825
1826    #[test]
1827    fn test_meta_analysis_address_minimal_value() {
1828        let meta = meta_analysis_address(false, Some(0.5), 0, None, None);
1829        assert!(meta.synthesis.contains("Minimal value"));
1830    }
1831
1832    #[test]
1833    fn test_meta_analysis_address_single_token() {
1834        let meta = meta_analysis_address(false, None, 1, None, None);
1835        assert!(meta.synthesis.contains("Concentrated in a single token"));
1836    }
1837
1838    #[test]
1839    fn test_meta_analysis_address_high_risk() {
1840        use scope::compliance::risk::RiskLevel;
1841        let level = RiskLevel::High;
1842        let meta = meta_analysis_address(false, None, 0, Some(8.5), Some(&level));
1843        assert!(meta.synthesis.contains("Elevated risk"));
1844        assert!(meta.key_takeaway.contains("scrutiny"));
1845        assert!(
1846            meta.recommendations
1847                .iter()
1848                .any(|r| r.contains("unusual transaction"))
1849        );
1850    }
1851
1852    #[test]
1853    fn test_meta_analysis_address_low_risk() {
1854        use scope::compliance::risk::RiskLevel;
1855        let level = RiskLevel::Low;
1856        let meta = meta_analysis_address(false, None, 0, Some(2.0), Some(&level));
1857        assert!(meta.synthesis.contains("Low risk"));
1858    }
1859
1860    #[test]
1861    fn test_meta_analysis_address_contract_no_value() {
1862        let meta = meta_analysis_address(true, None, 0, None, None);
1863        assert!(meta.key_takeaway.contains("Contract address"));
1864        assert!(
1865            meta.recommendations
1866                .iter()
1867                .any(|r| r.contains("Confirm contract"))
1868        );
1869    }
1870
1871    #[test]
1872    fn test_meta_analysis_address_high_value_wallet() {
1873        let meta = meta_analysis_address(false, Some(150_000.0), 0, None, None);
1874        assert!(meta.key_takeaway.contains("High-value wallet"));
1875    }
1876
1877    #[test]
1878    fn test_meta_analysis_address_default_takeaway() {
1879        let meta = meta_analysis_address(false, Some(5_000.0), 0, None, None);
1880        assert!(meta.key_takeaway.contains("Review full report"));
1881    }
1882
1883    #[test]
1884    fn test_meta_analysis_address_empty_synthesis() {
1885        let meta = meta_analysis_address(
1886            false,
1887            Some(5_000.0), // moderate value, not significant/minimal
1888            2,             // 2 tokens, not 1, not >5
1889            None,
1890            None,
1891        );
1892        assert!(meta.synthesis.contains("wallet (EOA)"));
1893    }
1894
1895    #[test]
1896    fn test_meta_analysis_address_synthesis_parts_joined() {
1897        let meta = meta_analysis_address(false, None, 0, None, None);
1898        assert!(!meta.synthesis.is_empty());
1899        assert!(meta.synthesis.contains("Address analyzed") || meta.synthesis.contains("wallet"));
1900    }
1901
1902    #[test]
1903    fn test_meta_analysis_address_with_tokens_recommendation() {
1904        let meta = meta_analysis_address(false, None, 3, None, None);
1905        assert!(
1906            meta.recommendations
1907                .iter()
1908                .any(|r| r.contains("Verify token contracts"))
1909        );
1910    }
1911
1912    // ====================================================================
1913    // meta_analysis_tx tests
1914    // ====================================================================
1915
1916    #[test]
1917    fn test_meta_analysis_tx_successful_native_transfer() {
1918        let meta = meta_analysis_tx("Native Transfer", true, false, "0xfrom", Some("0xto"));
1919        assert!(meta.synthesis.contains("Native Transfer"));
1920        assert!(meta.key_takeaway.contains("Routine"));
1921        assert!(meta.recommendations.is_empty());
1922    }
1923
1924    #[test]
1925    fn test_meta_analysis_tx_failed() {
1926        let meta = meta_analysis_tx("Contract Call", false, false, "0xfrom", Some("0xto"));
1927        assert!(meta.synthesis.contains("failed"));
1928        assert!(meta.key_takeaway.contains("Failed transaction"));
1929        assert!(meta.recommendations.iter().any(|r| r.contains("revert")));
1930    }
1931
1932    #[test]
1933    fn test_meta_analysis_tx_high_value_native() {
1934        let meta = meta_analysis_tx("Native Transfer", true, true, "0xfrom", Some("0xto"));
1935        assert!(meta.synthesis.contains("High-value"));
1936        assert!(meta.key_takeaway.contains("Large native transfer"));
1937        assert!(meta.recommendations.iter().any(|r| r.contains("recipient")));
1938    }
1939
1940    #[test]
1941    fn test_meta_analysis_tx_high_value_contract_call() {
1942        let meta = meta_analysis_tx("DEX Swap", true, true, "0xfrom", Some("0xto"));
1943        assert!(meta.key_takeaway.contains("High-value operation"));
1944    }
1945
1946    #[test]
1947    fn test_meta_analysis_tx_erc20_approve() {
1948        let meta = meta_analysis_tx("ERC-20 Approval", true, false, "0xfrom", Some("0xto"));
1949        assert!(meta.recommendations.iter().any(|r| r.contains("spender")));
1950    }
1951
1952    #[test]
1953    fn test_meta_analysis_tx_failed_high_value() {
1954        let meta = meta_analysis_tx("Contract Call", false, true, "0xfrom", Some("0xto"));
1955        assert!(meta.synthesis.contains("failed"));
1956        assert!(meta.synthesis.contains("High-value"));
1957        assert!(meta.recommendations.len() >= 2);
1958    }
1959
1960    // ====================================================================
1961    // meta_analysis_token tests
1962    // ====================================================================
1963
1964    #[test]
1965    fn test_meta_analysis_token_low_risk() {
1966        let summary = report::TokenRiskSummary {
1967            score: 2,
1968            level: "Low",
1969            emoji: "🟢",
1970            concerns: vec![],
1971            positives: vec!["Good liquidity".to_string()],
1972        };
1973        let meta = meta_analysis_token(&summary, false, None, None, 2_000_000.0);
1974        assert!(meta.synthesis.contains("Low-risk"));
1975        assert!(meta.synthesis.contains("Strong liquidity"));
1976        assert!(meta.key_takeaway.contains("Favorable"));
1977    }
1978
1979    #[test]
1980    fn test_meta_analysis_token_high_risk() {
1981        let summary = report::TokenRiskSummary {
1982            score: 8,
1983            level: "High",
1984            emoji: "🔴",
1985            concerns: vec!["Low liquidity".to_string()],
1986            positives: vec![],
1987        };
1988        let meta = meta_analysis_token(&summary, false, None, None, 10_000.0);
1989        assert!(meta.synthesis.contains("Elevated risk"));
1990        assert!(meta.synthesis.contains("Limited liquidity"));
1991        assert!(meta.key_takeaway.contains("High risk"));
1992        assert!(
1993            meta.recommendations
1994                .iter()
1995                .any(|r| r.contains("smaller position"))
1996        );
1997    }
1998
1999    #[test]
2000    fn test_meta_analysis_token_moderate_risk() {
2001        let summary = report::TokenRiskSummary {
2002            score: 5,
2003            level: "Medium",
2004            emoji: "🟡",
2005            concerns: vec!["Some concern".to_string()],
2006            positives: vec!["Some positive".to_string()],
2007        };
2008        let meta = meta_analysis_token(&summary, false, None, None, 500_000.0);
2009        assert!(meta.synthesis.contains("Moderate risk"));
2010        assert!(meta.key_takeaway.contains("Risk 5/10"));
2011    }
2012
2013    #[test]
2014    fn test_meta_analysis_token_stablecoin_healthy_peg() {
2015        let summary = report::TokenRiskSummary {
2016            score: 2,
2017            level: "Low",
2018            emoji: "🟢",
2019            concerns: vec![],
2020            positives: vec!["Stable peg".to_string()],
2021        };
2022        let meta = meta_analysis_token(&summary, true, Some(PegStatus::OnPeg), None, 5_000_000.0);
2023        assert!(meta.synthesis.contains("Stablecoin peg is healthy"));
2024    }
2025
2026    #[test]
2027    fn test_meta_analysis_token_stablecoin_unhealthy_peg() {
2028        let summary = report::TokenRiskSummary {
2029            score: 4,
2030            level: "Medium",
2031            emoji: "🟡",
2032            concerns: vec![],
2033            positives: vec![],
2034        };
2035        let meta = meta_analysis_token(&summary, true, Some(PegStatus::Deviation), None, 500_000.0);
2036        assert!(meta.synthesis.contains("peg deviation"));
2037        assert!(meta.key_takeaway.contains("deviating from peg"));
2038        assert!(meta.recommendations.iter().any(|r| r.contains("peg")));
2039    }
2040
2041    #[test]
2042    fn test_meta_analysis_token_concentration_risk() {
2043        let summary = report::TokenRiskSummary {
2044            score: 5,
2045            level: "Medium",
2046            emoji: "🟡",
2047            concerns: vec![],
2048            positives: vec![],
2049        };
2050        let meta = meta_analysis_token(&summary, false, None, Some(45.0), 500_000.0);
2051        assert!(meta.synthesis.contains("Concentration risk"));
2052        assert!(
2053            meta.recommendations
2054                .iter()
2055                .any(|r| r.contains("top holder"))
2056        );
2057    }
2058
2059    // M7: $75K is low liquidity in the report; the summary says so too.
2060    #[test]
2061    fn test_meta_analysis_names_low_liquidity_as_the_report_does() {
2062        let summary = report::TokenRiskSummary {
2063            score: 3,
2064            level: "Low",
2065            emoji: "🟢",
2066            concerns: vec![],
2067            positives: vec![],
2068        };
2069        let meta = meta_analysis_token(&summary, false, None, None, 75_000.0);
2070        assert!(
2071            meta.synthesis.contains("Limited liquidity"),
2072            "{}",
2073            meta.synthesis
2074        );
2075    }
2076
2077    #[test]
2078    fn test_meta_analysis_token_low_liquidity_low_risk() {
2079        let summary = report::TokenRiskSummary {
2080            score: 3,
2081            level: "Low",
2082            emoji: "🟢",
2083            concerns: vec![],
2084            positives: vec![],
2085        };
2086        let meta = meta_analysis_token(&summary, false, None, None, 50_000.0);
2087        assert!(
2088            meta.recommendations
2089                .iter()
2090                .any(|r| r.contains("limit orders") || r.contains("slippage"))
2091        );
2092    }
2093
2094    #[test]
2095    fn test_meta_analysis_token_stablecoin_no_peg_data() {
2096        let summary = report::TokenRiskSummary {
2097            score: 3,
2098            level: "Low",
2099            emoji: "🟢",
2100            concerns: vec![],
2101            positives: vec![],
2102        };
2103        let meta = meta_analysis_token(&summary, true, None, None, 1_000_000.0);
2104        // When there is no peg status, recommendation should still suggest verifying peg
2105        assert!(meta.recommendations.iter().any(|r| r.contains("peg")));
2106    }
2107
2108    #[test]
2109    fn test_meta_analysis_token_mixed_signals_key_takeaway() {
2110        let summary = report::TokenRiskSummary {
2111            score: 5,
2112            level: "Medium",
2113            emoji: "🟡",
2114            concerns: vec!["Some concern".to_string()],
2115            positives: vec!["Some positive".to_string()],
2116        };
2117        let meta = meta_analysis_token(&summary, false, None, None, 500_000.0);
2118        assert!(meta.key_takeaway.contains("Risk 5/10"));
2119        assert!(meta.key_takeaway.contains("Medium"));
2120    }
2121
2122    // ====================================================================
2123    // infer_target — additional edge cases
2124    // ====================================================================
2125
2126    #[test]
2127    fn test_infer_target_tx_hash_with_chain_override() {
2128        let t = infer_target(
2129            "0xabc123def456789012345678901234567890123456789012345678901234abcd",
2130            Some("polygon"),
2131        );
2132        assert!(matches!(t, InferredTarget::Transaction { chain } if chain == "polygon"));
2133    }
2134
2135    #[test]
2136    fn test_infer_target_whitespace_trimming() {
2137        let t = infer_target("  USDC  ", None);
2138        assert!(matches!(t, InferredTarget::Token { .. }));
2139    }
2140
2141    #[test]
2142    fn test_infer_target_long_token_name() {
2143        let t = infer_target("some-random-token-name", None);
2144        assert!(matches!(t, InferredTarget::Token { chain } if chain == "ethereum"));
2145    }
2146
2147    // ====================================================================
2148    // InsightsArgs struct validation
2149    // ====================================================================
2150
2151    #[test]
2152    fn test_insights_args_debug() {
2153        let args = InsightsArgs {
2154            target: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
2155            token_contract: None,
2156            chain: Some("ethereum".to_string()),
2157            decode: true,
2158            trace: false,
2159            health: Default::default(),
2160        };
2161        let debug_str = format!("{:?}", args);
2162        assert!(debug_str.contains("InsightsArgs"));
2163        assert!(debug_str.contains("0x742d"));
2164    }
2165
2166    // ====================================================================
2167    // Additional tests for classify_tx_type — selector matches and edge cases
2168    // ====================================================================
2169
2170    #[test]
2171    fn test_classify_tx_type_contract_creation() {
2172        assert_eq!(classify_tx_type("0xa9059cbb...", None), "Contract Creation");
2173    }
2174
2175    #[test]
2176    fn test_classify_tx_type_erc20_transfer() {
2177        assert_eq!(
2178            classify_tx_type("0xa9059cbb00000000", Some("0x1234")),
2179            "ERC-20 Transfer"
2180        );
2181    }
2182
2183    #[test]
2184    fn test_classify_tx_type_erc20_approve() {
2185        assert_eq!(
2186            classify_tx_type("0x095ea7b3...", Some("0x1234")),
2187            "ERC-20 Approve"
2188        );
2189    }
2190
2191    #[test]
2192    fn test_classify_tx_type_erc20_transfer_from() {
2193        assert_eq!(
2194            classify_tx_type("0x23b872dd...", Some("0x1234")),
2195            "ERC-20 Transfer From"
2196        );
2197    }
2198
2199    #[test]
2200    fn test_classify_tx_type_dex_swap() {
2201        assert_eq!(
2202            classify_tx_type("0x38ed1739...", Some("0x1234")),
2203            "DEX Swap"
2204        );
2205        assert_eq!(
2206            classify_tx_type("0x7ff36ab5...", Some("0x1234")),
2207            "DEX Swap"
2208        );
2209    }
2210
2211    #[test]
2212    fn test_classify_tx_type_native_transfer() {
2213        assert_eq!(classify_tx_type("0x", Some("0x1234")), "Native Transfer");
2214        assert_eq!(classify_tx_type("", Some("0x1234")), "Native Transfer");
2215    }
2216
2217    #[test]
2218    fn test_classify_tx_type_unknown_contract_call() {
2219        assert_eq!(
2220            classify_tx_type("0xdeadbeef12345678", Some("0x1234")),
2221            "Contract Call"
2222        );
2223    }
2224
2225    // ====================================================================
2226    // Additional tests for format_tx_value
2227    // ====================================================================
2228
2229    #[test]
2230    fn test_format_tx_value_ethereum_wei() {
2231        let (fmt, high) = format_tx_value("1000000000000000000", "ethereum");
2232        assert!(fmt.contains("1.000000"));
2233        assert!(fmt.contains("ETH"));
2234        assert!(!high); // 1 ETH < 10 threshold
2235    }
2236
2237    #[test]
2238    fn test_format_tx_value_hex() {
2239        let (fmt, _) = format_tx_value("0xde0b6b3a7640000", "ethereum");
2240        // 0xde0b6b3a7640000 = 10^18 = 1 ETH
2241        assert!(fmt.contains("ETH"));
2242    }
2243
2244    #[test]
2245    fn test_format_tx_value_high_value() {
2246        // 100 ETH in wei = 100000000000000000000
2247        let (_, high) = format_tx_value("100000000000000000000", "ethereum");
2248        assert!(high); // 100 ETH > 10
2249    }
2250
2251    #[test]
2252    fn test_format_tx_value_zero_decimal() {
2253        let (fmt, high) = format_tx_value("0", "ethereum");
2254        assert!(fmt.contains("0.000000"));
2255        assert!(!high);
2256    }
2257
2258    #[test]
2259    fn test_format_tx_value_solana_additional() {
2260        let (fmt, _) = format_tx_value("1000000000", "solana"); // 1 SOL
2261        assert!(fmt.contains("SOL"));
2262    }
2263
2264    #[test]
2265    fn test_format_tx_value_tron_additional() {
2266        let (fmt, _) = format_tx_value("1000000", "tron"); // 1 TRX
2267        assert!(fmt.contains("TRX"));
2268    }
2269
2270    #[test]
2271    fn test_format_tx_value_empty_hex_additional() {
2272        let (fmt, _) = format_tx_value("0x", "ethereum");
2273        assert!(fmt.contains("0.000000"));
2274    }
2275
2276    // ====================================================================
2277    // Combined tests for target_type_label and chain_label
2278    // ====================================================================
2279
2280    #[test]
2281    fn test_target_type_label_combined() {
2282        assert_eq!(
2283            target_type_label(&InferredTarget::Address {
2284                chain: "eth".to_string()
2285            }),
2286            "Address"
2287        );
2288        assert_eq!(
2289            target_type_label(&InferredTarget::Transaction {
2290                chain: "eth".to_string()
2291            }),
2292            "Transaction"
2293        );
2294        assert_eq!(
2295            target_type_label(&InferredTarget::Token {
2296                chain: "eth".to_string()
2297            }),
2298            "Token"
2299        );
2300    }
2301
2302    #[test]
2303    fn test_chain_label_combined() {
2304        assert_eq!(
2305            chain_label(&InferredTarget::Address {
2306                chain: "ethereum".to_string()
2307            }),
2308            "ethereum"
2309        );
2310        assert_eq!(
2311            chain_label(&InferredTarget::Transaction {
2312                chain: "polygon".to_string()
2313            }),
2314            "polygon"
2315        );
2316        assert_eq!(
2317            chain_label(&InferredTarget::Token {
2318                chain: "solana".to_string()
2319            }),
2320            "solana"
2321        );
2322    }
2323
2324    // ====================================================================
2325    // Additional tests for meta_analysis_address
2326    // ====================================================================
2327
2328    #[test]
2329    fn test_meta_analysis_address_contract_high_risk() {
2330        use scope::compliance::risk::RiskLevel;
2331        let meta = meta_analysis_address(
2332            true,
2333            Some(2_000_000.0),
2334            10,
2335            Some(8.0),
2336            Some(&RiskLevel::High),
2337        );
2338        assert!(meta.synthesis.contains("contract"));
2339        assert!(meta.synthesis.contains("Significant value"));
2340        assert!(meta.key_takeaway.contains("scrutiny"));
2341        assert!(!meta.recommendations.is_empty());
2342    }
2343
2344    #[test]
2345    fn test_meta_analysis_address_wallet_low_risk() {
2346        use scope::compliance::risk::RiskLevel;
2347        let meta = meta_analysis_address(false, Some(0.5), 0, Some(2.0), Some(&RiskLevel::Low));
2348        assert!(meta.synthesis.contains("wallet"));
2349        assert!(meta.synthesis.contains("Minimal value"));
2350    }
2351
2352    #[test]
2353    fn test_meta_analysis_address_no_risk_data() {
2354        let meta = meta_analysis_address(false, None, 0, None, None);
2355        assert!(!meta.synthesis.is_empty());
2356        assert!(meta.key_takeaway.contains("Review full report"));
2357    }
2358
2359    // ====================================================================
2360    // Additional tests for meta_analysis_tx
2361    // ====================================================================
2362
2363    #[test]
2364    fn test_meta_analysis_tx_failed_additional() {
2365        let meta = meta_analysis_tx("Contract Call", false, false, "0x...", Some("0x..."));
2366        assert!(meta.synthesis.contains("failed"));
2367        assert!(meta.key_takeaway.contains("Failed"));
2368    }
2369
2370    #[test]
2371    fn test_meta_analysis_tx_high_value_native_additional() {
2372        let meta = meta_analysis_tx("Native Transfer", true, true, "0x...", Some("0x..."));
2373        assert!(meta.synthesis.contains("High-value"));
2374        assert!(meta.key_takeaway.contains("Large native transfer"));
2375    }
2376
2377    #[test]
2378    fn test_meta_analysis_tx_routine() {
2379        let meta = meta_analysis_tx("ERC-20 Transfer", true, false, "0x...", Some("0x..."));
2380        assert!(meta.key_takeaway.contains("Routine"));
2381    }
2382
2383    // ====================================================================
2384    // Additional tests for meta_analysis_token
2385    // ====================================================================
2386
2387    #[test]
2388    fn test_meta_analysis_token_high_risk_additional() {
2389        let risk = report::TokenRiskSummary {
2390            score: 8,
2391            level: "High",
2392            emoji: "🔴",
2393            concerns: vec!["Low liquidity".to_string()],
2394            positives: vec![],
2395        };
2396        let meta = meta_analysis_token(&risk, false, None, None, 10_000.0);
2397        assert!(meta.synthesis.contains("Elevated risk"));
2398        assert!(meta.key_takeaway.contains("High risk"));
2399    }
2400
2401    #[test]
2402    fn test_meta_analysis_token_stablecoin_peg_healthy() {
2403        let risk = report::TokenRiskSummary {
2404            score: 2,
2405            level: "Low",
2406            emoji: "🟢",
2407            concerns: vec![],
2408            positives: vec!["Strong liquidity".to_string()],
2409        };
2410        let meta = meta_analysis_token(&risk, true, Some(PegStatus::OnPeg), Some(5.0), 5_000_000.0);
2411        assert!(meta.synthesis.contains("peg is healthy"));
2412        assert!(meta.synthesis.contains("Strong liquidity"));
2413    }
2414
2415    #[test]
2416    fn test_meta_analysis_token_stablecoin_peg_unhealthy() {
2417        let risk = report::TokenRiskSummary {
2418            score: 5,
2419            level: "Medium",
2420            emoji: "🟡",
2421            concerns: vec!["Peg deviation".to_string()],
2422            positives: vec![],
2423        };
2424        let meta = meta_analysis_token(
2425            &risk,
2426            true,
2427            Some(PegStatus::Deviation),
2428            Some(40.0),
2429            100_000.0,
2430        );
2431        assert!(meta.synthesis.contains("peg deviation"));
2432        assert!(meta.synthesis.contains("Concentration risk"));
2433    }
2434
2435    #[test]
2436    fn test_meta_analysis_token_high_risk_empty_concerns_uses_multiple_factors() {
2437        let risk = report::TokenRiskSummary {
2438            score: 8,
2439            level: "High",
2440            emoji: "🔴",
2441            concerns: vec![],
2442            positives: vec![],
2443        };
2444        let meta = meta_analysis_token(&risk, false, None, None, 50_000.0);
2445        assert!(
2446            meta.key_takeaway.contains("multiple factors")
2447                || meta.key_takeaway.contains("High risk")
2448        );
2449    }
2450
2451    #[test]
2452    fn test_infer_target_chain_override_with_address() {
2453        let t = infer_target(
2454            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
2455            Some("arbitrum"),
2456        );
2457        assert!(matches!(t, InferredTarget::Address { chain } if chain == "arbitrum"));
2458    }
2459
2460    #[test]
2461    fn test_meta_analysis_tx_approval_contains_approval_in_match() {
2462        let meta = meta_analysis_tx("ERC-20 Approval", true, false, "0xfrom", Some("0xto"));
2463        assert!(
2464            meta.recommendations
2465                .iter()
2466                .any(|r| r.contains("spender") || r.contains("allowance"))
2467        );
2468    }
2469}