// file: kb_lib/src/pair_analytic_signal.rs

//! Pair analytic signal service.

/// One pair-analytic-signal recording result.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PairAnalyticSignalResult {
    /// Related pair id.
    pub pair_id: i64,
    /// Signal kind.
    pub signal_kind: std::string::String,
    /// Timeframe in seconds. Zero means non-bucketed signal.
    pub timeframe_seconds: i64,
    /// Bucket start in unix seconds. Zero means non-bucketed signal.
    pub bucket_start_unix: i64,
    /// Persisted signal id.
    pub pair_analytic_signal_id: i64,
}

/// Pair analytic signal service.
#[derive(Debug, Clone)]
pub struct PairAnalyticSignalService {
    database: std::sync::Arc<crate::Database>,
    persistence: crate::DetectionPersistenceService,
}

impl PairAnalyticSignalService {
    /// Creates a new pair analytic signal service.
    pub fn new(database: std::sync::Arc<crate::Database>) -> Self {
        let persistence = crate::DetectionPersistenceService::new(database.clone());
        return Self { database, persistence };
    }

    /// Records richer analytic signals for one resolved transaction signature.
    pub async fn record_transaction_by_signature(
        &self,
        signature: &str,
    ) -> Result<std::vec::Vec<crate::PairAnalyticSignalResult>, crate::Error> {
        let transaction_result =
            crate::query_chain_transactions_get_by_signature(self.database.as_ref(), signature)
                .await;
        let transaction_option = match transaction_result {
            Ok(transaction_option) => transaction_option,
            Err(error) => return Err(error),
        };
        let transaction = match transaction_option {
            Some(transaction) => transaction,
            None => {
                return Err(crate::Error::InvalidState(format!(
                    "cannot record pair analytic signals for unknown transaction '{}'",
                    signature
                )));
            },
        };
        let transaction_id = match transaction.id {
            Some(transaction_id) => transaction_id,
            None => {
                return Err(crate::Error::InvalidState(format!(
                    "transaction '{}' has no internal id",
                    signature
                )));
            },
        };
        let trade_events_result = crate::query_trade_events_list_by_transaction_id(
            self.database.as_ref(),
            transaction_id,
        )
        .await;
        let trade_events = match trade_events_result {
            Ok(trade_events) => trade_events,
            Err(error) => return Err(error),
        };
        let mut impacted_pairs =
            std::collections::BTreeMap::<i64, std::collections::BTreeSet<i64>>::new();
        for trade_event in &trade_events {
            let event_time_option = self.trade_event_unix_time(trade_event).await;
            let event_time_unix = match event_time_option {
                Ok(event_time_option) => event_time_option,
                Err(error) => return Err(error),
            };
            let event_time_unix = match event_time_unix {
                Some(event_time_unix) => event_time_unix,
                None => continue,
            };
            let bucket_start_result = bucket_start_unix(event_time_unix, 60);
            let bucket_start_unix = match bucket_start_result {
                Ok(bucket_start_unix) => bucket_start_unix,
                Err(error) => return Err(error),
            };
            let entry = impacted_pairs.entry(trade_event.pair_id).or_default();
            entry.insert(bucket_start_unix);
        }
        let mut results = std::vec::Vec::new();
        for (pair_id, bucket_starts) in impacted_pairs {
            let pair_metric_result =
                crate::query_pair_metrics_get_by_pair_id(self.database.as_ref(), pair_id).await;
            let pair_metric_option = match pair_metric_result {
                Ok(pair_metric_option) => pair_metric_option,
                Err(error) => return Err(error),
            };
            if let Some(pair_metric) = pair_metric_option {
                if pair_metric.trade_count == 1 {
                    let signal_result = self
                        .upsert_signal(
                            pair_id,
                            "first_trade_seen".to_string(),
                            crate::AnalysisSignalSeverity::Low,
                            0,
                            0,
                            Some(1.0),
                            serde_json::json!({
                                "pairId": pair_id,
                                "tradeCount": pair_metric.trade_count,
                                "lastSignature": pair_metric.last_signature
                            }),
                            Some(transaction_id),
                        )
                        .await;
                    let signal_id = match signal_result {
                        Ok(signal_id) => signal_id,
                        Err(error) => return Err(error),
                    };
                    results.push(crate::PairAnalyticSignalResult {
                        pair_id,
                        signal_kind: "first_trade_seen".to_string(),
                        timeframe_seconds: 0,
                        bucket_start_unix: 0,
                        pair_analytic_signal_id: signal_id,
                    });
                }
            }
            for bucket_start_unix in bucket_starts {
                let candle_result = crate::query_pair_candles_get_by_key(
                    self.database.as_ref(),
                    pair_id,
                    60,
                    bucket_start_unix,
                )
                .await;
                let candle_option = match candle_result {
                    Ok(candle_option) => candle_option,
                    Err(error) => return Err(error),
                };
                let candle = match candle_option {
                    Some(candle) => candle,
                    None => continue,
                };
                let previous_candle_result = crate::query_pair_candles_get_by_key(
                    self.database.as_ref(),
                    pair_id,
                    60,
                    bucket_start_unix - 60,
                )
                .await;
                let previous_candle_option = match previous_candle_result {
                    Ok(previous_candle_option) => previous_candle_option,
                    Err(error) => return Err(error),
                };
                let trade_burst_result =
                    maybe_build_trade_burst_signal(pair_id, &candle, transaction_id);
                if let Some(signal) = trade_burst_result {
                    let signal_id_result = self.persist_signal(signal.clone()).await;
                    let signal_id = match signal_id_result {
                        Ok(signal_id) => signal_id,
                        Err(error) => return Err(error),
                    };
                    results.push(crate::PairAnalyticSignalResult {
                        pair_id,
                        signal_kind: signal.signal_kind,
                        timeframe_seconds: signal.timeframe_seconds,
                        bucket_start_unix: signal.bucket_start_unix,
                        pair_analytic_signal_id: signal_id,
                    });
                }
                let imbalance_result =
                    maybe_build_buy_sell_imbalance_signal(pair_id, &candle, transaction_id);
                if let Some(signal) = imbalance_result {
                    let signal_id_result = self.persist_signal(signal.clone()).await;
                    let signal_id = match signal_id_result {
                        Ok(signal_id) => signal_id,
                        Err(error) => return Err(error),
                    };
                    results.push(crate::PairAnalyticSignalResult {
                        pair_id,
                        signal_kind: signal.signal_kind,
                        timeframe_seconds: signal.timeframe_seconds,
                        bucket_start_unix: signal.bucket_start_unix,
                        pair_analytic_signal_id: signal_id,
                    });
                }
                if let Some(previous_candle) = previous_candle_option.clone() {
                    let jump_result = maybe_build_price_jump_signal(
                        pair_id,
                        &previous_candle,
                        &candle,
                        transaction_id,
                    );
                    if let Some(signal) = jump_result {
                        let signal_id_result = self.persist_signal(signal.clone()).await;
                        let signal_id = match signal_id_result {
                            Ok(signal_id) => signal_id,
                            Err(error) => return Err(error),
                        };
                        results.push(crate::PairAnalyticSignalResult {
                            pair_id,
                            signal_kind: signal.signal_kind,
                            timeframe_seconds: signal.timeframe_seconds,
                            bucket_start_unix: signal.bucket_start_unix,
                            pair_analytic_signal_id: signal_id,
                        });
                    }
                    let spike_result = maybe_build_volume_spike_signal(
                        pair_id,
                        &previous_candle,
                        &candle,
                        transaction_id,
                    );
                    if let Some(signal) = spike_result {
                        let signal_id_result = self.persist_signal(signal.clone()).await;
                        let signal_id = match signal_id_result {
                            Ok(signal_id) => signal_id,
                            Err(error) => return Err(error),
                        };
                        results.push(crate::PairAnalyticSignalResult {
                            pair_id,
                            signal_kind: signal.signal_kind,
                            timeframe_seconds: signal.timeframe_seconds,
                            bucket_start_unix: signal.bucket_start_unix,
                            pair_analytic_signal_id: signal_id,
                        });
                    }
                }
            }
        }
        if !results.is_empty() {
            let payload = serde_json::json!({
                "transactionSignature": signature,
                "pairAnalyticSignalCount": results.len()
            });
            let observation_result = self
                .persistence
                .record_observation(&crate::DetectionObservationInput::new(
                    "pair.analytic_signal".to_string(),
                    crate::ObservationSourceKind::Dex,
                    transaction.source_endpoint_name.clone(),
                    transaction.signature.clone(),
                    transaction.slot,
                    payload.clone(),
                ))
                .await;
            let observation_id = match observation_result {
                Ok(observation_id) => observation_id,
                Err(error) => return Err(error),
            };
            let generic_signal_result = self
                .persistence
                .record_signal(&crate::DetectionSignalInput::new(
                    "signal.pair.analytic_signal.recorded".to_string(),
                    crate::AnalysisSignalSeverity::Low,
                    transaction.signature.clone(),
                    Some(observation_id),
                    None,
                    payload,
                ))
                .await;
            if let Err(error) = generic_signal_result {
                return Err(error);
            }
        }
        return Ok(results);
    }

    async fn trade_event_unix_time(
        &self,
        trade_event: &crate::TradeEventDto,
    ) -> Result<std::option::Option<i64>, crate::Error> {
        let transaction_result = crate::query_chain_transactions_get_by_signature(
            self.database.as_ref(),
            trade_event.signature.as_str(),
        )
        .await;
        let transaction_option = match transaction_result {
            Ok(transaction_option) => transaction_option,
            Err(error) => return Err(error),
        };
        let transaction = match transaction_option {
            Some(transaction) => transaction,
            None => return Ok(Some(trade_event.created_at.timestamp())),
        };
        match transaction.block_time_unix {
            Some(block_time_unix) => return Ok(Some(block_time_unix)),
            None => return Ok(Some(trade_event.created_at.timestamp())),
        }
    }

    async fn upsert_signal(
        &self,
        pair_id: i64,
        signal_kind: std::string::String,
        severity: crate::AnalysisSignalSeverity,
        timeframe_seconds: i64,
        bucket_start_unix: i64,
        score: std::option::Option<f64>,
        signal_value: serde_json::Value,
        transaction_id: std::option::Option<i64>,
    ) -> Result<i64, crate::Error> {
        let existing_result = crate::query_pair_analytic_signals_get_by_key(
            self.database.as_ref(),
            pair_id,
            signal_kind.as_str(),
            timeframe_seconds,
            bucket_start_unix,
        )
        .await;
        let existing_option = match existing_result {
            Ok(existing_option) => existing_option,
            Err(error) => return Err(error),
        };
        let first_transaction_id = match existing_option {
            Some(existing) => existing.first_transaction_id,
            None => transaction_id,
        };
        let dto = crate::PairAnalyticSignalDto::new(
            pair_id,
            signal_kind,
            severity,
            timeframe_seconds,
            bucket_start_unix,
            score,
            signal_value,
            first_transaction_id,
            transaction_id,
        );
        return crate::query_pair_analytic_signals_upsert(self.database.as_ref(), &dto).await;
    }

    async fn persist_signal(&self, signal: PendingPairAnalyticSignal) -> Result<i64, crate::Error> {
        return self
            .upsert_signal(
                signal.pair_id,
                signal.signal_kind,
                signal.severity,
                signal.timeframe_seconds,
                signal.bucket_start_unix,
                signal.score,
                signal.signal_value,
                signal.transaction_id,
            )
            .await;
    }
}

#[derive(Debug, Clone)]
struct PendingPairAnalyticSignal {
    pair_id: i64,
    signal_kind: std::string::String,
    severity: crate::AnalysisSignalSeverity,
    timeframe_seconds: i64,
    bucket_start_unix: i64,
    score: std::option::Option<f64>,
    signal_value: serde_json::Value,
    transaction_id: std::option::Option<i64>,
}

fn bucket_start_unix(event_time_unix: i64, timeframe_seconds: i64) -> Result<i64, crate::Error> {
    if timeframe_seconds <= 0 {
        return Err(crate::Error::InvalidState(format!(
            "invalid timeframe_seconds '{}'",
            timeframe_seconds
        )));
    }
    return Ok((event_time_unix / timeframe_seconds) * timeframe_seconds);
}

fn maybe_build_trade_burst_signal(
    pair_id: i64,
    candle: &crate::PairCandleDto,
    transaction_id: i64,
) -> std::option::Option<PendingPairAnalyticSignal> {
    if candle.trade_count < 3 {
        return None;
    }
    let severity = if candle.trade_count >= 5 {
        crate::AnalysisSignalSeverity::High
    } else {
        crate::AnalysisSignalSeverity::Medium
    };
    return Some(PendingPairAnalyticSignal {
        pair_id,
        signal_kind: "trade_burst_60s".to_string(),
        severity,
        timeframe_seconds: 60,
        bucket_start_unix: candle.bucket_start_unix,
        score: Some(candle.trade_count as f64),
        signal_value: serde_json::json!({
            "pairId": pair_id,
            "tradeCount": candle.trade_count,
            "buyCount": candle.buy_count,
            "sellCount": candle.sell_count,
            "bucketStartUnix": candle.bucket_start_unix
        }),
        transaction_id: Some(transaction_id),
    });
}

fn maybe_build_buy_sell_imbalance_signal(
    pair_id: i64,
    candle: &crate::PairCandleDto,
    transaction_id: i64,
) -> std::option::Option<PendingPairAnalyticSignal> {
    if candle.trade_count < 3 {
        return None;
    }
    let difference = (candle.buy_count - candle.sell_count).abs();
    let ratio = difference as f64 / candle.trade_count as f64;
    if ratio < 0.75 {
        return None;
    }
    let severity = if ratio >= 0.9 {
        crate::AnalysisSignalSeverity::High
    } else {
        crate::AnalysisSignalSeverity::Medium
    };
    return Some(PendingPairAnalyticSignal {
        pair_id,
        signal_kind: "buy_sell_imbalance_60s".to_string(),
        severity,
        timeframe_seconds: 60,
        bucket_start_unix: candle.bucket_start_unix,
        score: Some(ratio),
        signal_value: serde_json::json!({
            "pairId": pair_id,
            "tradeCount": candle.trade_count,
            "buyCount": candle.buy_count,
            "sellCount": candle.sell_count,
            "imbalanceRatio": ratio,
            "bucketStartUnix": candle.bucket_start_unix
        }),
        transaction_id: Some(transaction_id),
    });
}

fn maybe_build_price_jump_signal(
    pair_id: i64,
    previous_candle: &crate::PairCandleDto,
    candle: &crate::PairCandleDto,
    transaction_id: i64,
) -> std::option::Option<PendingPairAnalyticSignal> {
    if previous_candle.close_price_quote_per_base <= 0.0 {
        return None;
    }
    let ratio = candle.close_price_quote_per_base / previous_candle.close_price_quote_per_base;
    let delta_ratio = ratio - 1.0;
    if delta_ratio >= 0.2 {
        let severity = if delta_ratio >= 0.5 {
            crate::AnalysisSignalSeverity::High
        } else {
            crate::AnalysisSignalSeverity::Medium
        };
        return Some(PendingPairAnalyticSignal {
            pair_id,
            signal_kind: "price_jump_up_60s".to_string(),
            severity,
            timeframe_seconds: 60,
            bucket_start_unix: candle.bucket_start_unix,
            score: Some(delta_ratio),
            signal_value: serde_json::json!({
                "pairId": pair_id,
                "previousClose": previous_candle.close_price_quote_per_base,
                "close": candle.close_price_quote_per_base,
                "deltaRatio": delta_ratio,
                "bucketStartUnix": candle.bucket_start_unix
            }),
            transaction_id: Some(transaction_id),
        });
    }
    if delta_ratio <= -0.2 {
        let severity = if delta_ratio <= -0.5 {
            crate::AnalysisSignalSeverity::High
        } else {
            crate::AnalysisSignalSeverity::Medium
        };
        return Some(PendingPairAnalyticSignal {
            pair_id,
            signal_kind: "price_jump_down_60s".to_string(),
            severity,
            timeframe_seconds: 60,
            bucket_start_unix: candle.bucket_start_unix,
            score: Some(delta_ratio.abs()),
            signal_value: serde_json::json!({
                "pairId": pair_id,
                "previousClose": previous_candle.close_price_quote_per_base,
                "close": candle.close_price_quote_per_base,
                "deltaRatio": delta_ratio,
                "bucketStartUnix": candle.bucket_start_unix
            }),
            transaction_id: Some(transaction_id),
        });
    }
    return None;
}

fn maybe_build_volume_spike_signal(
    pair_id: i64,
    previous_candle: &crate::PairCandleDto,
    candle: &crate::PairCandleDto,
    transaction_id: i64,
) -> std::option::Option<PendingPairAnalyticSignal> {
    let previous_quote_text = match previous_candle.quote_volume_raw.clone() {
        Some(previous_quote_text) => previous_quote_text,
        None => return None,
    };
    let current_quote_text = match candle.quote_volume_raw.clone() {
        Some(current_quote_text) => current_quote_text,
        None => return None,
    };
    let previous_quote_result = previous_quote_text.parse::<f64>();
    let previous_quote = match previous_quote_result {
        Ok(previous_quote) => previous_quote,
        Err(_) => return None,
    };
    if previous_quote <= 0.0 {
        return None;
    }
    let current_quote_result = current_quote_text.parse::<f64>();
    let current_quote = match current_quote_result {
        Ok(current_quote) => current_quote,
        Err(_) => return None,
    };
    let spike_ratio = current_quote / previous_quote;
    if spike_ratio < 2.0 {
        return None;
    }
    let severity = if spike_ratio >= 3.0 {
        crate::AnalysisSignalSeverity::High
    } else {
        crate::AnalysisSignalSeverity::Medium
    };
    return Some(PendingPairAnalyticSignal {
        pair_id,
        signal_kind: "volume_spike_60s".to_string(),
        severity,
        timeframe_seconds: 60,
        bucket_start_unix: candle.bucket_start_unix,
        score: Some(spike_ratio),
        signal_value: serde_json::json!({
            "pairId": pair_id,
            "previousQuoteVolumeRaw": previous_quote_text,
            "quoteVolumeRaw": current_quote_text,
            "spikeRatio": spike_ratio,
            "bucketStartUnix": candle.bucket_start_unix
        }),
        transaction_id: Some(transaction_id),
    });
}

#[cfg(test)]
mod tests {
    async fn make_database() -> std::sync::Arc<crate::Database> {
        let tempdir_result = tempfile::tempdir();
        let tempdir = match tempdir_result {
            Ok(tempdir) => tempdir,
            Err(error) => panic!("tempdir must succeed: {}", error),
        };
        let database_path = tempdir.path().join("pair_analytic_signal.sqlite3");
        let config = crate::DatabaseConfig {
            enabled: true,
            backend: crate::DatabaseBackend::Sqlite,
            sqlite: crate::SqliteDatabaseConfig {
                path: database_path.to_string_lossy().to_string(),
                create_if_missing: true,
                busy_timeout_ms: 5000,
                max_connections: 1,
                auto_initialize_schema: true,
                use_wal: true,
            },
        };
        let database_result = crate::Database::connect_and_initialize(&config).await;
        let database = match database_result {
            Ok(database) => database,
            Err(error) => panic!("database init must succeed: {}", error),
        };
        return std::sync::Arc::new(database);
    }

    async fn seed_fluxbeam_swap_transaction(
        database: std::sync::Arc<crate::Database>,
        signature: &str,
        block_time_unix: i64,
        quote_amount_raw: &str,
        buy_side: bool,
    ) {
        let transaction_model = crate::TransactionModelService::new(database.clone());
        let dex_decode = crate::DexDecodeService::new(database.clone());
        let dex_detect = crate::DexDetectService::new(database.clone());
        let trade_aggregation = crate::TradeAggregationService::new(database.clone());
        let pair_candle_aggregation = crate::PairCandleAggregationService::new(database.clone());
        let log_side = if buy_side { "buy" } else { "sell" };
        let resolved_transaction = serde_json::json!({
            "slot": 970001,
            "blockTime": block_time_unix,
            "version": 0,
            "transaction": {
                "message": {
                    "instructions": [
                        {
                            "programId": crate::FLUXBEAM_PROGRAM_ID,
                            "program": "fluxbeam",
                            "stackHeight": 1,
                            "accounts": [
                                "AnalyticPool111",
                                "AnalyticLpMint111",
                                "AnalyticTokenA111",
                                crate::WSOL_MINT_ID
                            ],
                            "parsed": {
                                "info": {
                                    "instruction": "swap",
                                    "pool": "AnalyticPool111",
                                    "tokenA": "AnalyticTokenA111",
                                    "tokenB": crate::WSOL_MINT_ID,
                                    "baseAmountRaw": "1000",
                                    "quoteAmountRaw": quote_amount_raw
                                }
                            },
                            "data": "opaque"
                        }
                    ]
                }
            },
            "meta": {
                "err": null,
                "logMessages": [
                    "Program log: Instruction: Swap",
                    format!("Program log: {}", log_side)
                ]
            }
        });
        let project_result = transaction_model
            .persist_resolved_transaction(
                signature,
                Some("helius_primary_http".to_string()),
                &resolved_transaction,
            )
            .await;
        if let Err(error) = project_result {
            panic!("projection must succeed: {}", error);
        }
        let decode_result = dex_decode.decode_transaction_by_signature(signature).await;
        if let Err(error) = decode_result {
            panic!("dex decode must succeed: {}", error);
        }
        let detect_result = dex_detect.detect_transaction_by_signature(signature).await;
        if let Err(error) = detect_result {
            panic!("dex detect must succeed: {}", error);
        }
        let trade_result = trade_aggregation.record_transaction_by_signature(signature).await;
        if let Err(error) = trade_result {
            panic!("trade aggregation must succeed: {}", error);
        }
        let candle_result =
            pair_candle_aggregation.record_transaction_by_signature(signature).await;
        if let Err(error) = candle_result {
            panic!("pair candle aggregation must succeed: {}", error);
        }
    }

    #[tokio::test]
    async fn record_transaction_by_signature_creates_first_trade_signal() {
        let database = make_database().await;
        seed_fluxbeam_swap_transaction(
            database.clone(),
            "sig-analytic-first-trade",
            1_700_020_000,
            "2000",
            true,
        )
        .await;
        let service = crate::PairAnalyticSignalService::new(database.clone());
        let record_result =
            service.record_transaction_by_signature("sig-analytic-first-trade").await;
        let results = match record_result {
            Ok(results) => results,
            Err(error) => panic!("analytic signal service must succeed: {}", error),
        };
        let pools_result = crate::query_pools_list(database.as_ref()).await;
        let pools = match pools_result {
            Ok(pools) => pools,
            Err(error) => panic!("pool list must succeed: {}", error),
        };
        let pool_id = pools[0].id.unwrap_or_default();
        let pair_result = crate::query_pairs_get_by_pool_id(database.as_ref(), pool_id).await;
        let pair_option = match pair_result {
            Ok(pair_option) => pair_option,
            Err(error) => panic!("pair fetch must succeed: {}", error),
        };
        let pair = match pair_option {
            Some(pair) => pair,
            None => panic!("pair must exist"),
        };
        let pair_id = pair.id.unwrap_or_default();
        let signals_result =
            crate::query_pair_analytic_signals_list_by_pair_id(database.as_ref(), pair_id).await;
        let signals = match signals_result {
            Ok(signals) => signals,
            Err(error) => panic!("signal list must succeed: {}", error),
        };
        assert_eq!(signals.len(), 1);
        assert_eq!(signals[0].signal_kind, "first_trade_seen".to_string());
        assert_eq!(signals[0].severity, crate::AnalysisSignalSeverity::Low);
        assert!(!results.is_empty());
    }

    #[tokio::test]
    async fn record_transaction_by_signature_creates_bucketed_signals_and_is_idempotent() {
        let database = make_database().await;
        seed_fluxbeam_swap_transaction(
            database.clone(),
            "sig-analytic-bucket-1",
            1_700_021_000,
            "1000",
            true,
        )
        .await;
        seed_fluxbeam_swap_transaction(
            database.clone(),
            "sig-analytic-bucket-2",
            1_700_021_010,
            "1100",
            true,
        )
        .await;
        seed_fluxbeam_swap_transaction(
            database.clone(),
            "sig-analytic-bucket-3",
            1_700_021_020,
            "1200",
            true,
        )
        .await;
        seed_fluxbeam_swap_transaction(
            database.clone(),
            "sig-analytic-bucket-4",
            1_700_021_030,
            "1300",
            true,
        )
        .await;
        seed_fluxbeam_swap_transaction(
            database.clone(),
            "sig-analytic-bucket-5",
            1_700_021_070,
            "15000",
            true,
        )
        .await;
        let service = crate::PairAnalyticSignalService::new(database.clone());
        let record_1 = service.record_transaction_by_signature("sig-analytic-bucket-1").await;
        if let Err(error) = record_1 {
            panic!("analytic signal service call 1 must succeed: {}", error);
        }
        let record_2 = service.record_transaction_by_signature("sig-analytic-bucket-2").await;
        if let Err(error) = record_2 {
            panic!("analytic signal service call 2 must succeed: {}", error);
        }
        let record_3 = service.record_transaction_by_signature("sig-analytic-bucket-3").await;
        if let Err(error) = record_3 {
            panic!("analytic signal service call 3 must succeed: {}", error);
        }
        let record_4 = service.record_transaction_by_signature("sig-analytic-bucket-4").await;
        if let Err(error) = record_4 {
            panic!("analytic signal service call 4 must succeed: {}", error);
        }
        let record_5 = service.record_transaction_by_signature("sig-analytic-bucket-5").await;
        let results = match record_5 {
            Ok(results) => results,
            Err(error) => panic!("analytic signal service call 5 must succeed: {}", error),
        };
        assert!(!results.is_empty());
        let second_result = service.record_transaction_by_signature("sig-analytic-bucket-5").await;
        let second_results = match second_result {
            Ok(second_results) => second_results,
            Err(error) => panic!("second analytic signal service call must succeed: {}", error),
        };
        assert!(!second_results.is_empty());
        let pools_result = crate::query_pools_list(database.as_ref()).await;
        let pools = match pools_result {
            Ok(pools) => pools,
            Err(error) => panic!("pool list must succeed: {}", error),
        };
        let pool_id = pools[0].id.unwrap_or_default();
        let pair_result = crate::query_pairs_get_by_pool_id(database.as_ref(), pool_id).await;
        let pair_option = match pair_result {
            Ok(pair_option) => pair_option,
            Err(error) => panic!("pair fetch must succeed: {}", error),
        };
        let pair = match pair_option {
            Some(pair) => pair,
            None => panic!("pair must exist"),
        };
        let pair_id = pair.id.unwrap_or_default();
        let signals_result =
            crate::query_pair_analytic_signals_list_by_pair_id(database.as_ref(), pair_id).await;
        let signals = match signals_result {
            Ok(signals) => signals,
            Err(error) => panic!("signal list must succeed: {}", error),
        };
        let mut signal_kinds = std::collections::BTreeSet::<std::string::String>::new();
        for signal in &signals {
            signal_kinds.insert(signal.signal_kind.clone());
        }
        assert!(signal_kinds.contains("trade_burst_60s"));
        assert!(signal_kinds.contains("buy_sell_imbalance_60s"));
        assert!(signal_kinds.contains("price_jump_up_60s"));
        assert!(signal_kinds.contains("volume_spike_60s"));
        let trade_burst_result = crate::query_pair_analytic_signals_get_by_key(
            database.as_ref(),
            pair_id,
            "trade_burst_60s",
            60,
            1_700_020_980,
        )
        .await;
        let trade_burst_option = match trade_burst_result {
            Ok(trade_burst_option) => trade_burst_option,
            Err(error) => panic!("trade burst fetch must succeed: {}", error),
        };
        let trade_burst = match trade_burst_option {
            Some(trade_burst) => trade_burst,
            None => panic!("trade burst signal must exist"),
        };
        assert_eq!(trade_burst.severity, crate::AnalysisSignalSeverity::Medium);
        let jump_result = crate::query_pair_analytic_signals_get_by_key(
            database.as_ref(),
            pair_id,
            "price_jump_up_60s",
            60,
            1_700_021_040,
        )
        .await;
        let jump_option = match jump_result {
            Ok(jump_option) => jump_option,
            Err(error) => panic!("jump fetch must succeed: {}", error),
        };
        let jump = match jump_option {
            Some(jump) => jump,
            None => panic!("jump signal must exist"),
        };
        assert!(jump.score.unwrap_or_default() > 1.0 || jump.score.unwrap_or_default() > 0.2);
    }
}
